From 06156a19a77afdbedb6a6753e07ef142d6a7110f Mon Sep 17 00:00:00 2001 From: Rukayat Zakariyau Date: Mon, 11 May 2026 10:38:56 +0100 Subject: [PATCH] Fix and remove reduntant codes --- nest-cli.json | 5 +- package-lock.json | 10728 +++++----------- package.json | 16 +- simple-server.js | 41 + src/ab-testing/ab-testing.service.ts | 133 +- .../analysis/statistical-analysis.service.ts | 386 +- .../automation/automated-decision.service.ts | 395 +- .../experiments/experiment.service.ts | 252 +- .../reporting/ab-testing-reports.service.ts | 298 +- src/app.controller.spec.ts | 18 - src/app.controller.ts | 35 +- src/app.module.ts | 474 +- src/app.service.spec.ts | 29 - src/assessment/assessments.service.spec.ts | 530 - src/assessment/assessments.service.ts | 93 +- .../scoring/score-calculation.service.ts | 1 + src/audit-log/audit-log.controller.ts | 450 - src/audit-log/audit-log.module.ts | 22 - src/audit-log/audit-log.service.spec.ts | 777 -- src/audit-log/audit-log.service.ts | 449 +- .../interceptors/audit-log.interceptor.ts | 120 - .../sensitive-operation.interceptor.ts | 167 - src/audit-log/tests/audit-log.test.ts | 20 - src/auth/auth.controller.ts | 133 - src/auth/auth.module.ts | 76 - src/auth/auth.service.spec.ts | 696 - src/auth/auth.service.ts | 580 - src/auth/dto/auth.dto.ts | 102 - src/auth/guards/roles.guard.ts | 10 +- src/auth/guards/ws-jwt-auth.guard.ts | 100 - src/auth/jwt.strategy.ts | 184 - src/auth/services/password-policy.service.ts | 122 - src/auth/strategies/jwt.strategy.ts | 189 - src/backup/backup.controller.ts | 79 - src/backup/backup.module.ts | 50 - src/backup/backup.service.spec.ts | 564 - src/backup/backup.service.ts | 287 - .../disaster-recovery.service.ts | 99 - .../integrity/data-integrity.service.ts | 92 - .../monitoring/backup-monitoring.service.ts | 119 - .../processing/backup-queue.processor.ts | 290 - .../testing/recovery-testing.service.ts | 252 - .../analytics/cache-analytics.service.ts | 325 - .../cache-management.controller.spec.ts | 229 - src/caching/cache-management.controller.ts | 284 - src/caching/caching.module.ts | 50 - src/caching/caching.service.spec.ts | 192 - src/caching/caching.service.ts | 323 - src/caching/decorators/cache.decorator.ts | 217 - src/caching/interceptors/cache.interceptor.ts | 237 - .../invalidation/invalidation.service.spec.ts | 141 - .../invalidation/invalidation.service.ts | 263 - .../cache-strategies.service.spec.ts | 117 - .../strategies/cache-strategies.service.ts | 407 - src/caching/warming/cache-warming.service.ts | 318 - src/cdn/caching/edge-caching.service.ts | 192 - src/cdn/cdn.controller.ts | 675 - src/cdn/cdn.module.ts | 58 - src/cdn/cdn.service.ts | 273 - src/cdn/geo/geo-location.service.ts | 287 - .../asset-optimization.service.ts | 268 - src/cdn/providers/aws-cloudfront.service.ts | 332 - src/cdn/providers/cloudflare.service.ts | 375 - src/collaboration/collaboration.controller.ts | 305 - src/collaboration/collaboration.module.ts | 33 - src/collaboration/collaboration.service.ts | 50 - .../documents/shared-document.service.ts | 276 - .../gateway/collaboration.gateway.ts | 146 - .../collaboration-permissions.service.ts | 228 - .../versioning/version-control.service.ts | 322 - .../whiteboard/whiteboard.service.ts | 354 - src/common/csrf/csrf.controller.ts | 72 - src/common/csrf/csrf.module.ts | 23 - src/common/csrf/csrf.service.ts | 93 - src/common/database/database.module.ts | 18 - .../examples/booking-transaction.example.ts | 193 - .../examples/payment-transaction.example.ts | 149 - .../examples/voting-transaction.example.ts | 300 - src/common/database/sharding.spec.ts | 235 - .../cross-shard-query-coordinator.ts | 338 - .../decorators/shard-aware.decorator.ts | 77 - .../examples/cross-shard-sync.example.ts | 150 - .../examples/shard-payment.example.ts | 118 - .../examples/shard-user-repository.example.ts | 78 - .../database/sharding/hash/shard.hash.ts | 209 - src/common/database/sharding/index.ts | 48 - .../repository/shard-aware-repository.ts | 162 - .../database/sharding/router/shard.router.ts | 220 - .../services/shard-management.service.ts | 229 - .../sharding/shard-transaction.service.ts | 224 - .../database/sharding/sharding.module.ts | 76 - src/common/database/transaction.service.ts | 230 - .../database/transactional.decorator.ts | 42 - .../database/transactional.interceptor.ts | 34 - src/common/dto/pagination.dto.spec.ts | 33 - .../examples/timeout-example.controller.ts | 63 - .../transaction-management.example.ts | 235 - src/common/export/export.controller.ts | 92 - src/common/export/export.service.ts | 567 - src/common/guards/roles.guard.spec.ts | 57 - src/common/guards/roles.guard.ts | 25 - src/common/guards/ws-throttler.guard.ts | 22 - .../api-deprecation.interceptor.ts | 80 - .../api-version.interceptor.spec.ts | 24 - .../interceptors/api-version.interceptor.ts | 88 - src/common/interceptors/cache.interceptor.ts | 32 - .../global-exception.filter.spec.ts | 54 - .../interceptors/global-exception.filter.ts | 290 - .../interceptors/logging.interceptor.spec.ts | 34 - .../interceptors/logging.interceptor.ts | 168 - .../interceptors/monitoring.interceptor.ts | 28 - .../response-transform.interceptor.ts | 75 - .../interceptors/timeout.interceptor.ts | 84 - src/common/lazy-loading/index.ts | 3 - .../lazy-loading/lazy-loading.module.ts | 25 - .../lazy-module-loader.service.ts | 229 - .../lazy-loading/startup-logger.service.ts | 286 - src/common/middleware/csrf.middleware.ts | 58 - src/common/modules/api-versioning.module.ts | 72 - .../services/idempotency.service.spec.ts | 84 - src/common/timeout/timeout-config.service.ts | 191 - src/common/timeout/timeout.controller.ts | 98 - src/common/timeout/timeout.module.ts | 15 - src/common/utils/correlation.utils.spec.ts | 40 - src/common/utils/pagination.util.spec.ts | 276 - src/common/utils/pagination.util.ts | 231 - src/common/utils/sanitization.utils.spec.ts | 24 - src/common/utils/websocket.utils.ts | 87 - .../is-strong-password.validator.ts | 5 - .../validators/password.validator.spec.ts | 34 - src/common/validators/password.validator.ts | 101 - src/config/feature-flags.config.spec.ts | 154 - src/config/swagger.config.ts | 39 - src/courses/courses.controller.ts | 161 - src/courses/courses.module.ts | 29 - src/courses/courses.service.spec.ts | 177 - src/courses/courses.service.ts | 268 - .../enrollments/enrollments.service.ts | 77 - src/courses/guards/ws-jwt-auth.guard.ts | 26 - .../search-sync/course-search-sync.service.ts | 40 - .../data-warehouse.controller.ts | 369 - src/data-warehouse/data-warehouse.module.ts | 31 - .../etl/etl-pipeline.service.ts | 478 - .../lineage/data-lineage.service.ts | 709 - .../loading/incremental-loader.service.ts | 570 - .../modeling/dimensional-modeling.service.ts | 468 - .../quality/data-quality.service.ts | 747 -- .../ab-testing/ab-testing.controller.ts | 90 - .../ab-testing/ab-testing.service.ts | 276 - .../analytics/email-analytics.controller.ts | 76 - .../analytics/email-analytics.service.ts | 481 - .../email-marketing.controller.ts | 172 - src/email-marketing/email-marketing.module.ts | 91 - .../email-marketing.service.ts | 220 - .../processors/email-queue.processor.ts | 166 - .../segmentation/segment.controller.ts | 123 - .../segmentation/segmentation.service.ts | 422 - .../sender/email-sender.service.ts | 266 - .../tracking/tracking.controller.ts | 182 - .../analytics/flag-analytics.service.ts | 252 - .../evaluation/flag-evaluation.service.ts | 227 - .../experimentation.service.ts | 162 - src/feature-flags/feature-flags.controller.ts | 166 - src/feature-flags/feature-flags.module.ts | 30 - src/feature-flags/rollout/rollout.service.ts | 83 - .../targeting/targeting.service.ts | 230 - .../challenges/challenges.service.ts | 47 - src/gamification/gamification.controller.ts | 76 - src/gamification/gamification.module.ts | 41 - src/gamification/gamification.service.ts | 24 - src/gateways/messaging/messaging.gateway.ts | 69 - .../notifications/notifications.gateway.ts | 130 - src/graphql/graphql.module.ts | 129 - .../middleware/dataloader.middleware.ts | 24 - src/graphql/resolvers/course.resolver.ts | 27 - src/graphql/resolvers/index.ts | 6 - src/graphql/resolvers/mutation.resolver.ts | 163 - src/graphql/resolvers/query.resolver.ts | 150 - src/graphql/resolvers/user.resolver.ts | 26 - .../services/complexity-analysis.service.ts | 117 - src/graphql/services/dataloader.service.ts | 76 - .../services/query-complexity.service.ts | 195 - src/health/health.controller.ts | 205 - src/health/health.module.ts | 14 - src/health/health.service.ts | 576 - .../learning-paths.controller.ts | 20 - src/learning-paths/learning-paths.module.ts | 22 - src/learning-paths/learning-paths.service.ts | 27 - .../services/path-generation.service.ts | 19 - .../language-detection.service.spec.ts | 70 - .../language-detection.service.ts | 167 - src/localization/language.middleware.ts | 30 - src/localization/localization.controller.ts | 170 - src/localization/localization.module.ts | 35 - src/localization/localization.service.spec.ts | 155 - src/localization/localization.service.ts | 390 - src/main.ts | 364 +- src/media/file-cleanup.task.ts | 26 - src/media/media.controller.spec.ts | 46 - src/media/media.controller.ts | 236 - src/media/media.module.ts | 49 - src/media/media.service.spec.ts | 84 - src/media/media.service.ts | 544 - .../processing/document-processing.service.ts | 55 - .../processing/image-processing.service.ts | 425 - src/media/processing/video.processor.ts | 216 - src/media/storage/file-storage.service.ts | 139 - .../file-upload-validation.service.ts | 184 - .../validation/file-validation.service.ts | 510 - .../validation/malware-scanning.service.ts | 314 - .../validation/upload-validation.util.spec.ts | 27 - .../circuit-breaker.service.ts | 283 - .../discovery/service-discovery.service.ts | 176 - src/messaging/event-bus/event-bus.service.ts | 98 - src/messaging/messaging.module.ts | 45 - .../conflicts/conflict-resolution.service.ts | 207 - .../environments/environment-sync.service.ts | 176 - src/migrations/migration-runner.service.ts | 43 - src/migrations/migration.controller.ts | 220 - src/migrations/migration.module.ts | 36 - src/migrations/migration.registry.ts | 27 - src/migrations/migration.service.spec.ts | 215 - src/migrations/migration.service.ts | 313 - .../rollback/rollback.service.spec.ts | 191 - src/migrations/rollback/rollback.service.ts | 200 - .../001-create-users-table.migration.ts | 82 - .../002-create-courses-table.migration.ts | 62 - ...3-create-course-modules-table.migration.ts | 54 - .../004-create-lessons-table.migration.ts | 57 - .../005-create-enrollments-table.migration.ts | 69 - ...ate-migrations-tracking-table.migration.ts | 65 - ...-add-payment-idempotency-keys.migration.ts | 60 - .../samples/sample-user-table.migration.ts | 56 - ...setup-sharding-infrastructure.migration.ts | 114 - .../schema-validation.service.spec.ts | 94 - .../validation/schema-validation.service.ts | 187 - src/moderation/moderation.module.ts | 26 - src/moderation/moderation.service.ts | 33 - src/monitoring/logging/typeorm-logger.ts | 88 - src/monitoring/monitoring.controller.ts | 182 - src/monitoring/monitoring.module.ts | 41 - src/monitoring/monitoring.service.ts | 39 - .../optimization/optimization.service.ts | 23 - .../performance-analysis.service.ts | 45 - .../scheduled-task-monitoring.service.ts | 277 - src/notifications/email/email.processor.ts | 37 - src/notifications/email/email.service.spec.ts | 74 - src/notifications/email/email.service.ts | 174 - .../notification-templates.service.ts | 69 - src/notifications/notifications.controller.ts | 122 - src/notifications/notifications.module.ts | 47 - src/notifications/notifications.service.ts | 414 - .../anomaly/anomaly-detection.service.ts | 625 - .../logging/log-aggregation.service.ts | 321 - .../metrics/metrics-analysis.service.ts | 369 - src/observability/observability.controller.ts | 206 - src/observability/observability.module.ts | 40 - src/observability/observability.service.ts | 130 - .../tracing/distributed-tracing.service.ts | 407 - src/onboarding/onboarding.controller.ts | 210 - src/onboarding/onboarding.module.ts | 21 - src/onboarding/onboarding.service.spec.ts | 377 - src/onboarding/onboarding.service.ts | 362 - .../service-discovery.service.spec.ts | 39 - .../health/health-checker.service.ts | 27 - src/orchestration/orchestration.module.ts | 31 - .../service-mesh/service-mesh.service.spec.ts | 58 - .../service-mesh/service-mesh.service.ts | 80 - .../workflow/workflow-engine.service.ts | 32 - .../interfaces/payment-provider.interface.ts | 60 - src/payments/payments.controller.spec.ts | 58 - src/payments/payments.controller.ts | 179 - src/payments/payments.module.ts | 70 - src/payments/payments.service.spec.ts | 410 - src/payments/payments.service.ts | 409 - .../providers/provider-factory.service.ts | 18 - src/payments/providers/stripe.service.ts | 58 - .../webhooks/webhook-management.controller.ts | 76 - .../webhooks/webhook-queue.service.spec.ts | 155 - .../webhooks/webhook-queue.service.ts | 145 - .../webhooks/webhook-retry.e2e-spec.ts | 206 - .../webhooks/webhook-retry.processor.spec.ts | 155 - .../webhooks/webhook-retry.processor.ts | 221 - .../webhooks/webhook-security.service.spec.ts | 287 - src/payments/webhooks/webhook.controller.ts | 76 - src/payments/webhooks/webhook.service.ts | 157 - .../monitoring/queue-monitoring.service.ts | 414 - .../processors/default-queue.processor.ts | 105 - src/queues/queue.controller.ts | 367 - src/queues/queue.module.ts | 54 - src/queues/queue.service.ts | 174 - src/queues/queue.spec.ts | 1122 -- src/queues/retry/retry-logic.service.ts | 226 - src/queues/scheduler/job-scheduler.service.ts | 263 - .../rate-limiting.controller.spec.ts | 25 - src/rate-limiting/rate-limiting.controller.ts | 65 - src/rate-limiting/rate-limiting.service.ts | 64 - .../services/distrubutes.service.ts | 41 - .../services/limit-guard/guard.ts | 26 - .../services/rate-limiting.module.ts | 22 - .../services/throttling.service.ts | 28 - .../autocomplete/autocomplete.service.ts | 43 - .../elasticsearch.service.spec.ts | 98 - src/search/filters/search-filters.service.ts | 60 - src/search/indexing/indexing.service.ts | 236 - .../search-index-optimizer.service.ts | 204 - src/search/search.controller.spec.ts | 36 - src/search/search.controller.ts | 57 +- src/search/search.module.ts | 35 +- src/search/search.service.spec.ts | 95 - src/search/search.service.ts | 516 +- src/security/compliance/compliance.service.ts | 8 +- src/security/encryption/encryption.service.ts | 68 +- .../secrets/secrets-manager.service.spec.ts | 56 - src/session/session.service.ts | 66 +- .../cache/cache-invalidation.service.spec.ts | 57 - src/sync/cache/cache-invalidation.service.ts | 22 +- .../conflict-resolution.service.spec.ts | 80 - .../data-consistency.service.spec.ts | 53 - .../consistency/data-consistency.service.ts | 103 +- .../replication/replication.service.spec.ts | 47 - src/sync/replication/replication.service.ts | 77 +- src/tenancy/admin/tenant-admin.service.ts | 153 +- src/tenancy/billing/tenant-billing.service.ts | 11 +- src/tenancy/entities/tenant-config.entity.ts | 71 +- .../entities/tenant-customization.entity.ts | 78 +- src/tenancy/guards/tenant.guard.ts | 7 + src/tenancy/isolation/isolation.service.ts | 13 +- src/tenancy/tenancy.guard.ts | 2 +- src/tenancy/tenancy.service.ts | 304 +- src/users/dto/bulk-user.dto.ts | 22 - src/users/dto/create-user.dto.ts | 34 - src/users/dto/update-user.dto.ts | 17 - src/users/users.controller.ts | 156 - src/users/users.module.ts | 43 - src/users/users.service.spec.ts | 137 - src/users/users.service.ts | 299 - src/utils/masking/masking.spec.ts | 136 - src/workers/base/base.worker.spec.ts | 229 - .../worker-health-check.service.spec.ts | 174 - .../health/worker-health-check.service.ts | 2 +- .../worker-orchestration.service.spec.ts | 251 - .../worker-orchestration.service.ts | 16 +- .../backup-processing.worker.spec.ts | 129 - .../processors/data-sync.worker.spec.ts | 84 - src/workers/processors/email.worker.spec.ts | 83 - .../media-processing.worker.spec.ts | 148 - .../processors/subscriptions.worker.spec.ts | 193 - .../processors/webhooks.worker.spec.ts | 99 - test/analytics/analytics.service.spec.js | 16 + test/analytics/analytics.service.spec.js.map | 1 + test/app.e2e-spec.js | 81 + test/app.e2e-spec.js.map | 1 + test/auth.e2e-spec.ts | 313 - test/mocks/uuid.js | 6 + test/mocks/uuid.js.map | 1 + test/monitoring/cost-tracking.service.spec.js | 21 + .../cost-tracking.service.spec.js.map | 1 + test/setup.js | 49 + test/setup.js.map | 1 + test/utils/flaky-test-detector.js | 146 + test/utils/flaky-test-detector.js.map | 1 + test/utils/http-outcome-assertions.js | 31 + test/utils/http-outcome-assertions.js.map | 1 + test/utils/index.js | 19 + test/utils/index.js.map | 1 + test/utils/mock-factories.js | 333 + test/utils/mock-factories.js.map | 1 + test/utils/module-test-cases.js | 21 + test/utils/module-test-cases.js.map | 1 + test/utils/test-database.service.js | 75 + test/utils/test-database.service.js.map | 1 + test/utils/test-http-client.js | 147 + test/utils/test-http-client.js.map | 1 + test/utils/test-retry-helper.js | 154 + test/utils/test-retry-helper.js.map | 1 + tmp_tsc_errors.txt | 120 - tsconfig.build.json | 4 +- tsconfig.build.tsbuildinfo | 2 +- tsconfig.json | 7 +- 380 files changed, 5173 insertions(+), 63630 deletions(-) create mode 100644 simple-server.js delete mode 100644 src/app.controller.spec.ts delete mode 100644 src/app.service.spec.ts delete mode 100644 src/assessment/assessments.service.spec.ts delete mode 100644 src/audit-log/audit-log.controller.ts delete mode 100644 src/audit-log/audit-log.module.ts delete mode 100644 src/audit-log/audit-log.service.spec.ts delete mode 100644 src/audit-log/interceptors/audit-log.interceptor.ts delete mode 100644 src/audit-log/interceptors/sensitive-operation.interceptor.ts delete mode 100644 src/audit-log/tests/audit-log.test.ts delete mode 100644 src/auth/auth.controller.ts delete mode 100644 src/auth/auth.module.ts delete mode 100644 src/auth/auth.service.spec.ts delete mode 100644 src/auth/auth.service.ts delete mode 100644 src/auth/dto/auth.dto.ts delete mode 100644 src/auth/guards/ws-jwt-auth.guard.ts delete mode 100644 src/auth/jwt.strategy.ts delete mode 100644 src/auth/services/password-policy.service.ts delete mode 100644 src/auth/strategies/jwt.strategy.ts delete mode 100644 src/backup/backup.controller.ts delete mode 100644 src/backup/backup.module.ts delete mode 100644 src/backup/backup.service.spec.ts delete mode 100644 src/backup/backup.service.ts delete mode 100644 src/backup/disaster-recovery/disaster-recovery.service.ts delete mode 100644 src/backup/integrity/data-integrity.service.ts delete mode 100644 src/backup/monitoring/backup-monitoring.service.ts delete mode 100644 src/backup/processing/backup-queue.processor.ts delete mode 100644 src/backup/testing/recovery-testing.service.ts delete mode 100644 src/caching/analytics/cache-analytics.service.ts delete mode 100644 src/caching/cache-management.controller.spec.ts delete mode 100644 src/caching/cache-management.controller.ts delete mode 100644 src/caching/caching.module.ts delete mode 100644 src/caching/caching.service.spec.ts delete mode 100644 src/caching/caching.service.ts delete mode 100644 src/caching/decorators/cache.decorator.ts delete mode 100644 src/caching/interceptors/cache.interceptor.ts delete mode 100644 src/caching/invalidation/invalidation.service.spec.ts delete mode 100644 src/caching/invalidation/invalidation.service.ts delete mode 100644 src/caching/strategies/cache-strategies.service.spec.ts delete mode 100644 src/caching/strategies/cache-strategies.service.ts delete mode 100644 src/caching/warming/cache-warming.service.ts delete mode 100644 src/cdn/caching/edge-caching.service.ts delete mode 100644 src/cdn/cdn.controller.ts delete mode 100644 src/cdn/cdn.module.ts delete mode 100644 src/cdn/cdn.service.ts delete mode 100644 src/cdn/geo/geo-location.service.ts delete mode 100644 src/cdn/optimization/asset-optimization.service.ts delete mode 100644 src/cdn/providers/aws-cloudfront.service.ts delete mode 100644 src/cdn/providers/cloudflare.service.ts delete mode 100644 src/collaboration/collaboration.controller.ts delete mode 100644 src/collaboration/collaboration.module.ts delete mode 100644 src/collaboration/collaboration.service.ts delete mode 100644 src/collaboration/documents/shared-document.service.ts delete mode 100644 src/collaboration/gateway/collaboration.gateway.ts delete mode 100644 src/collaboration/permissions/collaboration-permissions.service.ts delete mode 100644 src/collaboration/versioning/version-control.service.ts delete mode 100644 src/collaboration/whiteboard/whiteboard.service.ts delete mode 100644 src/common/csrf/csrf.controller.ts delete mode 100644 src/common/csrf/csrf.module.ts delete mode 100644 src/common/csrf/csrf.service.ts delete mode 100644 src/common/database/database.module.ts delete mode 100644 src/common/database/examples/booking-transaction.example.ts delete mode 100644 src/common/database/examples/payment-transaction.example.ts delete mode 100644 src/common/database/examples/voting-transaction.example.ts delete mode 100644 src/common/database/sharding.spec.ts delete mode 100644 src/common/database/sharding/coordinator/cross-shard-query-coordinator.ts delete mode 100644 src/common/database/sharding/decorators/shard-aware.decorator.ts delete mode 100644 src/common/database/sharding/examples/cross-shard-sync.example.ts delete mode 100644 src/common/database/sharding/examples/shard-payment.example.ts delete mode 100644 src/common/database/sharding/examples/shard-user-repository.example.ts delete mode 100644 src/common/database/sharding/hash/shard.hash.ts delete mode 100644 src/common/database/sharding/index.ts delete mode 100644 src/common/database/sharding/repository/shard-aware-repository.ts delete mode 100644 src/common/database/sharding/router/shard.router.ts delete mode 100644 src/common/database/sharding/services/shard-management.service.ts delete mode 100644 src/common/database/sharding/shard-transaction.service.ts delete mode 100644 src/common/database/sharding/sharding.module.ts delete mode 100644 src/common/database/transaction.service.ts delete mode 100644 src/common/database/transactional.decorator.ts delete mode 100644 src/common/database/transactional.interceptor.ts delete mode 100644 src/common/dto/pagination.dto.spec.ts delete mode 100644 src/common/examples/timeout-example.controller.ts delete mode 100644 src/common/examples/transaction-management.example.ts delete mode 100644 src/common/export/export.controller.ts delete mode 100644 src/common/export/export.service.ts delete mode 100644 src/common/guards/roles.guard.spec.ts delete mode 100644 src/common/guards/roles.guard.ts delete mode 100644 src/common/guards/ws-throttler.guard.ts delete mode 100644 src/common/interceptors/api-deprecation.interceptor.ts delete mode 100644 src/common/interceptors/api-version.interceptor.spec.ts delete mode 100644 src/common/interceptors/api-version.interceptor.ts delete mode 100644 src/common/interceptors/cache.interceptor.ts delete mode 100644 src/common/interceptors/global-exception.filter.spec.ts delete mode 100644 src/common/interceptors/global-exception.filter.ts delete mode 100644 src/common/interceptors/logging.interceptor.spec.ts delete mode 100644 src/common/interceptors/logging.interceptor.ts delete mode 100644 src/common/interceptors/monitoring.interceptor.ts delete mode 100644 src/common/interceptors/response-transform.interceptor.ts delete mode 100644 src/common/interceptors/timeout.interceptor.ts delete mode 100644 src/common/lazy-loading/index.ts delete mode 100644 src/common/lazy-loading/lazy-loading.module.ts delete mode 100644 src/common/lazy-loading/lazy-module-loader.service.ts delete mode 100644 src/common/lazy-loading/startup-logger.service.ts delete mode 100644 src/common/middleware/csrf.middleware.ts delete mode 100644 src/common/modules/api-versioning.module.ts delete mode 100644 src/common/services/idempotency.service.spec.ts delete mode 100644 src/common/timeout/timeout-config.service.ts delete mode 100644 src/common/timeout/timeout.controller.ts delete mode 100644 src/common/timeout/timeout.module.ts delete mode 100644 src/common/utils/correlation.utils.spec.ts delete mode 100644 src/common/utils/pagination.util.spec.ts delete mode 100644 src/common/utils/pagination.util.ts delete mode 100644 src/common/utils/sanitization.utils.spec.ts delete mode 100644 src/common/utils/websocket.utils.ts delete mode 100644 src/common/validators/is-strong-password.validator.ts delete mode 100644 src/common/validators/password.validator.spec.ts delete mode 100644 src/common/validators/password.validator.ts delete mode 100644 src/config/feature-flags.config.spec.ts delete mode 100644 src/config/swagger.config.ts delete mode 100644 src/courses/courses.controller.ts delete mode 100644 src/courses/courses.module.ts delete mode 100644 src/courses/courses.service.spec.ts delete mode 100644 src/courses/courses.service.ts delete mode 100644 src/courses/enrollments/enrollments.service.ts delete mode 100644 src/courses/guards/ws-jwt-auth.guard.ts delete mode 100644 src/courses/search-sync/course-search-sync.service.ts delete mode 100644 src/data-warehouse/data-warehouse.controller.ts delete mode 100644 src/data-warehouse/data-warehouse.module.ts delete mode 100644 src/data-warehouse/etl/etl-pipeline.service.ts delete mode 100644 src/data-warehouse/lineage/data-lineage.service.ts delete mode 100644 src/data-warehouse/loading/incremental-loader.service.ts delete mode 100644 src/data-warehouse/modeling/dimensional-modeling.service.ts delete mode 100644 src/data-warehouse/quality/data-quality.service.ts delete mode 100644 src/email-marketing/ab-testing/ab-testing.controller.ts delete mode 100644 src/email-marketing/ab-testing/ab-testing.service.ts delete mode 100644 src/email-marketing/analytics/email-analytics.controller.ts delete mode 100644 src/email-marketing/analytics/email-analytics.service.ts delete mode 100644 src/email-marketing/email-marketing.controller.ts delete mode 100644 src/email-marketing/email-marketing.module.ts delete mode 100644 src/email-marketing/email-marketing.service.ts delete mode 100644 src/email-marketing/processors/email-queue.processor.ts delete mode 100644 src/email-marketing/segmentation/segment.controller.ts delete mode 100644 src/email-marketing/segmentation/segmentation.service.ts delete mode 100644 src/email-marketing/sender/email-sender.service.ts delete mode 100644 src/email-marketing/tracking/tracking.controller.ts delete mode 100644 src/feature-flags/analytics/flag-analytics.service.ts delete mode 100644 src/feature-flags/evaluation/flag-evaluation.service.ts delete mode 100644 src/feature-flags/experimentation/experimentation.service.ts delete mode 100644 src/feature-flags/feature-flags.controller.ts delete mode 100644 src/feature-flags/feature-flags.module.ts delete mode 100644 src/feature-flags/rollout/rollout.service.ts delete mode 100644 src/feature-flags/targeting/targeting.service.ts delete mode 100644 src/gamification/challenges/challenges.service.ts delete mode 100644 src/gamification/gamification.controller.ts delete mode 100644 src/gamification/gamification.module.ts delete mode 100644 src/gamification/gamification.service.ts delete mode 100644 src/gateways/messaging/messaging.gateway.ts delete mode 100644 src/gateways/notifications/notifications.gateway.ts delete mode 100644 src/graphql/graphql.module.ts delete mode 100644 src/graphql/middleware/dataloader.middleware.ts delete mode 100644 src/graphql/resolvers/course.resolver.ts delete mode 100644 src/graphql/resolvers/index.ts delete mode 100644 src/graphql/resolvers/mutation.resolver.ts delete mode 100644 src/graphql/resolvers/query.resolver.ts delete mode 100644 src/graphql/resolvers/user.resolver.ts delete mode 100644 src/graphql/services/complexity-analysis.service.ts delete mode 100644 src/graphql/services/dataloader.service.ts delete mode 100644 src/graphql/services/query-complexity.service.ts delete mode 100644 src/health/health.controller.ts delete mode 100644 src/health/health.module.ts delete mode 100644 src/health/health.service.ts delete mode 100644 src/learning-paths/learning-paths.controller.ts delete mode 100644 src/learning-paths/learning-paths.module.ts delete mode 100644 src/learning-paths/learning-paths.service.ts delete mode 100644 src/learning-paths/services/path-generation.service.ts delete mode 100644 src/localization/language-detection.service.spec.ts delete mode 100644 src/localization/language-detection.service.ts delete mode 100644 src/localization/language.middleware.ts delete mode 100644 src/localization/localization.controller.ts delete mode 100644 src/localization/localization.module.ts delete mode 100644 src/localization/localization.service.spec.ts delete mode 100644 src/localization/localization.service.ts delete mode 100644 src/media/file-cleanup.task.ts delete mode 100644 src/media/media.controller.spec.ts delete mode 100644 src/media/media.controller.ts delete mode 100644 src/media/media.module.ts delete mode 100644 src/media/media.service.spec.ts delete mode 100644 src/media/media.service.ts delete mode 100644 src/media/processing/document-processing.service.ts delete mode 100644 src/media/processing/image-processing.service.ts delete mode 100644 src/media/processing/video.processor.ts delete mode 100644 src/media/storage/file-storage.service.ts delete mode 100644 src/media/validation/file-upload-validation.service.ts delete mode 100644 src/media/validation/file-validation.service.ts delete mode 100644 src/media/validation/malware-scanning.service.ts delete mode 100644 src/media/validation/upload-validation.util.spec.ts delete mode 100644 src/messaging/circuit-breaker/circuit-breaker.service.ts delete mode 100644 src/messaging/discovery/service-discovery.service.ts delete mode 100644 src/messaging/event-bus/event-bus.service.ts delete mode 100644 src/messaging/messaging.module.ts delete mode 100644 src/migrations/conflicts/conflict-resolution.service.ts delete mode 100644 src/migrations/environments/environment-sync.service.ts delete mode 100644 src/migrations/migration-runner.service.ts delete mode 100644 src/migrations/migration.controller.ts delete mode 100644 src/migrations/migration.module.ts delete mode 100644 src/migrations/migration.registry.ts delete mode 100644 src/migrations/migration.service.spec.ts delete mode 100644 src/migrations/migration.service.ts delete mode 100644 src/migrations/rollback/rollback.service.spec.ts delete mode 100644 src/migrations/rollback/rollback.service.ts delete mode 100644 src/migrations/samples/001-create-users-table.migration.ts delete mode 100644 src/migrations/samples/002-create-courses-table.migration.ts delete mode 100644 src/migrations/samples/003-create-course-modules-table.migration.ts delete mode 100644 src/migrations/samples/004-create-lessons-table.migration.ts delete mode 100644 src/migrations/samples/005-create-enrollments-table.migration.ts delete mode 100644 src/migrations/samples/006-create-migrations-tracking-table.migration.ts delete mode 100644 src/migrations/samples/007-add-payment-idempotency-keys.migration.ts delete mode 100644 src/migrations/samples/sample-user-table.migration.ts delete mode 100644 src/migrations/samples/setup-sharding-infrastructure.migration.ts delete mode 100644 src/migrations/validation/schema-validation.service.spec.ts delete mode 100644 src/migrations/validation/schema-validation.service.ts delete mode 100644 src/moderation/moderation.module.ts delete mode 100644 src/moderation/moderation.service.ts delete mode 100644 src/monitoring/logging/typeorm-logger.ts delete mode 100644 src/monitoring/monitoring.controller.ts delete mode 100644 src/monitoring/monitoring.module.ts delete mode 100644 src/monitoring/monitoring.service.ts delete mode 100644 src/monitoring/optimization/optimization.service.ts delete mode 100644 src/monitoring/performance/performance-analysis.service.ts delete mode 100644 src/monitoring/scheduled-task-monitoring.service.ts delete mode 100644 src/notifications/email/email.processor.ts delete mode 100644 src/notifications/email/email.service.spec.ts delete mode 100644 src/notifications/email/email.service.ts delete mode 100644 src/notifications/notification-templates.service.ts delete mode 100644 src/notifications/notifications.controller.ts delete mode 100644 src/notifications/notifications.module.ts delete mode 100644 src/notifications/notifications.service.ts delete mode 100644 src/observability/anomaly/anomaly-detection.service.ts delete mode 100644 src/observability/logging/log-aggregation.service.ts delete mode 100644 src/observability/metrics/metrics-analysis.service.ts delete mode 100644 src/observability/observability.controller.ts delete mode 100644 src/observability/observability.module.ts delete mode 100644 src/observability/observability.service.ts delete mode 100644 src/observability/tracing/distributed-tracing.service.ts delete mode 100644 src/onboarding/onboarding.controller.ts delete mode 100644 src/onboarding/onboarding.module.ts delete mode 100644 src/onboarding/onboarding.service.spec.ts delete mode 100644 src/onboarding/onboarding.service.ts delete mode 100644 src/orchestration/discovery/service-discovery.service.spec.ts delete mode 100644 src/orchestration/health/health-checker.service.ts delete mode 100644 src/orchestration/orchestration.module.ts delete mode 100644 src/orchestration/service-mesh/service-mesh.service.spec.ts delete mode 100644 src/orchestration/service-mesh/service-mesh.service.ts delete mode 100644 src/orchestration/workflow/workflow-engine.service.ts delete mode 100644 src/payments/interfaces/payment-provider.interface.ts delete mode 100644 src/payments/payments.controller.spec.ts delete mode 100644 src/payments/payments.controller.ts delete mode 100644 src/payments/payments.module.ts delete mode 100644 src/payments/payments.service.spec.ts delete mode 100644 src/payments/payments.service.ts delete mode 100644 src/payments/providers/provider-factory.service.ts delete mode 100644 src/payments/providers/stripe.service.ts delete mode 100644 src/payments/webhooks/webhook-management.controller.ts delete mode 100644 src/payments/webhooks/webhook-queue.service.spec.ts delete mode 100644 src/payments/webhooks/webhook-queue.service.ts delete mode 100644 src/payments/webhooks/webhook-retry.e2e-spec.ts delete mode 100644 src/payments/webhooks/webhook-retry.processor.spec.ts delete mode 100644 src/payments/webhooks/webhook-retry.processor.ts delete mode 100644 src/payments/webhooks/webhook-security.service.spec.ts delete mode 100644 src/payments/webhooks/webhook.controller.ts delete mode 100644 src/payments/webhooks/webhook.service.ts delete mode 100644 src/queues/monitoring/queue-monitoring.service.ts delete mode 100644 src/queues/processors/default-queue.processor.ts delete mode 100644 src/queues/queue.controller.ts delete mode 100644 src/queues/queue.module.ts delete mode 100644 src/queues/queue.service.ts delete mode 100644 src/queues/queue.spec.ts delete mode 100644 src/queues/retry/retry-logic.service.ts delete mode 100644 src/queues/scheduler/job-scheduler.service.ts delete mode 100644 src/rate-limiting/rate-limiting.controller.spec.ts delete mode 100644 src/rate-limiting/rate-limiting.controller.ts delete mode 100644 src/rate-limiting/rate-limiting.service.ts delete mode 100644 src/rate-limiting/services/distrubutes.service.ts delete mode 100644 src/rate-limiting/services/limit-guard/guard.ts delete mode 100644 src/rate-limiting/services/rate-limiting.module.ts delete mode 100644 src/rate-limiting/services/throttling.service.ts delete mode 100644 src/search/autocomplete/autocomplete.service.ts delete mode 100644 src/search/elasticsearch/elasticsearch.service.spec.ts delete mode 100644 src/search/filters/search-filters.service.ts delete mode 100644 src/search/indexing/indexing.service.ts delete mode 100644 src/search/indexing/search-index-optimizer.service.ts delete mode 100644 src/search/search.controller.spec.ts delete mode 100644 src/search/search.service.spec.ts delete mode 100644 src/security/secrets/secrets-manager.service.spec.ts delete mode 100644 src/sync/cache/cache-invalidation.service.spec.ts delete mode 100644 src/sync/conflicts/conflict-resolution.service.spec.ts delete mode 100644 src/sync/consistency/data-consistency.service.spec.ts delete mode 100644 src/sync/replication/replication.service.spec.ts delete mode 100644 src/users/dto/bulk-user.dto.ts delete mode 100644 src/users/dto/create-user.dto.ts delete mode 100644 src/users/dto/update-user.dto.ts delete mode 100644 src/users/users.controller.ts delete mode 100644 src/users/users.module.ts delete mode 100644 src/users/users.service.spec.ts delete mode 100644 src/users/users.service.ts delete mode 100644 src/utils/masking/masking.spec.ts delete mode 100644 src/workers/base/base.worker.spec.ts delete mode 100644 src/workers/health/worker-health-check.service.spec.ts delete mode 100644 src/workers/orchestration/worker-orchestration.service.spec.ts delete mode 100644 src/workers/processors/backup-processing.worker.spec.ts delete mode 100644 src/workers/processors/data-sync.worker.spec.ts delete mode 100644 src/workers/processors/email.worker.spec.ts delete mode 100644 src/workers/processors/media-processing.worker.spec.ts delete mode 100644 src/workers/processors/subscriptions.worker.spec.ts delete mode 100644 src/workers/processors/webhooks.worker.spec.ts create mode 100644 test/analytics/analytics.service.spec.js create mode 100644 test/analytics/analytics.service.spec.js.map create mode 100644 test/app.e2e-spec.js create mode 100644 test/app.e2e-spec.js.map delete mode 100644 test/auth.e2e-spec.ts create mode 100644 test/mocks/uuid.js create mode 100644 test/mocks/uuid.js.map create mode 100644 test/monitoring/cost-tracking.service.spec.js create mode 100644 test/monitoring/cost-tracking.service.spec.js.map create mode 100644 test/setup.js create mode 100644 test/setup.js.map create mode 100644 test/utils/flaky-test-detector.js create mode 100644 test/utils/flaky-test-detector.js.map create mode 100644 test/utils/http-outcome-assertions.js create mode 100644 test/utils/http-outcome-assertions.js.map create mode 100644 test/utils/index.js create mode 100644 test/utils/index.js.map create mode 100644 test/utils/mock-factories.js create mode 100644 test/utils/mock-factories.js.map create mode 100644 test/utils/module-test-cases.js create mode 100644 test/utils/module-test-cases.js.map create mode 100644 test/utils/test-database.service.js create mode 100644 test/utils/test-database.service.js.map create mode 100644 test/utils/test-http-client.js create mode 100644 test/utils/test-http-client.js.map create mode 100644 test/utils/test-retry-helper.js create mode 100644 test/utils/test-retry-helper.js.map delete mode 100644 tmp_tsc_errors.txt diff --git a/nest-cli.json b/nest-cli.json index f9aa683b..4106ffad 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -3,6 +3,7 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { - "deleteOutDir": true + "deleteOutDir": false, + "builder": "tsc" } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 14c3e6a2..f2268f9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@aws-sdk/client-kms": "^3.978.0", "@aws-sdk/client-s3": "^3.975.0", "@aws-sdk/client-secrets-manager": "^3.978.0", - "@elastic/elasticsearch": "^8.19.1", "@aws-sdk/client-sns": "^3.1038.0", "@aws-sdk/client-sqs": "^3.1038.0", "@elastic/elasticsearch": "^9.3.4", @@ -24,15 +23,15 @@ "@nestjs/axios": "^4.0.1", "@nestjs/bull": "^11.0.2", "@nestjs/cache-manager": "^3.0.1", - "@nestjs/common": "^10.4.22", + "@nestjs/common": "10.4.22", "@nestjs/config": "^4.0.3", - "@nestjs/core": "^10.4.22", + "@nestjs/core": "10.4.22", "@nestjs/elasticsearch": "^11.1.0", "@nestjs/event-emitter": "^3.1.0", "@nestjs/graphql": "^12.2.2", "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", - "@nestjs/platform-express": "^10.4.18", + "@nestjs/platform-express": "10.4.22", "@nestjs/platform-socket.io": "^10.4.22", "@nestjs/schedule": "^6.1.0", "@nestjs/swagger": "^7.4.2", @@ -83,7 +82,6 @@ "pdf-parse": "^1.1.1", "pg": "^8.17.2", "prom-client": "^15.1.3", - "reacts-cli": "^1.0.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "sharp": "^0.34.3", @@ -100,7 +98,7 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", - "@openapitools/openapi-generator-cli": "^2.32.0", + "@openapitools/openapi-generator-cli": "^2.31.0", "@types/babel__core": "^7.20.5", "@types/babel__generator": "^7.27.0", "@types/babel__template": "^7.4.4", @@ -114,8 +112,6 @@ "@types/istanbul-lib-report": "^3.0.3", "@types/jest": "^29.5.2", "@types/jsonwebtoken": "^9.0.10", - "@types/mime": "^3.0.4", - "@types/node": "^20.19.39", "@types/mime": "^4.0.0", "@types/node": "^20.19.4", "@types/passport-jwt": "^4.0.1", @@ -142,19 +138,13 @@ "ts-loader": "^9.5.7", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", - "typescript": "^6.0.3" + "typescript": "^5.4.5" }, "engines": { "node": ">=18.0.0", "npm": ">=9.0.0" } }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", - "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", - "license": "MIT" - }, "node_modules/@angular-devkit/core": { "version": "17.3.11", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", @@ -252,32 +242,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@angular-devkit/schematics-cli/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/@angular-devkit/schematics-cli/node_modules/inquirer": { "version": "9.2.15", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", @@ -305,41 +269,6 @@ "node": ">=18" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/@angular-devkit/schematics-cli/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/@angular-devkit/schematics/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -360,9 +289,9 @@ } }, "node_modules/@apollo/protobufjs": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.7.tgz", - "integrity": "sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.8.tgz", + "integrity": "sha512-r7xNeUqZX+eBBEmyvaPw0/cSz6zgf5jdH8mjUz8ynKpNs/GU7vi2T7sNcZINk2ZID7wwjG91FCgdpCrQuJ8rzA==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -717,6 +646,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -727,12 +657,12 @@ } }, "node_modules/@apollo/usage-reporting-protobuf": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz", - "integrity": "sha512-u40dIUePHaSKVshcedO7Wp+mPiZsaU6xjv9J+VyxpoU/zL6Jle+9zWeG98tr/+SZ0nZ4OXhrbb8SNr0rAPpIDA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.2.tgz", + "integrity": "sha512-aTnAD41RYz0d5dawlyR5Iclkgzx0Xb0njUJmEfvZ6pS4f4HU8wCYyctPpWat/HWp2PmRwDfX5R1k4uVcDKZ4xA==", "license": "MIT", "dependencies": { - "@apollo/protobufjs": "1.2.7" + "@apollo/protobufjs": "1.2.8" } }, "node_modules/@apollo/utils.createhash": { @@ -1092,24 +1022,24 @@ } }, "node_modules/@aws-sdk/client-cloudfront": { - "version": "3.1036.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudfront/-/client-cloudfront-3.1036.0.tgz", - "integrity": "sha512-uK22RpfNJMIncG9H3vmq3gaoztyXPWiNQoDizuFlLuBojgStp58QzGsDZAQYjiqio3YNg1TF/gIsMJTwF/Caug==", + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudfront/-/client-cloudfront-3.1045.0.tgz", + "integrity": "sha512-84RIiLrMXcinBK1JXnP1bOavvQ+jxTxN4xsB20e39MfOZMyr7wIxMNn35kTYTg8UTJgN7zwEgzGJo64sU3mtPw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1117,7 +1047,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1133,10 +1063,10 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.6", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.16", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -1144,24 +1074,24 @@ } }, "node_modules/@aws-sdk/client-kms": { - "version": "3.1036.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.1036.0.tgz", - "integrity": "sha512-tpqED4Wxmwx3gKv4czaMBbptyoYYX/2KuJ/F3+ZUQ5xPimQ448Wrx6Ci0aOfnbBIKek8DIL24LdgYSoApgnHoQ==", + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.1045.0.tgz", + "integrity": "sha512-IWnBhZ/tOi/gCc7xaTCBQQYZWb8z1z8uQnARtT3j8y1jBJ6/1o2/YVXiW2Lkakh0vcXtI8b40n0KSqhXXEx4Tw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1169,7 +1099,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1185,7 +1115,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1194,32 +1124,32 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1036.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1036.0.tgz", - "integrity": "sha512-QGjLHw1xklwWX+MWt/7X66lMxjNQLOb0tjcwAU3PaBrYZ51kphDlfvc2sInNEsIU03+I158Y4WSMhl8l71SAsw==", + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1045.0.tgz", + "integrity": "sha512-fsuO3Y6t+3Ro9Bsg41DKj4Sfy53CGSrhnMldNplWmG8Tx0UbYk+YDa4RD1hVlJpERw4JBmPkl0+J9qlxMh1pcA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", "@aws-sdk/middleware-bucket-endpoint": "^3.972.10", "@aws-sdk/middleware-expect-continue": "^3.972.10", - "@aws-sdk/middleware-flexible-checksums": "^3.974.13", + "@aws-sdk/middleware-flexible-checksums": "^3.974.16", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-location-constraint": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-sdk-s3": "^3.972.34", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", "@aws-sdk/middleware-ssec": "^3.972.10", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", @@ -1233,7 +1163,7 @@ "@smithy/md5-js": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1249,10 +1179,10 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.6", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.16", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -1260,23 +1190,24 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.1038.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1038.0.tgz", - "integrity": "sha512-cTNiqnVErYo8fCb7dw/BnHiubfWJIE1Ur97DT5faTncI8OEibs1A7E1GyD9Y5L77xn8edB5XJ4WBwBlTdyzk+Q==", + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1045.0.tgz", + "integrity": "sha512-ceXmaTE/3j7bHgVzUrpL/ECjQQ+aE/x8wNbblC/SIb020OxYRMj0DscFimnI5kEjutGHQ+A68bbX2A+bZuAMEA==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.6", - "@aws-sdk/credential-provider-node": "^3.972.37", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.36", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.22", + "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1284,7 +1215,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.6", + "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1300,7 +1231,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.5", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1309,24 +1240,24 @@ } }, "node_modules/@aws-sdk/client-sns": { - "version": "3.1038.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.1038.0.tgz", - "integrity": "sha512-LitoeLXm6yZ2PeYlLkbxp9B6NAGAxu/LvLoXENpm2vK9lLlPrie4nKzkoFhMqfgE57YMhP6y8Q3jTBl8Mli2nQ==", + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.1045.0.tgz", + "integrity": "sha512-w/iwPYVAXx62dD2E5nBQaUOntlYWCjaIPXGZpdC6+WWF/idTPjN3qu5Q0KgcrP6zQDGCgtAVaLlLVaK36/5x7A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.6", - "@aws-sdk/credential-provider-node": "^3.972.37", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.36", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.22", + "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1334,7 +1265,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.6", + "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1350,7 +1281,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.5", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1359,25 +1290,25 @@ } }, "node_modules/@aws-sdk/client-sqs": { - "version": "3.1038.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1038.0.tgz", - "integrity": "sha512-tGJ131fvczx8+79Kbs6mfeRBqnSzuDOIqS/3es9iRTE6ohHqQ2phUjBCLGlfwBkhsp0chOa+i5nvJssTZ3dCUQ==", + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1045.0.tgz", + "integrity": "sha512-reWeEE53mgCv8uUFSicvVf0A6BoWGImdC4y5x5clkeEmfIikahIBtons6u0d32kwI4NczpcretpUkOCE/nvWAA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.6", - "@aws-sdk/credential-provider-node": "^3.972.37", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-sdk-sqs": "^3.972.22", - "@aws-sdk/middleware-user-agent": "^3.972.36", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.22", + "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1386,7 +1317,7 @@ "@smithy/md5-js": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.6", + "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1402,7 +1333,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.5", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1411,13 +1342,13 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.6.tgz", - "integrity": "sha512-8Vu7zGxu+39ChR/s5J7nXBw3a2kMHAi0OfKT8ohgTVjX0qYed/8mIfdBb638oBmKrWCwwKjYAM5J/4gMJ8nAJA==", + "version": "3.974.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.20", + "@aws-sdk/xml-builder": "^3.972.22", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", @@ -1427,7 +1358,7 @@ "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.5", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1449,12 +1380,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.32", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.32.tgz", - "integrity": "sha512-7vA4GHg8NSmQxquJHSBcSM3RgB4ZaaRi6u4+zGFKOmOH6aqlgr2Sda46clkZDYzlirgfY96w15Zj0jh6PT48ng==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", @@ -1465,12 +1396,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.34.tgz", - "integrity": "sha512-vBrhWujFCLp1u8ptJRWYlipMutzPptb8pDQ00rKVH9q67T7rGd3VTWIj63aKrlLuY6qSsw1Rt5F/D/7wnNgryA==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.6.1", @@ -1486,19 +1417,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.36.tgz", - "integrity": "sha512-FBHyCmV8EB0gUvh1d+CZm87zt2PrdC7OyWexLRoH3I5zWSOUGa+9t58Y5jbxRfwUp3AWpHAFvKY6YzgR845sVA==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", - "@aws-sdk/credential-provider-env": "^3.972.32", - "@aws-sdk/credential-provider-http": "^3.972.34", - "@aws-sdk/credential-provider-login": "^3.972.36", - "@aws-sdk/credential-provider-process": "^3.972.32", - "@aws-sdk/credential-provider-sso": "^3.972.36", - "@aws-sdk/credential-provider-web-identity": "^3.972.36", - "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -1511,13 +1442,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.36.tgz", - "integrity": "sha512-IFap01lJKxQc0C/OHmZwZQr/cKq0DhrcmKedRrdnnl42D+P0SImnnnWQjv07uIPqpEdtqmkPXb9TiPYTU+prxQ==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", - "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", @@ -1530,17 +1461,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.37.tgz", - "integrity": "sha512-/WFixFAAiw8WpmjZcI0l4t3DerXLmVinOIfuotmRZnu2qmsFPoqqmstASz0z8bi1pGdFXzeLzf6bwucM3mZcUQ==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.32", - "@aws-sdk/credential-provider-http": "^3.972.34", - "@aws-sdk/credential-provider-ini": "^3.972.36", - "@aws-sdk/credential-provider-process": "^3.972.32", - "@aws-sdk/credential-provider-sso": "^3.972.36", - "@aws-sdk/credential-provider-web-identity": "^3.972.36", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-ini": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -1553,12 +1484,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.32", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.32.tgz", - "integrity": "sha512-uZp4tlGbpczV8QxmtIwOpSkcyGtBRR8/T4BAumRKfAt1nwCig3FSCZvrKl6ARDIDVRYn5p2oRcAsfFR01EgMGA==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -1570,14 +1501,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.36.tgz", - "integrity": "sha512-DsLr0UHMyKzRJKe2bjlwU8q1cfoXg8TIJKV/xwvnalAemiZLOZunFzj/whGnFDZIBVLdnbLiwv5SvRf1+CSwkg==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", - "@aws-sdk/nested-clients": "^3.997.4", - "@aws-sdk/token-providers": "3.1038.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/token-providers": "3.1041.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -1589,13 +1520,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.36.tgz", - "integrity": "sha512-uzrURO7frJhHQVVNR5zBJcCYeMYflmXcWBK1+MiBym2Dfjh6nXATrMixrmGZi+97Q7ETZ+y/4lUwAy0Nfnznjw==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", - "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -1640,15 +1571,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.13.tgz", - "integrity": "sha512-b6QUe2hQX9XsnCzp6mtzVaERhganDKeb8lmGL6pVhr7rRVH9S9keDFW7uKytuuqmcY5943FixoGqn/QL+sbUBA==", + "version": "3.974.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.16.tgz", + "integrity": "sha512-6ru8doI0/XzszqLIPXf0E/V7HhAw1Pu94010XCKYtBUfD0LxF0BuOzrUf8OQGR6j2o6wgKTHUniOmndQycHwCA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/crc64-nvme": "^3.972.7", "@aws-sdk/types": "^3.973.8", "@smithy/is-array-buffer": "^4.2.2", @@ -1724,12 +1655,12 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.35.tgz", - "integrity": "sha512-lLppaNTAz+wNgLdi4FtHzrlwrGF0ODTnBWHBaFg85SKs0eJ+M+tP5ifrA8f/0lNd+Ak3MC1NGC6RavV3ny4HTg==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.17", @@ -1780,18 +1711,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.36.tgz", - "integrity": "sha512-O2beToxguBvrZFFZ+fFgPbbae8MvyIBjQ6lImee4APHEXXNAD5ZJ2ayLF1mb7rsKw86TM81y5czg82bZncjSjg==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", - "@smithy/util-retry": "^4.3.5", + "@smithy/util-retry": "^4.3.6", "tslib": "^2.6.2" }, "engines": { @@ -1799,24 +1730,24 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.4.tgz", - "integrity": "sha512-4Sf+WY1lMJzXlw5MiyCMe/UzdILCwvuaHThbqMXS6dfh9gZy3No360I42RXquOI/ULUOhWy2HCyU0Fp20fQGPQ==", + "version": "3.997.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.6", + "@aws-sdk/core": "^3.974.8", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.36", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.23", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.22", + "@aws-sdk/util-user-agent-node": "^3.973.24", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1824,7 +1755,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.6", + "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1840,7 +1771,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.5", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1865,12 +1796,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.23", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.23.tgz", - "integrity": "sha512-wBbys3Y53Ikly556vyADurKpYQHXS7Jjaskbz+Ga9PZCz7PB/9f3VdKbDlz7dqIzn+xwz7L/a6TR4iXcOi8IRw==", + "version": "3.996.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.35", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", @@ -1882,13 +1813,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1038.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1038.0.tgz", - "integrity": "sha512-Qniru+9oGGb/HNK/gGZWbV3jsD0k71ngE7qMQ/x6gYNYLd2EOwHCS6E2E6jfkaqO4i0d+nNKmfRy8bNcshKdGQ==", + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.6", - "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -1965,12 +1896,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.22.tgz", - "integrity": "sha512-YTYqTmOUrwbm1h99Ee4y/mVYpFRl0oSO/amtP5cc1BZZWdaAVWs9zj3TkyRHWvR9aI/ZS8m3mS6awXtYUlWyaw==", + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.36", + "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", @@ -1990,12 +1921,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.21.tgz", - "integrity": "sha512-qxNiHUtlrsjTeSlrPWiFkWps7uD6YB4eKzg7eLAFH8jbiHTlt0ePNlo2Xu+WlftP38JIcMaIX4jTUjOlE2ySWw==", - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.20.tgz", - "integrity": "sha512-MDcUfroaMAnDAHn29vN781t0wudR8zjfgg+r3s5otx8TJXFWg01NZB7HvHkBbOf7UUmKEwIZf5kHxiaVUgwjlQ==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", "license": "Apache-2.0", "dependencies": { "@nodable/entities": "2.1.0", @@ -2007,6 +1935,27 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/@aws/lambda-invoke-store": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", @@ -2020,6 +1969,7 @@ "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", @@ -2031,9 +1981,10 @@ } }, "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==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2043,6 +1994,7 @@ "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", @@ -2073,6 +2025,7 @@ "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" @@ -2082,6 +2035,7 @@ "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", @@ -2094,23 +2048,11 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/types": "^7.27.3" - }, - "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", @@ -2127,6 +2069,7 @@ "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" @@ -2136,70 +2079,27 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "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", @@ -2213,6 +2113,7 @@ "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", @@ -2226,64 +2127,21 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.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-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "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" @@ -2293,6 +2151,7 @@ "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" @@ -2302,6 +2161,7 @@ "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" @@ -2311,6 +2171,7 @@ "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", @@ -2321,9 +2182,10 @@ } }, "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==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -2339,6 +2201,7 @@ "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" @@ -2351,6 +2214,7 @@ "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" @@ -2363,6 +2227,7 @@ "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" @@ -2375,6 +2240,7 @@ "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" @@ -2390,6 +2256,7 @@ "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" @@ -2405,6 +2272,7 @@ "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" @@ -2417,6 +2285,7 @@ "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" @@ -2429,6 +2298,7 @@ "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" @@ -2444,6 +2314,7 @@ "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" @@ -2456,6 +2327,7 @@ "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" @@ -2468,6 +2340,7 @@ "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" @@ -2480,6 +2353,7 @@ "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" @@ -2492,6 +2366,7 @@ "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" @@ -2504,6 +2379,7 @@ "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" @@ -2516,6 +2392,7 @@ "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" @@ -2531,6 +2408,7 @@ "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" @@ -2546,6 +2424,7 @@ "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" @@ -2557,193 +2436,49 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-commonjs": { + "node_modules/@babel/template": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "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", - "peer": true, "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "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", - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@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" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", - "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "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", - "peer": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-syntax-jsx": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", - "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.28.0", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.28.5" - }, - "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==", - "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==", - "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==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2753,18 +2488,9 @@ "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/@bitcoinerlab/secp256k1": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.2.0.tgz", - "integrity": "sha512-jeujZSzb3JOZfmJYI0ph1PVpCRV5oaexCgy+RvCXV8XlY+XFB/2n3WOcvBsKLsOw78KYgnQrQWb2HrKE4be88Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@noble/curves": "^1.7.0" - } - }, "node_modules/@borewit/text-codec": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", @@ -2819,9 +2545,9 @@ } }, "node_modules/@commitlint/config-conventional": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", - "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", + "version": "20.5.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.3.tgz", + "integrity": "sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3121,71 +2847,25 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/modifiers": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", - "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@elastic/elasticsearch": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-9.3.4.tgz", - "integrity": "sha512-Mp14fPEYx+WTfZdcvAaZ9WkLYGHQCbwMx6EP5VCucYdhv4cn/g2sbnMT5HzK+gX3XEpBnnkEK/+WysCKzxuo3A==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-9.4.0.tgz", + "integrity": "sha512-UbvHAtGAqiQdAsAmb3vfCMwOTIf6BoSf6MDpz7mw5UzpU3pRSl4KeFzaXRfrk4+PrgiudYx14socGCI6frgguQ==", "license": "Apache-2.0", "dependencies": { "@elastic/transport": "^9.3.5", - "apache-arrow": "18.x - 21.x", "tslib": "^2.4.0" }, "engines": { - "node": ">=18" + "node": ">=20" + }, + "peerDependencies": { + "apache-arrow": "18.x - 21.x" + }, + "peerDependenciesMeta": { + "apache-arrow": { + "optional": true + } } }, "node_modules/@elastic/transport": { @@ -3345,44 +3025,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, "node_modules/@graphql-tools/merge": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.2.tgz", @@ -3447,14 +3089,14 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", "license": "Apache-2.0", "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", - "protobufjs": "^7.5.3", + "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { @@ -3532,9 +3174,9 @@ } }, "node_modules/@huggingface/jinja": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.7.tgz", - "integrity": "sha512-OosMEbF/R6zkKNNzqhI7kvKYCpo1F0UeIv46/h4D4UjVEKKd6k3TiV8sgu6fkreX4lbBiRI+lZG8UnXnqVQmEQ==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz", + "integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==", "license": "MIT", "engines": { "node": ">=18" @@ -3707,6 +3349,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3723,6 +3368,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3739,6 +3387,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3755,6 +3406,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3771,6 +3425,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3787,6 +3444,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3803,6 +3463,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3819,6 +3482,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3835,6 +3501,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3857,6 +3526,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3879,6 +3551,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3901,6 +3576,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3923,6 +3601,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3945,6 +3626,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3967,6 +3651,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3989,6 +3676,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4085,6 +3775,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "license": "MIT", "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -4110,6 +3801,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4125,6 +3817,7 @@ "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" @@ -4136,42 +3829,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@inquirer/core/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, - "engines": { - "node": ">= 12" - } - }, - "node_modules/@inquirer/core/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@inquirer/core/node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "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==", + "node_modules/@inquirer/core/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": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=8" @@ -4182,6 +3847,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-1.3.3.tgz", "integrity": "sha512-RzlRISXWqIKEf83FDC9ZtJ3JvuK1l7aGpretf41BCWYrvla2wU8W8MTRNMiPrPJ+1SIqrRC1nZdZ60hD9hRXLg==", "dev": true, + "license": "MIT", "dependencies": { "@inquirer/core": "^6.0.0", "@inquirer/type": "^1.1.6", @@ -4198,6 +3864,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4213,6 +3880,7 @@ "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" @@ -4224,11 +3892,25 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@inquirer/select/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/@inquirer/type": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", "dev": true, + "license": "MIT", "dependencies": { "mute-stream": "^1.0.0" }, @@ -4236,15 +3918,6 @@ "node": ">=18" } }, - "node_modules/@inquirer/type/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/@ioredis/commands": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", @@ -4351,6 +4024,7 @@ "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", @@ -4367,6 +4041,7 @@ "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" @@ -4376,6 +4051,7 @@ "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" @@ -4385,6 +4061,7 @@ "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", @@ -4398,6 +4075,7 @@ "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", @@ -4411,6 +4089,7 @@ "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" @@ -4423,6 +4102,7 @@ "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" @@ -4438,6 +4118,7 @@ "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" @@ -4450,6 +4131,7 @@ "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" @@ -4459,60 +4141,17 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/@jadonamite/chessify-sdk": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jadonamite/chessify-sdk/-/chessify-sdk-1.0.4.tgz", - "integrity": "sha512-EsKy/Ubk5RYU3t/LnXoyAgVqcVhH5BtFdWztxnwPeA+u8rdylgMjFIa+txJYHU+BfYcx6jzw463qDtCG/616Ng==", - "license": "MIT", - "dependencies": { - "@jadonamite/stacks-core": "^1.0.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@stacks/network": "^6.17.0", - "@stacks/transactions": "^6.17.0" - } - }, - "node_modules/@jadonamite/fundxagon-sdk": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jadonamite/fundxagon-sdk/-/fundxagon-sdk-1.0.4.tgz", - "integrity": "sha512-rZxe99KoS4sCF/kcXgS1o8ZlCavLey+BcO4rup8qOm6nToymGz64NRhSopRPjm2r8hcoZgHmi6m0UZ3TPB4z0g==", - "license": "MIT", - "dependencies": { - "@jadonamite/stacks-core": "^1.0.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@stacks/network": "^6.17.0", - "@stacks/transactions": "^6.17.0" - } - }, - "node_modules/@jadonamite/stacks-core": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jadonamite/stacks-core/-/stacks-core-1.0.4.tgz", - "integrity": "sha512-X6MYtcyxpbfn31bYTarArlWtFn/Y/WEzRY6fiGodAinMS6a/1VyNJ3S5vA3I0c+GyRLGgBlEcZErAulzaFfVKw==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@stacks/network": "^6.17.0", - "@stacks/transactions": "^6.17.0" - } - }, "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", @@ -4530,6 +4169,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4545,6 +4185,7 @@ "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", @@ -4557,10 +4198,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@jest/console/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/@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", @@ -4608,6 +4263,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4623,6 +4279,7 @@ "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", @@ -4635,10 +4292,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@jest/core/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/@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", @@ -4654,6 +4325,7 @@ "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", @@ -4667,6 +4339,7 @@ "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" @@ -4679,6 +4352,7 @@ "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", @@ -4696,6 +4370,7 @@ "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", @@ -4711,6 +4386,7 @@ "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", @@ -4754,6 +4430,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4769,12 +4446,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/@jest/reporters/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", @@ -4785,6 +4464,7 @@ "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", @@ -4802,6 +4482,7 @@ "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", @@ -4822,6 +4503,7 @@ "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" @@ -4830,10 +4512,24 @@ "node": "*" } }, + "node_modules/@jest/reporters/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/@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" @@ -4846,6 +4542,7 @@ "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", @@ -4860,6 +4557,7 @@ "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", @@ -4875,6 +4573,7 @@ "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", @@ -4890,6 +4589,7 @@ "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", @@ -4916,6 +4616,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4931,6 +4632,7 @@ "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", @@ -4943,10 +4645,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@jest/transform/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/@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", @@ -4964,6 +4680,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4979,6 +4696,7 @@ "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", @@ -4991,10 +4709,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@jest/types/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/@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", @@ -5005,6 +4737,7 @@ "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", @@ -5015,6 +4748,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -5035,12 +4769,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": 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", @@ -5063,31 +4799,6 @@ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", "license": "MIT" }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", - "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/react": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.8.tgz", - "integrity": "sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==", - "license": "BSD-3-Clause", - "optional": true, - "peerDependencies": { - "@types/react": "17 || 18 || 19" - } - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", - "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0" - } - }, "node_modules/@ljharb/through": { "version": "2.3.14", "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", @@ -5116,15 +4827,6 @@ "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, - "node_modules/@msgpack/msgpack": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz", - "integrity": "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==", - "license": "ISC", - "engines": { - "node": ">= 18" - } - }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -5435,6 +5137,19 @@ "node": ">= 0.6" } }, + "node_modules/@nestjs/cli/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/@nestjs/cli/node_modules/typescript": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", @@ -6199,6 +5914,7 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", + "license": "MIT", "dependencies": { "@microsoft/tsdoc": "^0.15.0", "@nestjs/mapped-types": "2.0.5", @@ -6398,264 +6114,81 @@ } } }, - "node_modules/@next/env": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", - "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", - "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", - "cpu": [ - "arm64" - ], + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">= 10" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", - "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", - "cpu": [ - "x64" + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, "engines": { - "node": ">= 10" + "node": ">= 8" } }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", - "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", - "cpu": [ - "arm64" - ], + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": ">= 8" } }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", - "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", - "cpu": [ - "arm64" - ], + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, "engines": { - "node": ">= 10" + "node": ">= 8" } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", - "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", - "cpu": [ - "x64" - ], + "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==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", - "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", - "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", - "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", - "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", - "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "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==", - "dev": true, - "dependencies": { - "consola": "^3.2.3" - }, - "bin": { - "opencollective": "bin/opencollective.js" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0", - "npm": ">=5.10.0" + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" } }, "node_modules/@nuxt/opencollective/node_modules/consola": { @@ -6663,6 +6196,7 @@ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, + "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" } @@ -6716,12 +6250,25 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@nuxtjs/opencollective/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@openapitools/openapi-generator-cli": { "version": "2.32.0", "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.32.0.tgz", "integrity": "sha512-9HZ3fp3cankdUC89UNsnW+HZFmRUadjjtqOvIIo6/D+bAVs+VJRqyhDy4rT4/cxqcLhXw40njs/vJLj21r60JA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "@inquirer/select": "1.3.3", "@nestjs/axios": "4.0.1", @@ -6757,6 +6304,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", "integrity": "sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "21.3.2", "iterare": "1.2.1", @@ -6789,6 +6337,7 @@ "integrity": "sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -6824,80 +6373,12 @@ } } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/platform-express": { - "version": "11.1.19", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.19.tgz", - "integrity": "sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==", - "dev": true, - "optional": true, - "peer": true, - "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/@openapitools/openapi-generator-cli/node_modules/@nestjs/platform-socket.io": { - "version": "11.1.19", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.19.tgz", - "integrity": "sha512-gu1nPIEaP5Qjjg/Cl8wXyvwGpdZGzgbtK4KcH65YRAA+GTKUkIHb4BNpLJ27Ymq/wqLJKNEbCjajfzD0BEjMGA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "socket.io": "4.8.3", - "tslib": "2.8.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, - "peerDependencies": { - "@nestjs/common": "^11.0.0", - "@nestjs/websockets": "^11.0.0", - "rxjs": "^7.1.0" - } - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/websockets": { - "version": "11.1.19", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.19.tgz", - "integrity": "sha512-2qo8jtIwwwgkqAI1BtnJ02EaFLrRkKA39eYXS8IhZCHilhBHCWdjnJ5cLcFq4oF+s+KZ7LcLGD/3stxJy8ijzg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "iterare": "1.2.1", - "object-hash": "3.0.0", - "tslib": "2.8.1" - }, - "peerDependencies": { - "@nestjs/common": "^11.0.0", - "@nestjs/core": "^11.0.0", - "@nestjs/platform-socket.io": "^11.0.0", - "reflect-metadata": "^0.1.12 || ^0.2.0", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "@nestjs/platform-socket.io": { - "optional": true - } - } - }, "node_modules/@openapitools/openapi-generator-cli/node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" @@ -6915,6 +6396,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -6930,6 +6412,7 @@ "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" @@ -6946,6 +6429,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } @@ -6955,6 +6439,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", "dev": true, + "license": "MIT", "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", @@ -6968,25 +6453,12 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@openapitools/openapi-generator-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", @@ -7000,10 +6472,11 @@ } }, "node_modules/@openapitools/openapi-generator-cli/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -7013,6 +6486,7 @@ "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" @@ -7029,11 +6503,25 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, + "node_modules/@openapitools/openapi-generator-cli/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/@opentelemetry/api": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", @@ -7068,9 +6556,9 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", - "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -7187,18 +6675,34 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.203.0.tgz", - "integrity": "sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==", + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", - "@opentelemetry/otlp-exporter-base": "0.203.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", - "@opentelemetry/otlp-transformer": "0.203.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.203.0.tgz", + "integrity": "sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-metrics": "2.0.1" }, @@ -7224,6 +6728,38 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", + "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { "version": "0.203.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.203.0.tgz", @@ -7258,6 +6794,38 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", + "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { "version": "0.203.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.203.0.tgz", @@ -7293,54 +6861,69 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.215.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.215.0.tgz", - "integrity": "sha512-7ghCl1G84jccmxG3B8UwUMZ1OlequBzB1jt5tZ4DDiAyVKeA4Roz5D6VK8SQ0ZyBQffVyX/rtXrpVXKVzRCGfg==", + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.0", - "@opentelemetry/resources": "2.7.0", - "@opentelemetry/sdk-metrics": "2.7.0", + "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.0.tgz", - "integrity": "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==", + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", + "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.215.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.215.0.tgz", + "integrity": "sha512-7ghCl1G84jccmxG3B8UwUMZ1OlequBzB1jt5tZ4DDiAyVKeA4Roz5D6VK8SQ0ZyBQffVyX/rtXrpVXKVzRCGfg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.7.0", + "@opentelemetry/resources": "2.7.0", + "@opentelemetry/sdk-metrics": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/core": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.0.tgz", - "integrity": "sha512-Vd7h95av/LYRsAVN7wbprvvJnHkq7swMXAo7Uad0Uxf9jl6NSReLa0JNivrcc5BVIx/vl2t+cgdVQQbnVhsR9w==", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", + "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.0", - "@opentelemetry/resources": "2.7.0" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { @@ -7379,6 +6962,22 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { "version": "0.203.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.203.0.tgz", @@ -7413,6 +7012,22 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { "version": "0.203.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.203.0.tgz", @@ -7447,6 +7062,22 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-zipkin": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.0.1.tgz", @@ -7480,6 +7111,22 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/instrumentation": { "version": "0.203.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", @@ -7597,6 +7244,38 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", + "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/propagator-b3": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.0.1.tgz", @@ -7658,12 +7337,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", - "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.0.tgz", + "integrity": "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", + "@opentelemetry/core": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -7674,9 +7353,9 @@ } }, "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", - "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", + "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -7720,38 +7399,54 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-metrics": { + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", - "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", - "@opentelemetry/resources": "2.0.1" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", - "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.0.tgz", + "integrity": "sha512-Vd7h95av/LYRsAVN7wbprvvJnHkq7swMXAo7Uad0Uxf9jl6NSReLa0JNivrcc5BVIx/vl2t+cgdVQQbnVhsR9w==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/core": "2.7.0", + "@opentelemetry/resources": "2.7.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node": { + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", + "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { "version": "0.203.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.203.0.tgz", "integrity": "sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==", @@ -7819,6 +7514,38 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", + "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", @@ -7851,6 +7578,22 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-node": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.0.1.tgz", @@ -7938,9 +7681,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { @@ -7966,9 +7709,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { @@ -7984,688 +7727,414 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@redis/bloom": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz", + "integrity": "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==", "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "peer": true, + "engines": { + "node": ">= 18.19.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@redis/client": "^5.12.1" } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@redis/client": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", + "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", "license": "MIT", + "peer": true, "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18.19.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" }, "peerDependenciesMeta": { - "@types/react": { + "@node-rs/xxhash": { "optional": true }, - "@types/react-dom": { + "@opentelemetry/api": { "optional": true } } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@redis/json": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.12.1.tgz", + "integrity": "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==", "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "peer": true, + "engines": { + "node": ">= 18.19.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@redis/client": "^5.12.1" } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "node_modules/@redis/search": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.12.1.tgz", + "integrity": "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 18.19.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@redis/client": "^5.12.1" } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "node_modules/@redis/time-series": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.12.1.tgz", + "integrity": "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==", "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "peer": true, + "engines": { + "node": ">= 18.19.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "@redis/client": "^5.12.1" } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "node_modules/@simple-libs/stream-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", + "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=18" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://ko-fi.com/dangreen" } }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", + "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": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "type-detect": "4.0.8" } }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "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/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@smithy/config-resolver": { + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", + "node_modules/@smithy/core": { + "version": "3.23.17", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", + "license": "Apache-2.0", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.15.tgz", + "integrity": "sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", + "node_modules/@smithy/hash-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/types": "^4.14.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.14.tgz", + "integrity": "sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/types": "^4.14.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", + "node_modules/@smithy/md5-js": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.14.tgz", + "integrity": "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/types": "^4.14.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@redis/bloom": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz", - "integrity": "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==", - "license": "MIT", - "peer": true, "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@redis/client": "^5.12.1" + "node": ">=18.0.0" } }, "node_modules/@smithy/middleware-retry": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", + "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", @@ -8677,3079 +8146,876 @@ "@smithy/util-retry": "^4.3.6", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" - "node_modules/@redis/client": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", - "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", - "license": "MIT", - "peer": true, - "dependencies": { - "cluster-key-slot": "1.1.2" }, "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@node-rs/xxhash": "^1.1.0", - "@opentelemetry/api": ">=1 <2" - }, - "peerDependenciesMeta": { - "@node-rs/xxhash": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@redis/json": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.12.1.tgz", - "integrity": "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 18.19.0" + "node_modules/@smithy/middleware-serde": { + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@redis/client": "^5.12.1" - } - }, - "node_modules/@redis/search": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.12.1.tgz", - "integrity": "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==", - "license": "MIT", - "peer": true, "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@redis/client": "^5.12.1" + "node": ">=18.0.0" } }, - "node_modules/@redis/time-series": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.12.1.tgz", - "integrity": "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 18.19.0" + "node_modules/@smithy/middleware-stack": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@redis/client": "^5.12.1" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.7.17.tgz", - "integrity": "sha512-gME4Ery7HGTNEGzLckWP7qfD2ec/1UEuUkcGskGeisUnGcAsPH9z2deFFX1szialsgzTNU4/H5ZGdWqZQA8p2w==", - "hasInstallScript": true, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", "license": "Apache-2.0", "dependencies": { - "@reown/appkit-common": "1.7.17", - "@reown/appkit-controllers": "1.7.17", - "@reown/appkit-pay": "1.7.17", - "@reown/appkit-polyfills": "1.7.17", - "@reown/appkit-scaffold-ui": "1.7.17", - "@reown/appkit-ui": "1.7.17", - "@reown/appkit-utils": "1.7.17", - "@reown/appkit-wallet": "1.7.17", - "@walletconnect/universal-provider": "2.21.5", - "bs58": "6.0.0", - "semver": "7.7.2", - "valtio": "2.1.5", - "viem": ">=2.32.0" + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@lit/react": "1.0.8", - "@reown/appkit-siwx": "1.7.17" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-common": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.7.17.tgz", - "integrity": "sha512-zfrlNosQ5XBGC7OBG56+lur0nJWCdRKoWVlUnr0dCVVfBmHIgdhFkRNzDqrX/zGqg4OoWDQLO7qaGiijRskfBQ==", + "node_modules/@smithy/node-http-handler": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", "license": "Apache-2.0", "dependencies": { - "big.js": "6.2.2", - "dayjs": "1.11.13", - "viem": ">=2.32.0" + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-common/node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, - "node_modules/@reown/appkit-controllers": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.7.17.tgz", - "integrity": "sha512-rYgXf3nAzxgu1s10rSfibpAqnm/Y3wyY47v6BpN98Y57NArWqxYXhBtdRQL1ZKpSTV9OmrzwMxPNKePOmFgxZQ==", + "node_modules/@smithy/property-provider": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", "license": "Apache-2.0", "dependencies": { - "@reown/appkit-common": "1.7.17", - "@reown/appkit-wallet": "1.7.17", - "@walletconnect/universal-provider": "2.21.5", - "valtio": "2.1.5", - "viem": ">=2.32.0" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-pay": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.7.17.tgz", - "integrity": "sha512-RukQ5oZ+zGzWy9gu4butVcscZ9GB9/h6zmQFXDo9qkAbOicwZKaLR5XMKrjLQIYisu+ODV/ff6NuxnUYs+/r9Q==", + "node_modules/@smithy/protocol-http": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", "license": "Apache-2.0", "dependencies": { - "@reown/appkit-common": "1.7.17", - "@reown/appkit-controllers": "1.7.17", - "@reown/appkit-ui": "1.7.17", - "@reown/appkit-utils": "1.7.17", - "lit": "3.3.0", - "valtio": "2.1.5" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-polyfills": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.7.17.tgz", - "integrity": "sha512-vWRIYS+wc2ByWKn76KMV7zxqTvQ+512KwXAKQcRulu13AdKvnBbr0eYx+ctvSKL+kZoAp9zj4R3RulX3eXnJ8Q==", + "node_modules/@smithy/querystring-builder": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", "license": "Apache-2.0", "dependencies": { - "buffer": "6.0.3" + "@smithy/types": "^4.14.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-polyfills/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", + "node_modules/@smithy/querystring-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", + "license": "Apache-2.0", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@smithy/service-error-classification": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", - "node_modules/@reown/appkit-scaffold-ui": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.7.17.tgz", - "integrity": "sha512-7nk8DEHQf9/7Ij8Eo85Uj1D/3M9Ybq/LjXyePyaGusZ9E8gf4u/UjKpQK7cTfMNsNl4nrB2mBI9Tk/rwNECdCg==", "license": "Apache-2.0", "dependencies": { - "@reown/appkit-common": "1.7.17", - "@reown/appkit-controllers": "1.7.17", - "@reown/appkit-ui": "1.7.17", - "@reown/appkit-utils": "1.7.17", - "@reown/appkit-wallet": "1.7.17", - "lit": "3.3.0" + "@smithy/types": "^4.14.1" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-siwx": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-siwx/-/appkit-siwx-1.7.17.tgz", - "integrity": "sha512-frTTDnj5111+ZNNyHmEWeXiX0IWFlRhP240kmxKTamLElc2PdLUfQq/1yX8Y3bUBHryISjcQYzEtWSEI2oRYKA==", + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", "license": "Apache-2.0", - "optional": true, "dependencies": { - "@reown/appkit-common": "1.7.17", - "@reown/appkit-controllers": "1.7.17", - "@reown/appkit-scaffold-ui": "1.7.17", - "@reown/appkit-ui": "1.7.17", - "@reown/appkit-utils": "1.7.17", - "bip322-js": "2.0.0", - "bs58": "6.0.0", - "tweetnacl": "1.0.3", - "viem": "2.32.0" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "lit": "3.3.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-siwx/node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", - "license": "MIT", - "optional": true, + "node_modules/@smithy/signature-v4": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "license": "Apache-2.0", "dependencies": { - "@noble/hashes": "1.8.0" + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-siwx/node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node_modules/@smithy/smithy-client": { + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-siwx/node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", - "license": "MIT", - "optional": true, + "node_modules/@smithy/types": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "license": "Apache-2.0", "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "tslib": "^2.6.2" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-siwx/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/wevm" + "node_modules/@smithy/url-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-siwx/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT", - "optional": true + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@reown/appkit-siwx/node_modules/ox": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.8.1.tgz", - "integrity": "sha512-e+z5epnzV+Zuz91YYujecW8cF01mzmrUtWotJ0oEPym/G82uccs7q0WDHTYL3eiONbTUEvcZrptAKLgTBD3u2A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "optional": true, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", "dependencies": { - "@adraffy/ens-normalize": "^1.11.0", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.9.1", - "@noble/hashes": "^1.8.0", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "abitype": "^1.0.8", - "eventemitter3": "5.0.1" + "tslib": "^2.6.2" }, - "peerDependencies": { - "typescript": ">=5.4.0" + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-siwx/node_modules/viem": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.32.0.tgz", - "integrity": "sha512-pHwKXQSyEWX+8ttOQJdU5dSBfYd6L9JxARY/Sx0MBj3uF/Zaiqt6o1SbzjFjQXkNzWSgtxK7H89ZI1SMIA2iLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "optional": true, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", "dependencies": { - "@noble/curves": "1.9.2", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0", - "abitype": "1.0.8", - "isows": "1.0.7", - "ox": "0.8.1", - "ws": "8.18.2" + "tslib": "^2.6.2" }, - "peerDependencies": { - "typescript": ">=5.0.4" + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-siwx/node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "optional": true, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-ui": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.7.17.tgz", - "integrity": "sha512-7lscJjtFZIfdcUv5zAsmgiFG2dMziQE0IfqY3U/H5qhnGW8v4ITcTi1gNS3A4lQrNDbcA083LecfVdyKnTdi1A==", + "node_modules/@smithy/util-middleware": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", "license": "Apache-2.0", "dependencies": { - "@reown/appkit-common": "1.7.17", - "@reown/appkit-controllers": "1.7.17", - "@reown/appkit-wallet": "1.7.17", - "lit": "3.3.0", - "qrcode": "1.5.3" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-universal-connector": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-universal-connector/-/appkit-universal-connector-1.7.17.tgz", - "integrity": "sha512-2LqcKuEURwoHFBYE+6BdsUsPQ5bCN8xXuqxGJeEkAJ95apXTWyLLlpadVofKRh7dzM4lf0Uam8NpWogYWPxnnQ==", + "node_modules/@smithy/util-retry": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", + "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", "license": "Apache-2.0", "dependencies": { - "@reown/appkit": "1.7.17", - "@reown/appkit-common": "1.7.17", - "@walletconnect/types": "2.21.5", - "@walletconnect/universal-provider": "2.21.5", - "bs58": "6.0.0" + "@smithy/service-error-classification": "^4.3.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-utils": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.7.17.tgz", - "integrity": "sha512-QWzHTmSDFy90Bp5pUUQASzcjnJXPiEvasJV68j3PZifenTPDCfFW+VsiHduWNodTHAA/rZ12O3uBQE+stM3xmQ==", + "node_modules/@smithy/util-stream": { + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", "license": "Apache-2.0", "dependencies": { - "@reown/appkit-common": "1.7.17", - "@reown/appkit-controllers": "1.7.17", - "@reown/appkit-polyfills": "1.7.17", - "@reown/appkit-wallet": "1.7.17", - "@wallet-standard/wallet": "1.1.0", - "@walletconnect/logger": "2.1.2", - "@walletconnect/universal-provider": "2.21.5", - "valtio": "2.1.5", - "viem": ">=2.32.0" + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "valtio": "2.1.5" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit-wallet": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.7.17.tgz", - "integrity": "sha512-tgIqHZZJISGCir0reQ/pXcIKXuP7JNqSuEDunfi5whNJi6z27h3g468RGk1Zo+MC//DRnQb01xMrv+iWRr8mCQ==", + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", "license": "Apache-2.0", "dependencies": { - "@reown/appkit-common": "1.7.17", - "@reown/appkit-polyfills": "1.7.17", - "@walletconnect/logger": "2.1.2", - "zod": "3.22.4" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@reown/appkit/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=10" + "node": ">=18.0.0" } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", - "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", - "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", - "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", - "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", - "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@ts-morph/common": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz", - "integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==", - "license": "MIT", - "dependencies": { - "minimatch": "^9.0.4", - "path-browserify": "^1.0.1", - "tinyglobby": "^0.2.9" - } - }, - "node_modules/@ts-morph/common/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/@ts-morph/common/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", + "node_modules/@smithy/util-waiter": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", + "license": "Apache-2.0", "dependencies": { - "balanced-match": "^1.0.0" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@ts-morph/common/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", + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", "dependencies": { - "brace-expansion": "^2.0.2" + "tslib": "^2.6.2" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18.0.0" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", - "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", - "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", - "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.3.tgz", - "integrity": "sha512-dSH3+LCWONlSNQuF34xZrG6Xas7tp2jSSqHb/pMfXWM0vKE4JZOtK3uJfoWouUVW5IGlls75HkXmYLldZ8ySgQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.1.3", - "@noble/secp256k1": "~1.7.0", - "@scure/base": "~1.1.0" - } }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "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/@scure/bip32/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { + "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", - "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.1.1", - "@scure/base": "~1.1.0" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, - "node_modules/@scure/bip39/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", - "dev": true, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://ko-fi.com/dangreen" - } - }, - "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==", - "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==", - "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==", - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@smithy/chunked-blob-reader": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", - "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", - "license": "Apache-2.0", "dependencies": { - "tslib": "^2.6.2" + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/chunked-blob-reader-native": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", - "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-base64": "^4.3.2", - "tslib": "^2.6.2" + "node": ">=18" }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { - "version": "4.4.17", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", - "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core": { - "version": "3.23.17", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", - "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.25", - "@smithy/util-utf8": "^4.2.2", - "@smithy/uuid": "^1.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.6.tgz", - "integrity": "sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==", - "dependencies": { - "@smithy/service-error-classification": "^4.3.1", - "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", - "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-codec": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", - "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.1", - "@smithy/util-hex-encoding": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", - "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", - "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", - "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", - "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-codec": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.17", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", - "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.15", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.15.tgz", - "integrity": "sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/chunked-blob-reader": "^5.2.2", - "@smithy/chunked-blob-reader-native": "^4.2.3", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", - "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-stream-node": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.14.tgz", - "integrity": "sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", - "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", - "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/md5-js": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.14.tgz", - "integrity": "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", - "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.32", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", - "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-middleware": "^4.2.14", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.6.tgz", - "integrity": "sha512-5zhmo2AkstmM/RMKYP0NHfmuYWBR+/umlmSuALgajLxf0X0rLE6d17MfzTxpzkILWVhwvCJkCyPH0AfMlbaucQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/service-error-classification": "^4.3.1", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.5", - "@smithy/uuid": "^1.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.2.20", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", - "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", - "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", - "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", - "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", - "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", - "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", - "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "@smithy/util-uri-escape": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", - "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", - "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", - "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", - "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-uri-escape": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.12.13", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", - "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-stream": "^4.5.25", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", - "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", - "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", - "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", - "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", - "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", - "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.49", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", - "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.54", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", - "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.4.17", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", - "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", - "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", - "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.5.tgz", - "integrity": "sha512-h1IJsbgMDA+jaTjrco/JsyfWOgHRJBv8myB1y4AEI2fjIzD6ktZ7pFAyTw+gwN9GKIAygvC6db0mq0j8N2rFOg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.3.1", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.5.25", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", - "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", - "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", - "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-waiter": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.16.tgz", - "integrity": "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/uuid": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", - "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "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/@stacks/auth": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-7.4.0.tgz", - "integrity": "sha512-2nEPQuhJMifxX7YhuaI1Qn7D0Bjvdav3cmQQXF6PGA/MKIQniExhvHKhuGrshMaJ2OUui+lJxFWA8RCyJVd5ow==", - "license": "MIT", - "dependencies": { - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^7.3.1", - "@stacks/encryption": "^7.4.0", - "@stacks/network": "^7.3.1", - "@stacks/profile": "^7.4.0", - "cross-fetch": "^3.1.5", - "jsontokens": "^4.0.1" - } - }, - "node_modules/@stacks/auth/node_modules/@stacks/common": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.3.1.tgz", - "integrity": "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ==", - "license": "MIT" - }, - "node_modules/@stacks/auth/node_modules/@stacks/network": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-7.3.1.tgz", - "integrity": "sha512-dQjhcwkz8lihSYSCUMf7OYeEh/Eh0++NebDtXbIB3pHWTvNCYEH7sxhYTB1iyunurv31/QEi0RuWdlfXK/BjeA==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.3.1", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/@stacks/bns": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@stacks/bns/-/bns-7.2.0.tgz", - "integrity": "sha512-2WMckA/I8/ztYeQxas8UNQ8YZBHEwpotboWnho7EZbq6kyC9Tc64C7DvSS/sdJZfNIjUSFeY9HtO6kpgSSiLWQ==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.0.2", - "@stacks/network": "^7.2.0", - "@stacks/transactions": "^7.2.0" - } - }, - "node_modules/@stacks/bns/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@stacks/bns/node_modules/@stacks/common": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.3.1.tgz", - "integrity": "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ==", - "license": "MIT" - }, - "node_modules/@stacks/bns/node_modules/@stacks/network": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-7.3.1.tgz", - "integrity": "sha512-dQjhcwkz8lihSYSCUMf7OYeEh/Eh0++NebDtXbIB3pHWTvNCYEH7sxhYTB1iyunurv31/QEi0RuWdlfXK/BjeA==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.3.1", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/@stacks/bns/node_modules/@stacks/transactions": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-7.4.0.tgz", - "integrity": "sha512-scsQO3rSNNKcPHp56Wy5OeZiIpQNmmZOORz8bkQKWjzvzycAodtSWmAoHiMFAKSleR1NyeRIz642fReqlZU9tw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^7.3.1", - "@stacks/network": "^7.3.1", - "c32check": "^2.0.0", - "lodash.clonedeep": "^4.5.0" - } - }, - "node_modules/@stacks/common": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.16.0.tgz", - "integrity": "sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg==", - "license": "MIT", - "dependencies": { - "@types/bn.js": "^5.1.0", - "@types/node": "^18.0.4" - } - }, - "node_modules/@stacks/common/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@stacks/common/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, - "node_modules/@stacks/connect": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/@stacks/connect/-/connect-8.2.6.tgz", - "integrity": "sha512-MXkMVIDPPQLlL83UKb3rNdm7Gh/LQGdPhSsq+sL36KCM6qM1by8+igxX/R/5G+rgn6KdCG+iIRTh4TkAqntrTg==", - "license": "MIT", - "workspaces": [ - "packages/**" - ], - "dependencies": { - "@reown/appkit": "1.7.17", - "@reown/appkit-universal-connector": "1.7.17", - "@scure/base": "^1.2.4", - "@stacks/common": "^7.0.2", - "@stacks/connect-ui": "8.1.2", - "@stacks/network": "^7.0.2", - "@stacks/network-v6": "npm:@stacks/network@^6.16.0", - "@stacks/profile": "^7.0.5", - "@stacks/transactions": "^7.0.5", - "@stacks/transactions-v6": "npm:@stacks/transactions@^6.16.0", - "type-fest": "^5.2.0" - } - }, - "node_modules/@stacks/connect-ui": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@stacks/connect-ui/-/connect-ui-8.1.2.tgz", - "integrity": "sha512-C3T1QmEGJocnmsamQnKJvlvHFfjr/FDebLSokrAAwTr6x76JPS5xuACB7u3a7+vVMUdINV+pFvfn442oic2R5w==", - "license": "MIT", - "dependencies": { - "@stencil/core": "^4.29.3" - } - }, - "node_modules/@stacks/connect/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@stacks/connect/node_modules/@stacks/common": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.3.1.tgz", - "integrity": "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ==", - "license": "MIT" - }, - "node_modules/@stacks/connect/node_modules/@stacks/network": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-7.3.1.tgz", - "integrity": "sha512-dQjhcwkz8lihSYSCUMf7OYeEh/Eh0++NebDtXbIB3pHWTvNCYEH7sxhYTB1iyunurv31/QEi0RuWdlfXK/BjeA==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.3.1", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/@stacks/connect/node_modules/@stacks/transactions": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-7.4.0.tgz", - "integrity": "sha512-scsQO3rSNNKcPHp56Wy5OeZiIpQNmmZOORz8bkQKWjzvzycAodtSWmAoHiMFAKSleR1NyeRIz642fReqlZU9tw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^7.3.1", - "@stacks/network": "^7.3.1", - "c32check": "^2.0.0", - "lodash.clonedeep": "^4.5.0" - } - }, - "node_modules/@stacks/connect/node_modules/type-fest": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", - "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", - "license": "(MIT OR CC0-1.0)", - "dependencies": { - "tagged-tag": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@stacks/encryption": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-7.4.0.tgz", - "integrity": "sha512-jrxgHui3P8M2o2sXs01cAd9uJ6JrtB0g6BDBSKj6K40zvlBbEOiki4wXYQNC3g3AmO73Udv31EhCQrWZtwZspA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@scure/bip39": "1.1.0", - "@stacks/common": "^7.3.1", - "base64-js": "^1.5.1", - "bs58": "^5.0.0", - "ripemd160-min": "^0.0.6", - "varuint-bitcoin": "^1.1.2" - } - }, - "node_modules/@stacks/encryption/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@stacks/encryption/node_modules/@stacks/common": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.3.1.tgz", - "integrity": "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ==", - "license": "MIT" - }, - "node_modules/@stacks/encryption/node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "license": "MIT", - "dependencies": { - "base-x": "^4.0.0" - } - }, - "node_modules/@stacks/network": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.17.0.tgz", - "integrity": "sha512-numHbfKjwco/rbkGPOEz8+FcJ2nBnS/tdJ8R422Q70h3SiA9eqk9RjSzB8p4JP8yW1SZvW+eihADHfMpBuZyfw==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^6.16.0", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/@stacks/network-v6": { - "name": "@stacks/network", - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.17.0.tgz", - "integrity": "sha512-numHbfKjwco/rbkGPOEz8+FcJ2nBnS/tdJ8R422Q70h3SiA9eqk9RjSzB8p4JP8yW1SZvW+eihADHfMpBuZyfw==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^6.16.0", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/@stacks/profile": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-7.4.0.tgz", - "integrity": "sha512-1fJlYtV/mIwc6Mp63hchsebcoPL1So/6kba4uJHdd41DQk0U7zMIN6/QQ+BWF9nQfP8m7pWy5U+xAOLyYU5TMQ==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.3.1", - "@stacks/network": "^7.3.1", - "@stacks/transactions": "^7.4.0", - "jsontokens": "^4.0.1", - "schema-inspector": "^2.0.2", - "zone-file": "^2.0.0-beta.3" - } - }, - "node_modules/@stacks/profile/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@stacks/profile/node_modules/@stacks/common": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.3.1.tgz", - "integrity": "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ==", - "license": "MIT" - }, - "node_modules/@stacks/profile/node_modules/@stacks/network": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-7.3.1.tgz", - "integrity": "sha512-dQjhcwkz8lihSYSCUMf7OYeEh/Eh0++NebDtXbIB3pHWTvNCYEH7sxhYTB1iyunurv31/QEi0RuWdlfXK/BjeA==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.3.1", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/@stacks/profile/node_modules/@stacks/transactions": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-7.4.0.tgz", - "integrity": "sha512-scsQO3rSNNKcPHp56Wy5OeZiIpQNmmZOORz8bkQKWjzvzycAodtSWmAoHiMFAKSleR1NyeRIz642fReqlZU9tw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^7.3.1", - "@stacks/network": "^7.3.1", - "c32check": "^2.0.0", - "lodash.clonedeep": "^4.5.0" - } - }, - "node_modules/@stacks/stacking": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-7.4.0.tgz", - "integrity": "sha512-rA+OddO0eCw74eeJnwEAGVHZFLU/F8TFedLmn0sK3UfZQY5OcwbA0hOTh9JeBEC/kOEbfDFShunOmLQ+AexRLg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@scure/base": "1.1.1", - "@stacks/common": "^7.3.1", - "@stacks/encryption": "^7.4.0", - "@stacks/network": "^7.3.1", - "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^7.4.0", - "bs58": "^5.0.0" - } - }, - "node_modules/@stacks/stacking/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@stacks/stacking/node_modules/@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@stacks/stacking/node_modules/@stacks/common": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.3.1.tgz", - "integrity": "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ==", - "license": "MIT" - }, - "node_modules/@stacks/stacking/node_modules/@stacks/network": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-7.3.1.tgz", - "integrity": "sha512-dQjhcwkz8lihSYSCUMf7OYeEh/Eh0++NebDtXbIB3pHWTvNCYEH7sxhYTB1iyunurv31/QEi0RuWdlfXK/BjeA==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.3.1", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/@stacks/stacking/node_modules/@stacks/transactions": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-7.4.0.tgz", - "integrity": "sha512-scsQO3rSNNKcPHp56Wy5OeZiIpQNmmZOORz8bkQKWjzvzycAodtSWmAoHiMFAKSleR1NyeRIz642fReqlZU9tw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^7.3.1", - "@stacks/network": "^7.3.1", - "c32check": "^2.0.0", - "lodash.clonedeep": "^4.5.0" - } - }, - "node_modules/@stacks/stacking/node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "license": "MIT", - "dependencies": { - "base-x": "^4.0.0" - } - }, - "node_modules/@stacks/stacks-blockchain-api-types": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-0.61.0.tgz", - "integrity": "sha512-yPOfTUboo5eA9BZL/hqMcM71GstrFs9YWzOrJFPeP4cOO1wgYvAcckgBRbgiE3NqeX0A7SLZLDAXLZbATuRq9w==", - "license": "ISC" - }, - "node_modules/@stacks/storage": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-7.4.0.tgz", - "integrity": "sha512-te94se3ZqucbU/O5p5uzl2gZSCD7p1W6mQLBFeM9LHA+VVCEnEMEK6sXoYEoiMl9EwicWnACUVIDlvHGZiPVFQ==", - "license": "MIT", - "dependencies": { - "@stacks/auth": "^7.4.0", - "@stacks/common": "^7.3.1", - "@stacks/encryption": "^7.4.0", - "@stacks/network": "^7.3.1", - "base64-js": "^1.5.1", - "jsontokens": "^4.0.1" - } - }, - "node_modules/@stacks/storage/node_modules/@stacks/common": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.3.1.tgz", - "integrity": "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ==", - "license": "MIT" - }, - "node_modules/@stacks/storage/node_modules/@stacks/network": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-7.3.1.tgz", - "integrity": "sha512-dQjhcwkz8lihSYSCUMf7OYeEh/Eh0++NebDtXbIB3pHWTvNCYEH7sxhYTB1iyunurv31/QEi0RuWdlfXK/BjeA==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.3.1", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/@stacks/transactions": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.17.0.tgz", - "integrity": "sha512-FUah2BRgV66ApLcEXGNGhwyFTRXqX5Zco3LpiM3essw8PF0NQlHwwdPgtDko5RfrJl3LhGXXe/30nwsfNnB3+g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^6.16.0", - "@stacks/network": "^6.17.0", - "c32check": "^2.0.0", - "lodash.clonedeep": "^4.5.0" - } - }, - "node_modules/@stacks/transactions-v6": { - "name": "@stacks/transactions", - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.17.0.tgz", - "integrity": "sha512-FUah2BRgV66ApLcEXGNGhwyFTRXqX5Zco3LpiM3essw8PF0NQlHwwdPgtDko5RfrJl3LhGXXe/30nwsfNnB3+g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^6.16.0", - "@stacks/network": "^6.17.0", - "c32check": "^2.0.0", - "lodash.clonedeep": "^4.5.0" - } - }, - "node_modules/@stacks/transactions-v6/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@stacks/transactions/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@stacks/wallet-sdk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-7.2.0.tgz", - "integrity": "sha512-w4UmIaulB03ki0eosWA2ju4vXtF1N+n+nX+/GuV8ZW3rbZ7xeRCv16IzZZL6TspMcaUKyZKTVB2uximqBNbqPQ==", - "license": "MIT", - "dependencies": { - "@scure/bip32": "1.1.3", - "@scure/bip39": "1.1.0", - "@stacks/auth": "^7.2.0", - "@stacks/common": "^7.0.2", - "@stacks/encryption": "^7.2.0", - "@stacks/network": "^7.2.0", - "@stacks/profile": "^7.2.0", - "@stacks/storage": "^7.2.0", - "@stacks/transactions": "^7.2.0", - "c32check": "^2.0.0", - "jsontokens": "^4.0.1", - "zone-file": "^2.0.0-beta.3" - } - }, - "node_modules/@stacks/wallet-sdk/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@stacks/wallet-sdk/node_modules/@stacks/common": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.3.1.tgz", - "integrity": "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ==", - "license": "MIT" - }, - "node_modules/@stacks/wallet-sdk/node_modules/@stacks/network": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-7.3.1.tgz", - "integrity": "sha512-dQjhcwkz8lihSYSCUMf7OYeEh/Eh0++NebDtXbIB3pHWTvNCYEH7sxhYTB1iyunurv31/QEi0RuWdlfXK/BjeA==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.3.1", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/@stacks/wallet-sdk/node_modules/@stacks/transactions": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-7.4.0.tgz", - "integrity": "sha512-scsQO3rSNNKcPHp56Wy5OeZiIpQNmmZOORz8bkQKWjzvzycAodtSWmAoHiMFAKSleR1NyeRIz642fReqlZU9tw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^7.3.1", - "@stacks/network": "^7.3.1", - "c32check": "^2.0.0", - "lodash.clonedeep": "^4.5.0" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@stencil/core": { - "version": "4.43.4", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.43.4.tgz", - "integrity": "sha512-QWawMM1XIpSz4k+k+VyHZMr2YSxlCNAPWO/jTdJ+2kdgdN7ZQVEFZpc4WBm3E3mrDPTZ79lLcnIPa399bg4XOg==", - "license": "MIT", - "bin": { - "stencil": "bin/stencil" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=7.10.0" - }, - "optionalDependencies": { - "@rollup/rollup-darwin-arm64": "4.44.0", - "@rollup/rollup-darwin-x64": "4.44.0", - "@rollup/rollup-linux-arm64-gnu": "4.44.0", - "@rollup/rollup-linux-arm64-musl": "4.44.0", - "@rollup/rollup-linux-x64-gnu": "4.44.0", - "@rollup/rollup-linux-x64-musl": "4.44.0", - "@rollup/rollup-win32-arm64-msvc": "4.44.0", - "@rollup/rollup-win32-x64-msvc": "4.44.0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", - "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.100.5", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.5.tgz", - "integrity": "sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.100.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.5.tgz", - "integrity": "sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.100.5" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@tokenizer/inflate": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", - "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "fflate": "^0.8.2", - "token-types": "^6.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "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==", - "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==", - "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==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/bcryptjs": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bull": { - "version": "3.15.9", - "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.9.tgz", - "integrity": "sha512-MPUcyPPQauAmynoO3ezHAmCOhbB0pWmYyijr/5ctaCqhbKWsjW0YCod38ZcLzUBprosfZ9dPqfYIcfdKjk7RNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ioredis": "*", - "@types/redis": "^2.8.0" - } - }, - "node_modules/@types/command-line-args": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", - "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", - "license": "MIT" - }, - "node_modules/@types/command-line-usage": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", - "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==", - "license": "MIT" - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/conventional-commits-parser": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", - "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/csurf": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.5.tgz", - "integrity": "sha512-5rw87+5YGixyL2W8wblSUl5DSZi5YOlXE6Awwn2ofLvqKr/1LruKffrQipeJKUX44VaxKj8m5es3vfhltJTOoA==", - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*" - } - }, - "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/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express-session": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.19.0.tgz", - "integrity": "sha512-GbypG0bog68UbOq2tSAp7SclvCUm3ha1uDi58OPRGK1NfRvCIu7Gz0M7fTGtpNG1T9a29GpuurQj9zEcT/lMXQ==", - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/fluent-ffmpeg": { - "version": "2.1.28", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.28.tgz", - "integrity": "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/handlebars": { - "version": "4.0.40", - "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.40.tgz", - "integrity": "sha512-sGWNtsjNrLOdKha2RV1UeF8+UbQnPSG7qbe5wwbni0mw4h2gHXyPFUMOC+xwGirIiiydM/HSqjDO4rk6NFB18w==", - "license": "MIT" - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "license": "MIT" - }, - "node_modules/@types/ioredis": { - "version": "4.28.10", - "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz", - "integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==", - "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==", - "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==", - "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==", - "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/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "devOptional": true, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT" - }, - "node_modules/@types/luxon": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", - "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", - "license": "MIT" - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-4.0.0.tgz", - "integrity": "sha512-5eEkJZ/BLvTE3vXGKkWlyTSUVZuzj23Wj8PoyOq2lt5I3CYbiLBOPb3XmCW6QcuOibIUE6emHXHt9E/F/rCa6w==", - "deprecated": "This is a stub types definition. mime provides its own type definitions, so you do not need this installed.", - "dev": true, - "license": "MIT", - "dependencies": { - "mime": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/multer": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", - "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/@types/nodemailer": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "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/passport": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", - "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "dev": true, + "license": "MIT" + }, + "node_modules/@ts-morph/common": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz", + "integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==", "license": "MIT", "dependencies": { - "@types/express": "*" + "minimatch": "^9.0.4", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.9" } }, - "node_modules/@types/passport-jwt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", - "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", - "dev": true, + "node_modules/@ts-morph/common/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/@ts-morph/common/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": { - "@types/jsonwebtoken": "*", - "@types/passport-strategy": "*" + "balanced-match": "^1.0.0" } }, - "node_modules/@types/passport-strategy": { - "version": "0.2.38", - "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", - "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", - "dev": true, - "license": "MIT", + "node_modules/@ts-morph/common/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": { - "@types/express": "*", - "@types/passport": "*" + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "devOptional": true, "license": "MIT" }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" }, - "node_modules/@types/redis": { - "version": "2.8.32", - "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz", - "integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==", + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "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": { - "@types/node": "*" + "@babel/types": "^7.0.0" } }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "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": { - "@types/http-errors": "*", - "@types/node": "*" + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/@types/sharp": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", - "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", + "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": { - "@types/node": "*" + "@babel/types": "^7.28.2" } }, - "node_modules/@types/socket.io": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", - "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "license": "MIT", "dependencies": { - "socket.io": "*" + "@types/connect": "*", + "@types/node": "*" } }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "license": "MIT" - }, - "node_modules/@types/stripe": { - "version": "8.0.416", - "resolved": "https://registry.npmjs.org/@types/stripe/-/stripe-8.0.416.tgz", - "integrity": "sha512-LDA574j7g30dg4R+SI1JIpkS+rkIuXgbe6+/qlf62avd7ZNntbbl2DYZwAIj9CfJYVh7FG/PLeoNB5OXTsEehg==", + "node_modules/@types/bull": { + "version": "3.15.9", + "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.9.tgz", + "integrity": "sha512-MPUcyPPQauAmynoO3ezHAmCOhbB0pWmYyijr/5ctaCqhbKWsjW0YCod38ZcLzUBprosfZ9dPqfYIcfdKjk7RNQ==", + "dev": true, "license": "MIT", "dependencies": { - "stripe": "*" + "@types/ioredis": "*", + "@types/redis": "^2.8.0" } }, - "node_modules/@types/superagent": { - "version": "8.1.9", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", - "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "license": "MIT", "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*", - "form-data": "^4.0.0" + "@types/node": "*" } }, - "node_modules/@types/supertest": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", - "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", + "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", "dev": true, "license": "MIT", "dependencies": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" + "@types/node": "*" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "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==", + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true, "license": "MIT" }, - "node_modules/@types/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/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "license": "MIT", "dependencies": { "@types/node": "*" } }, - "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==", + "node_modules/@types/csurf": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.5.tgz", + "integrity": "sha512-5rw87+5YGixyL2W8wblSUl5DSZi5YOlXE6Awwn2ofLvqKr/1LruKffrQipeJKUX44VaxKj8m5es3vfhltJTOoA==", "license": "MIT", "dependencies": { - "@types/yargs-parser": "*" + "@types/express-serve-static-core": "*" } }, - "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==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", - "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "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": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/type-utils": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "@types/estree": "*", + "@types/json-schema": "*" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", - "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "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": { - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "@types/eslint": "*", + "@types/estree": "*" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", - "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.0", - "@typescript-eslint/types": "^8.59.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", - "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", - "dev": true, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", - "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", - "dev": true, + "node_modules/@types/express-session": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-GbypG0bog68UbOq2tSAp7SclvCUm3ha1uDi58OPRGK1NfRvCIu7Gz0M7fTGtpNG1T9a29GpuurQj9zEcT/lMXQ==", "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "dependencies": { + "@types/express": "*" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", - "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", - "dev": true, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", - "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "node_modules/@types/fluent-ffmpeg": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.28.tgz", + "integrity": "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "dependencies": { + "@types/node": "*" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", - "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "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": { - "@typescript-eslint/project-service": "8.59.0", - "@typescript-eslint/tsconfig-utils": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "@types/node": "*" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", - "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "node_modules/@types/handlebars": { + "version": "4.0.40", + "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.40.tgz", + "integrity": "sha512-sGWNtsjNrLOdKha2RV1UeF8+UbQnPSG7qbe5wwbni0mw4h2gHXyPFUMOC+xwGirIiiydM/HSqjDO4rk6NFB18w==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/ioredis": { + "version": "4.28.10", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz", + "integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "@types/node": "*" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", - "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "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": { - "@typescript-eslint/types": "8.59.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@types/istanbul-lib-coverage": "*" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "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": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@wagmi/connectors": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-8.0.5.tgz", - "integrity": "sha512-Xxysn4jalQS5W4b687LX0znp2eswonS/1fvRRVAlPD+LG15YRs8nHaC7xAjI9lVMWAx2TePw9Car6pQ5nzYVsA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@base-org/account": "^2.5.1", - "@coinbase/wallet-sdk": "^4.3.6", - "@metamask/connect-evm": "~0.9.0", - "@safe-global/safe-apps-provider": "~0.18.6", - "@safe-global/safe-apps-sdk": "^9.1.0", - "@wagmi/core": "3.4.6", - "@walletconnect/ethereum-provider": "^2.21.1", - "accounts": "~0.6.7", - "porto": "~0.2.35", - "typescript": ">=5.7.3", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "@base-org/account": { - "optional": true - }, - "@coinbase/wallet-sdk": { - "optional": true - }, - "@metamask/connect-evm": { - "optional": true - }, - "@safe-global/safe-apps-provider": { - "optional": true - }, - "@safe-global/safe-apps-sdk": { - "optional": true - }, - "@walletconnect/ethereum-provider": { - "optional": true - }, - "accounts": { - "optional": true - }, - "porto": { - "optional": true - }, - "typescript": { - "optional": true - } + "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/@wagmi/core": { - "version": "3.4.6", - "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.4.6.tgz", - "integrity": "sha512-wDZpRfzQo6NJj770mt23HdeU9O0MDO3cnxVP7tP/1HL7DLqOGMN3hADIc0wEF51ejrpnJlGLf8hS1qb2ZAzqJA==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "license": "MIT", "dependencies": { - "eventemitter3": "5.0.1", - "mipd": "0.0.7", - "zustand": "5.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@tanstack/query-core": ">=5.0.0", - "accounts": "~0.8.1", - "typescript": ">=5.7.3", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "@tanstack/query-core": { - "optional": true - }, - "accounts": { - "optional": true - }, - "typescript": { - "optional": true - } + "@types/ms": "*", + "@types/node": "*" } }, - "node_modules/@wagmi/core/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "license": "MIT" }, - "node_modules/@wagmi/core/node_modules/zustand": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", - "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-4.0.0.tgz", + "integrity": "sha512-5eEkJZ/BLvTE3vXGKkWlyTSUVZuzj23Wj8PoyOq2lt5I3CYbiLBOPb3XmCW6QcuOibIUE6emHXHt9E/F/rCa6w==", + "deprecated": "This is a stub types definition. mime provides its own type definitions, so you do not need this installed.", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } + "dependencies": { + "mime": "*" } }, - "node_modules/@wallet-standard/base": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", - "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16" - } + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" }, - "node_modules/@wallet-standard/wallet": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/wallet/-/wallet-1.1.0.tgz", - "integrity": "sha512-Gt8TnSlDZpAl+RWOOAB/kuvC7RpcdWAlFbHNoi4gsXsfaWa1QCT6LBcfIYTPdOZC9OVZUDwqGuGAcqZejDmHjg==", - "license": "Apache-2.0", + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "license": "MIT", "dependencies": { - "@wallet-standard/base": "^1.1.0" - }, - "engines": { - "node": ">=16" + "@types/express": "*" } }, "node_modules/@types/mute-stream": { @@ -11757,261 +9023,170 @@ "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "license": "MIT", - "node_modules/@walletconnect/core": { - "version": "2.21.5", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.5.tgz", - "integrity": "sha512-CxGbio1TdCkou/TYn8X6Ih1mUX3UtFTk+t618/cIrT3VX5IjQW09n9I/pVafr7bQbBtm9/ATr7ugUEMrLu5snA==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.5", - "@walletconnect/utils": "2.21.5", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.39.3", - "events": "3.3.0", - "uint8arrays": "3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@walletconnect/environment": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", - "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", + "version": "20.19.40", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.40.tgz", + "integrity": "sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q==", "license": "MIT", "dependencies": { - "tslib": "1.14.1" + "undici-types": "~6.21.0" } }, - "node_modules/@walletconnect/environment/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/events": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", - "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", "license": "MIT", "dependencies": { - "keyvaluestorage-interface": "^1.0.0", - "tslib": "1.14.1" + "@types/node": "*", + "form-data": "^4.0.4" } }, - "node_modules/@walletconnect/events/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/heartbeat": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", - "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", + "node_modules/@types/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", "license": "MIT", "dependencies": { - "@walletconnect/events": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "events": "^3.3.0" + "@types/node": "*" } }, - "node_modules/@walletconnect/jsonrpc-http-connection": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.8.tgz", - "integrity": "sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==", + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, "license": "MIT", "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.1", - "cross-fetch": "^3.1.4", - "events": "^3.3.0" + "@types/express": "*" } }, - "node_modules/@walletconnect/jsonrpc-provider": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", - "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, "license": "MIT", "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.8", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0" + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" } }, - "node_modules/@walletconnect/jsonrpc-types": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", - "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, "license": "MIT", "dependencies": { - "events": "^3.3.0", - "keyvaluestorage-interface": "^1.0.0" + "@types/express": "*", + "@types/passport": "*" } }, - "node_modules/@walletconnect/jsonrpc-utils": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", - "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", - "license": "MIT", - "dependencies": { - "@walletconnect/environment": "^1.0.1", - "@walletconnect/jsonrpc-types": "^1.0.3", - "tslib": "1.14.1" - } + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "license": "MIT" }, - "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" }, - "node_modules/@walletconnect/jsonrpc-ws-connection": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", - "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", + "node_modules/@types/redis": { + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz", + "integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==", + "dev": true, "license": "MIT", "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0", - "ws": "^7.5.1" - } - }, - "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "@types/node": "*" } }, - "node_modules/@walletconnect/keyvaluestorage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", - "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "license": "MIT", "dependencies": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" - }, - "peerDependencies": { - "@react-native-async-storage/async-storage": "1.x" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } + "@types/node": "*" } }, - "node_modules/@walletconnect/logger": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-2.1.2.tgz", - "integrity": "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw==", + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "license": "MIT", "dependencies": { - "@walletconnect/safe-json": "^1.0.2", - "pino": "7.11.0" + "@types/http-errors": "*", + "@types/node": "*" } }, - "node_modules/@walletconnect/relay-api": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", - "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", + "node_modules/@types/sharp": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", + "dev": true, "license": "MIT", "dependencies": { - "@walletconnect/jsonrpc-types": "^1.0.2" + "@types/node": "*" } }, - "node_modules/@walletconnect/relay-auth": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", - "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/curves": "1.8.0", - "@noble/hashes": "1.7.0", - "@walletconnect/safe-json": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "uint8arrays": "^3.0.0" + "socket.io": "*" } }, - "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==", + "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/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/wrap-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", - "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", - "dev": true - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", - "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "node_modules/@types/stripe": { + "version": "8.0.416", + "resolved": "https://registry.npmjs.org/@types/stripe/-/stripe-8.0.416.tgz", + "integrity": "sha512-LDA574j7g30dg4R+SI1JIpkS+rkIuXgbe6+/qlf62avd7ZNntbbl2DYZwAIj9CfJYVh7FG/PLeoNB5OXTsEehg==", "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "dependencies": { + "stripe": "*" + } + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" } }, - "node_modules/@walletconnect/safe-json": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", - "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "1.14.1" + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" } }, "node_modules/@types/swagger-ui-express": { @@ -12037,303 +9212,279 @@ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", "license": "MIT" - "node_modules/@walletconnect/safe-json/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" }, - "node_modules/@walletconnect/sign-client": { - "version": "2.21.5", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.5.tgz", - "integrity": "sha512-IAs/IqmE1HVL9EsvqkNRU4NeAYe//h9NwqKi7ToKYZv4jhcC3BBemUD1r8iQJSTHMhO41EKn1G9/DiBln3ZiwQ==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", "dependencies": { - "@walletconnect/core": "2.21.5", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.5", - "@walletconnect/utils": "2.21.5", - "events": "3.3.0" + "@types/node": "*" } }, - "node_modules/@walletconnect/time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", - "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", + "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": { - "tslib": "1.14.1" + "@types/yargs-parser": "*" } }, - "node_modules/@walletconnect/time/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" + "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/@walletconnect/types": { - "version": "2.21.5", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.5.tgz", - "integrity": "sha512-kpTXbenKeMdaz6mgMN/jKaHHbu6mdY3kyyrddzE/mthOd2KLACVrZr7hrTf+Fg2coPVen5d1KKyQjyECEdzOCw==", - "license": "Apache-2.0", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", + "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/type-utils": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@walletconnect/universal-provider": { - "version": "2.21.5", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.5.tgz", - "integrity": "sha512-SMXGGXyj78c8Ru2f665ZFZU24phn0yZyCP5Ej7goxVQxABwqWKM/odj3j/IxZv+hxA8yU13yxaubgVefnereqw==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.5", - "@walletconnect/types": "2.21.5", - "@walletconnect/utils": "2.21.5", - "es-toolkit": "1.39.3", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/utils": { - "version": "2.21.5", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.5.tgz", - "integrity": "sha512-RSPSxPvGMuvfGhd5au1cf9cmHB/KVVLFotJR9ltisjFABGtH2215U5oaVp+a7W18QX37aemejRkvacqOELVySA==", - "license": "Apache-2.0", - "dependencies": { - "@msgpack/msgpack": "3.1.2", - "@noble/ciphers": "1.3.0", - "@noble/curves": "1.9.2", - "@noble/hashes": "1.8.0", - "@scure/base": "1.2.6", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.5", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "blakejs": "1.2.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.1", - "viem": "2.31.0" - } - }, - "node_modules/@walletconnect/utils/node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", + "node_modules/@typescript-eslint/parser": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", + "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3" }, "engines": { - "node": "^14.21.3 || >=16" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@walletconnect/utils/node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@typescript-eslint/tsconfig-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@walletconnect/utils/node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", + "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@walletconnect/utils/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "dev": true, "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://github.com/sponsors/wevm" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@walletconnect/utils/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/@walletconnect/utils/node_modules/ox": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.7.1.tgz", - "integrity": "sha512-+k9fY9PRNuAMHRFIUbiK9Nt5seYHHzSQs9Bj+iMETcGtlpS7SmBzcGSVUQO3+nqGLEiNK4598pHNFlVRaZbRsg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", + "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "dev": true, "license": "MIT", "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, - "peerDependencies": { - "typescript": ">=5.4.0" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.31.0.tgz", - "integrity": "sha512-U7OMQ6yqK+bRbEIarf2vqxL7unSEQvNxvML/1zG7suAmKuJmipqdVTVJGKBCJiYsm/EremyO2FS4dHIPpGv+eA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], + "node_modules/@typescript-eslint/types": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "dev": true, "license": "MIT", - "dependencies": { - "@noble/curves": "1.9.1", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0", - "abitype": "1.0.8", - "isows": "1.0.7", - "ox": "0.7.1", - "ws": "8.18.2" - }, - "peerDependencies": { - "typescript": ">=5.0.4" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@walletconnect/utils/node_modules/viem/node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@typescript-eslint/project-service": "8.59.2", + "@typescript-eslint/tsconfig-utils": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^14.21.3 || >=16" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@walletconnect/utils/node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", + "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "dev": true, "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2" + }, "engines": { - "node": ">=10.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@walletconnect/window-getters": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", - "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "1.14.1" + "@typescript-eslint/types": "8.59.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@walletconnect/window-getters/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/window-metadata": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", - "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", - "license": "MIT", - "dependencies": { - "@walletconnect/window-getters": "^1.0.1", - "tslib": "1.14.1" + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@walletconnect/window-metadata/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", @@ -12562,27 +9713,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/abitype": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", - "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -12668,14 +9798,15 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -12743,6 +9874,7 @@ "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" @@ -12754,18 +9886,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-escapes/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==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "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", @@ -12779,6 +9899,7 @@ "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" @@ -12800,6 +9921,7 @@ "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", @@ -12813,6 +9935,7 @@ "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" @@ -12821,41 +9944,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/apache-arrow": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-21.1.0.tgz", - "integrity": "sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.11", - "@types/command-line-args": "^5.2.3", - "@types/command-line-usage": "^5.0.4", - "@types/node": "^24.0.3", - "command-line-args": "^6.0.1", - "command-line-usage": "^7.0.1", - "flatbuffers": "^25.1.24", - "json-bignum": "^0.0.3", - "tslib": "^2.6.2" - }, - "bin": { - "arrow2csv": "bin/arrow2csv.js" - } - }, - "node_modules/apache-arrow/node_modules/@types/node": { - "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/apache-arrow/node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, "node_modules/app-root-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", @@ -12884,27 +9972,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/array-back": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz", - "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -12937,6 +10004,7 @@ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, @@ -12964,15 +10032,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -12989,20 +10048,20 @@ } }, "node_modules/axios": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", - "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -13017,6 +10076,7 @@ "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", @@ -13038,6 +10098,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -13053,6 +10114,7 @@ "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", @@ -13065,10 +10127,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/babel-jest/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/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", @@ -13085,6 +10161,7 @@ "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", @@ -13101,6 +10178,7 @@ "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" @@ -13110,6 +10188,7 @@ "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", @@ -13125,6 +10204,7 @@ "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", @@ -13151,6 +10231,7 @@ "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", @@ -13218,9 +10299,9 @@ } }, "node_modules/bare-os": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", - "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", "license": "Apache-2.0", "engines": { "bare": ">=1.14.0" @@ -13236,9 +10317,9 @@ } }, "node_modules/bare-stream": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz", - "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", "license": "Apache-2.0", "dependencies": { "streamx": "^2.25.0", @@ -13262,20 +10343,14 @@ } }, "node_modules/bare-url": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", - "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", + "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", "license": "Apache-2.0", "dependencies": { "bare-path": "^3.0.0" } }, - "node_modules/base-x": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", - "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", - "license": "MIT" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -13306,9 +10381,10 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", - "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -13322,6 +10398,7 @@ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" } @@ -13349,26 +10426,6 @@ "bcrypt": "bin/bcrypt" } }, - "node_modules/bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", - "license": "MIT", - "optional": true - }, - "node_modules/big.js": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", - "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/bigjs" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -13382,154 +10439,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bintrees": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", "license": "MIT" }, - "node_modules/bip174": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", - "integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/bip322-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bip322-js/-/bip322-js-2.0.0.tgz", - "integrity": "sha512-wyewxyCLl+wudZWiyvA46SaNQL41dVDJ+sx4HvD6zRXScHzAycwuKEMmbvr2qN+P/IIYArF4XVqlyZVnjutELQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@bitcoinerlab/secp256k1": "^1.1.1", - "bitcoinjs-lib": "^6.1.5", - "bitcoinjs-message": "^2.2.0", - "ecpair": "^2.1.0", - "elliptic": "^6.5.5", - "fast-sha256": "^1.3.0", - "secp256k1": "^5.0.0" - } - }, - "node_modules/bip66": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/bitcoinjs-lib": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz", - "integrity": "sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@noble/hashes": "^1.2.0", - "bech32": "^2.0.0", - "bip174": "^2.1.1", - "bs58check": "^3.0.1", - "typeforce": "^1.11.3", - "varuint-bitcoin": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/bitcoinjs-message": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.2.0.tgz", - "integrity": "sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==", - "license": "MIT", - "optional": true, - "dependencies": { - "bech32": "^1.1.3", - "bs58check": "^2.1.2", - "buffer-equals": "^1.0.3", - "create-hash": "^1.1.2", - "secp256k1": "^3.0.1", - "varuint-bitcoin": "^1.0.1" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/bitcoinjs-message/node_modules/base-x": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", - "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/bitcoinjs-message/node_modules/bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", - "license": "MIT", - "optional": true - }, - "node_modules/bitcoinjs-message/node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", - "license": "MIT", - "optional": true, - "dependencies": { - "base-x": "^3.0.2" - } - }, - "node_modules/bitcoinjs-message/node_modules/bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", - "license": "MIT", - "optional": true, - "dependencies": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/bitcoinjs-message/node_modules/secp256k1": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.1.tgz", - "integrity": "sha512-tArjQw2P0RTdY7QmkNehgp6TVvQXq6ulIhxv8gaH6YubKG/wxxAoNKcbuXjDhybbc+b2Ihc7e0xxiGN744UIiQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bindings": "^1.5.0", - "bip66": "^1.1.5", - "bn.js": "^4.11.8", - "create-hash": "^1.2.0", - "drbg.js": "^1.0.1", - "elliptic": "^6.5.7", - "nan": "^2.14.0", - "safe-buffer": "^5.1.2" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -13541,19 +10456,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/blakejs": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", - "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", - "license": "MIT" - }, - "node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT", - "optional": true - }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -13652,10 +10554,51 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/boxen/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/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/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==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -13677,32 +10620,11 @@ "node": ">=8" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "license": "MIT", - "optional": true - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "license": "MIT", - "optional": true, - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "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", @@ -13745,46 +10667,11 @@ "node": ">= 6" } }, - "node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "dependencies": { - "base-x": "^5.0.0" - } - }, - "node_modules/bs58/node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT" - }, - "node_modules/bs58check": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", - "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@noble/hashes": "^1.2.0", - "bs58": "^5.0.0" - } - }, - "node_modules/bs58check/node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "base-x": "^4.0.0" - } - }, "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" @@ -13820,28 +10707,11 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/buffer-equals": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", - "integrity": "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "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/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "license": "MIT", - "optional": true + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" }, "node_modules/bull": { "version": "4.16.5", @@ -13865,6 +10735,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -13890,19 +10761,6 @@ "node": ">= 0.8" } }, - "node_modules/c32check": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/c32check/-/c32check-2.0.0.tgz", - "integrity": "sha512-rpwfAcS/CMqo0oCqDf3r9eeLgScRE3l/xHDCXhM3UyrfvIn7PrLq63uHh7yYbv8NzaZn5MVsVhIRpQ+5GZ5HyA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.2", - "base-x": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cache-manager": { "version": "7.2.8", "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.8.tgz", @@ -14073,6 +10931,7 @@ "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" @@ -14091,9 +10950,10 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001790", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", - "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -14123,56 +10983,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk-template": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", - "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/chalk-template?sponsor=1" - } - }, - "node_modules/chalk-template/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/chalk-template/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "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" @@ -14194,90 +11009,6 @@ "node": ">=16" } }, - "node_modules/chess.js": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/chess.js/-/chess.js-1.4.0.tgz", - "integrity": "sha512-BBJgrrtKQOzFLonR0l+k64A98NLemPwNsCskwb+29bRwobUa4iTm51E1kwGPbWXAcfdDa18nad6vpPPKPWarqw==", - "license": "BSD-2-Clause" - }, - "node_modules/chessify-protocol": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/chessify-protocol/-/chessify-protocol-0.1.2.tgz", - "integrity": "sha512-zvFQ4yv764vZ9hOAUfiw4RQS+7n0WJy1aINI4OwI+ayX4jFIxXBftYgK+CqyyQIXvjtikXkw1S55f1sFjZyHgA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-toast": "^1.2.15", - "@radix-ui/react-tooltip": "^1.2.8", - "@stacks/auth": "^7.4.0", - "@stacks/bns": "^7.2.0", - "@stacks/common": "^7.3.1", - "@stacks/connect": "^8.2.6", - "@stacks/encryption": "^7.4.0", - "@stacks/network": "^7.3.1", - "@stacks/profile": "^7.4.0", - "@stacks/stacking": "^7.4.0", - "@stacks/storage": "^7.4.0", - "@stacks/transactions": "^7.4.0", - "@stacks/wallet-sdk": "^7.2.0", - "@tanstack/react-query": "^5.99.0", - "chess.js": "^1.4.0", - "framer-motion": "^12.38.0", - "next": "16.2.1", - "next-themes": "^0.4.6", - "react-chessboard": "^5.10.0", - "viem": "^2.48.0", - "wagmi": "^3.6.2", - "zustand": "^5.0.12" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/chessify-protocol/node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/chessify-protocol/node_modules/@stacks/common": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.3.1.tgz", - "integrity": "sha512-29ANTFcSSlXnGQlgDVWg7OQ74lgQhu3x8JkeN19Q+UE/1lbQrzcctgPHG74XHjWNp8NPBqskUYA8/HLgIKuKNQ==", - "license": "MIT" - }, - "node_modules/chessify-protocol/node_modules/@stacks/network": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-7.3.1.tgz", - "integrity": "sha512-dQjhcwkz8lihSYSCUMf7OYeEh/Eh0++NebDtXbIB3pHWTvNCYEH7sxhYTB1iyunurv31/QEi0RuWdlfXK/BjeA==", - "license": "MIT", - "dependencies": { - "@stacks/common": "^7.3.1", - "cross-fetch": "^3.1.5" - } - }, - "node_modules/chessify-protocol/node_modules/@stacks/transactions": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-7.4.0.tgz", - "integrity": "sha512-scsQO3rSNNKcPHp56Wy5OeZiIpQNmmZOORz8bkQKWjzvzycAodtSWmAoHiMFAKSleR1NyeRIz642fReqlZU9tw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.1.5", - "@noble/secp256k1": "1.7.1", - "@stacks/common": "^7.3.1", - "@stacks/network": "^7.3.1", - "c32check": "^2.0.0", - "lodash.clonedeep": "^4.5.0" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -14323,6 +11054,7 @@ "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", @@ -14334,21 +11066,6 @@ "node": ">=8" } }, - "node_modules/cipher-base": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", - "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.2" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -14410,18 +11127,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-table": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", - "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", - "peer": true, - "dependencies": { - "colors": "1.0.3" - }, - "engines": { - "node": ">= 0.2.0" - } - }, "node_modules/cli-table3": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", @@ -14510,21 +11215,15 @@ } }, "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "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": ">= 10" + "node": ">= 12" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -14539,6 +11238,38 @@ "node": ">=12" } }, + "node_modules/cliui/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/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -14562,6 +11293,7 @@ "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", @@ -14578,6 +11310,7 @@ "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": { @@ -14628,16 +11361,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -14650,44 +11373,6 @@ "node": ">= 0.8" } }, - "node_modules/command-line-args": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.2.tgz", - "integrity": "sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==", - "license": "MIT", - "dependencies": { - "array-back": "^6.2.3", - "find-replace": "^5.0.2", - "lodash.camelcase": "^4.3.0", - "typical": "^7.3.0" - }, - "engines": { - "node": ">=12.20" - }, - "peerDependencies": { - "@75lb/nature": "latest" - }, - "peerDependenciesMeta": { - "@75lb/nature": { - "optional": true - } - } - }, - "node_modules/command-line-usage": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.4.tgz", - "integrity": "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==", - "license": "MIT", - "dependencies": { - "array-back": "^6.2.2", - "chalk-template": "^0.4.0", - "table-layout": "^4.1.1", - "typical": "^7.3.0" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -14730,7 +11415,8 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/component-emitter": { "version": "1.3.1", @@ -14746,6 +11432,7 @@ "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": { @@ -14768,6 +11455,7 @@ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", @@ -14792,6 +11480,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -14807,6 +11496,7 @@ "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" @@ -14823,6 +11513,7 @@ "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" }, @@ -14830,21 +11521,6 @@ "node": ">=8" } }, - "node_modules/concurrently/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/connect-redis": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-9.0.0.tgz", @@ -14869,6 +11545,7 @@ "resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz", "integrity": "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==", "dev": true, + "license": "MIT", "dependencies": { "easy-table": "1.1.0" }, @@ -14947,6 +11624,7 @@ "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": { @@ -14958,12 +11636,6 @@ "node": ">= 0.6" } }, - "node_modules/cookie-es": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", - "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", - "license": "MIT" - }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -14981,7 +11653,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -15058,39 +11730,11 @@ "node": ">=0.8" } }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "license": "MIT", - "optional": true, - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "license": "MIT", - "optional": true, - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, "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", @@ -15112,6 +11756,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -15127,6 +11772,7 @@ "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", @@ -15139,6 +11785,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/create-jest/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/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -15175,15 +11834,6 @@ "node": ">=12.0.0" } }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, "node_modules/cross-inspect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", @@ -15210,15 +11860,6 @@ "node": ">= 8" } }, - "node_modules/crossws": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", - "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", - "license": "MIT", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, "node_modules/csrf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", @@ -15239,14 +11880,6 @@ "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", "license": "MIT" }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/csurf": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", @@ -15330,6 +11963,7 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14" } @@ -15363,24 +11997,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -15430,6 +12046,7 @@ "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" @@ -15470,6 +12087,7 @@ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "dev": true, + "license": "MIT", "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -15478,11 +12096,6 @@ "engines": { "node": ">= 14" } - "node_modules/defu": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", - "license": "MIT" }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -15509,13 +12122,7 @@ "license": "MIT", "engines": { "node": ">= 0.8" - } - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "license": "MIT" + } }, "node_modules/destroy": { "version": "1.2.0", @@ -15527,12 +12134,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-browser": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", - "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", - "license": "MIT" - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -15546,17 +12147,12 @@ "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/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -15582,17 +12178,12 @@ "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/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -15658,21 +12249,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/drbg.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", - "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==", - "license": "MIT", - "optional": true, - "dependencies": { - "browserify-aes": "^1.0.6", - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", @@ -15696,18 +12272,6 @@ "node": ">= 0.4" } }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -15719,6 +12283,7 @@ "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz", "integrity": "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==", "dev": true, + "license": "MIT", "optionalDependencies": { "wcwidth": ">=1.0.1" } @@ -15732,21 +12297,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/ecpair": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.1.0.tgz", - "integrity": "sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw==", - "license": "MIT", - "optional": true, - "dependencies": { - "randombytes": "^2.1.0", - "typeforce": "^1.18.0", - "wif": "^2.0.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -15754,31 +12304,17 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.344", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", - "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, "license": "ISC" }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "license": "MIT", - "optional": true, - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "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" @@ -15793,12 +12329,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -15818,9 +12348,9 @@ } }, "node_modules/engine.io": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz", - "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==", + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.7.tgz", + "integrity": "sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ==", "license": "MIT", "dependencies": { "@types/cors": "^2.8.12", @@ -15921,9 +12451,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", - "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", + "integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15961,6 +12491,7 @@ "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" @@ -15985,9 +12516,9 @@ } }, "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT", "peer": true @@ -16019,16 +12550,6 @@ "node": ">= 0.4" } }, - "node_modules/es-toolkit": { - "version": "1.39.3", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.3.tgz", - "integrity": "sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -16062,6 +12583,7 @@ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -16083,6 +12605,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" @@ -16389,6 +12912,19 @@ "node": ">=8" } }, + "node_modules/eslint/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/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -16411,6 +12947,7 @@ "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", @@ -16492,6 +13029,7 @@ "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" @@ -16506,21 +13044,11 @@ "bare-events": "^2.7.0" } }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "license": "MIT", - "optional": true, - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, "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", @@ -16544,12 +13072,14 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/exit": { "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" } @@ -16567,6 +13097,7 @@ "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", @@ -16826,6 +13357,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -16835,32 +13367,16 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "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-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense", - "optional": true - }, "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==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -16875,9 +13391,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", @@ -16886,22 +13402,24 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.1.3" + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, "node_modules/fast-xml-parser": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", - "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], + "license": "MIT", "dependencies": { "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.5", + "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, @@ -16922,6 +13440,7 @@ "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" @@ -17007,13 +13526,6 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT", - "optional": true - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -17026,15 +13538,6 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -17056,23 +13559,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/find-replace": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", - "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@75lb/nature": "latest" - }, - "peerDependenciesMeta": { - "@75lb/nature": { - "optional": true - } - } - }, "node_modules/find-up": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", @@ -17117,10 +13603,10 @@ } }, "node_modules/flatbuffers": { - "version": "25.9.23", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", - "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", - "license": "Apache-2.0" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", + "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==", + "license": "SEE LICENSE IN LICENSE.txt" }, "node_modules/flatted": { "version": "3.4.2", @@ -17313,6 +13799,21 @@ } } }, + "node_modules/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -17326,6 +13827,19 @@ "node": "*" } }, + "node_modules/fork-ts-checker-webpack-plugin/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/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -17390,33 +13904,6 @@ "node": ">= 0.6" } }, - "node_modules/framer-motion": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", - "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.38.0", - "motion-utils": "^12.36.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -17433,9 +13920,9 @@ "license": "MIT" }, "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==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -17444,7 +13931,7 @@ "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs-monkey": { @@ -17458,12 +13945,14 @@ "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, @@ -17496,6 +13985,7 @@ "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" @@ -17511,9 +14001,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", "engines": { @@ -17547,19 +14037,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "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" @@ -17594,6 +14076,7 @@ "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" @@ -17607,6 +14090,7 @@ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", "dev": true, + "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -17747,6 +14231,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -17763,6 +14260,7 @@ "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/graphemer": { @@ -17773,9 +14271,9 @@ "license": "MIT" }, "node_modules/graphql": { - "version": "16.13.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", - "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz", + "integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==", "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -17826,23 +14324,6 @@ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", "license": "ISC" }, - "node_modules/h3": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", - "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", - "license": "MIT", - "dependencies": { - "cookie-es": "^1.2.3", - "crossws": "^0.3.5", - "defu": "^6.1.6", - "destr": "^2.0.5", - "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.4", - "radix3": "^1.1.2", - "ufo": "^1.6.3", - "uncrypto": "^0.1.3" - } - }, "node_modules/handlebars": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", @@ -17931,80 +14412,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hash-base": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", - "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/hash-base/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT", - "optional": true - }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "optional": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT", - "optional": true - }, - "node_modules/hash-base/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT", - "optional": true - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, "node_modules/hashery": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", @@ -18034,20 +14441,8 @@ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "license": "MIT", - "optional": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" + "engines": { + "node": ">=18.0.0" } }, "node_modules/hookified": { @@ -18069,6 +14464,7 @@ "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": { @@ -18096,6 +14492,7 @@ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -18109,6 +14506,7 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -18121,6 +14519,7 @@ "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" @@ -18154,12 +14553,6 @@ "node": ">=0.10.0" } }, - "node_modules/idb-keyval": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", - "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0" - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -18233,6 +14626,7 @@ "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", @@ -18263,6 +14657,7 @@ "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" @@ -18273,6 +14668,7 @@ "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", @@ -18355,16 +14751,41 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/inquirer/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==", + "node_modules/inquirer/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/inquirer/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inquirer/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/inquirer/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": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=8" @@ -18395,10 +14816,11 @@ } }, "node_modules/ip-address": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.1.tgz", - "integrity": "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } @@ -18412,19 +14834,11 @@ "node": ">= 0.10" } }, - "node_modules/iron-webcrypto": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", - "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/brc-dd" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, "node_modules/is-binary-path": { @@ -18453,12 +14867,12 @@ } }, "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==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -18493,6 +14907,7 @@ "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" @@ -18559,6 +14974,7 @@ "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" @@ -18620,25 +15036,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/isows": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", - "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, "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" @@ -18648,6 +15050,7 @@ "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", @@ -18664,6 +15067,7 @@ "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", @@ -18674,10 +15078,24 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-report/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/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", @@ -18692,6 +15110,7 @@ "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" @@ -18701,6 +15120,7 @@ "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", @@ -18744,6 +15164,7 @@ "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", @@ -18770,6 +15191,7 @@ "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", @@ -18784,6 +15206,7 @@ "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", @@ -18815,6 +15238,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -18830,6 +15254,7 @@ "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", @@ -18842,10 +15267,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-circus/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/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", @@ -18879,6 +15318,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -18894,6 +15334,7 @@ "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", @@ -18906,10 +15347,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-cli/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/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", @@ -18955,6 +15410,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -18970,12 +15426,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/jest-config/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", @@ -18986,6 +15444,7 @@ "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", @@ -19003,6 +15462,7 @@ "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", @@ -19023,6 +15483,7 @@ "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" @@ -19031,10 +15492,24 @@ "node": "*" } }, + "node_modules/jest-config/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/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", @@ -19050,6 +15525,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19065,6 +15541,7 @@ "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", @@ -19077,10 +15554,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-diff/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/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" @@ -19093,6 +15584,7 @@ "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", @@ -19109,6 +15601,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19124,6 +15617,7 @@ "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", @@ -19136,10 +15630,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-each/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/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", @@ -19157,6 +15665,7 @@ "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" @@ -19166,6 +15675,7 @@ "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", @@ -19191,6 +15701,7 @@ "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", @@ -19204,6 +15715,7 @@ "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", @@ -19219,6 +15731,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19234,6 +15747,7 @@ "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", @@ -19246,10 +15760,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-matcher-utils/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/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", @@ -19270,6 +15798,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19285,6 +15814,7 @@ "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", @@ -19297,10 +15827,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-message-util/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/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", @@ -19315,6 +15859,7 @@ "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" @@ -19332,6 +15877,7 @@ "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" @@ -19341,6 +15887,7 @@ "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", @@ -19361,6 +15908,7 @@ "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", @@ -19374,6 +15922,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19389,6 +15938,7 @@ "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", @@ -19401,10 +15951,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-resolve/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/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", @@ -19437,6 +16001,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19452,6 +16017,7 @@ "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", @@ -19468,6 +16034,7 @@ "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" @@ -19477,16 +16044,31 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, + "node_modules/jest-runner/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/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", @@ -19520,6 +16102,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19535,12 +16118,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/jest-runtime/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", @@ -19551,6 +16136,7 @@ "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", @@ -19568,6 +16154,7 @@ "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", @@ -19588,18 +16175,33 @@ "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" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-runtime/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": "*" + "node": ">=8" } }, "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", @@ -19631,6 +16233,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19646,6 +16249,7 @@ "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", @@ -19658,10 +16262,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-snapshot/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/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", @@ -19679,6 +16297,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19694,6 +16313,7 @@ "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", @@ -19710,6 +16330,7 @@ "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" @@ -19718,10 +16339,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/jest-util/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/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", @@ -19739,6 +16374,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19754,6 +16390,7 @@ "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", @@ -19766,10 +16403,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-validate/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/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", @@ -19789,6 +16440,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19804,6 +16456,7 @@ "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", @@ -19816,10 +16469,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-watcher/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/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": "*", @@ -19831,21 +16498,6 @@ "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==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -19857,9 +16509,9 @@ } }, "node_modules/joi": { - "version": "18.1.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.1.2.tgz", - "integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", + "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==", "license": "BSD-3-Clause", "dependencies": { "@hapi/address": "^5.1.1", @@ -19878,6 +16530,7 @@ "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": { @@ -19896,6 +16549,7 @@ "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" @@ -19904,14 +16558,6 @@ "node": ">=6" } }, - "node_modules/json-bignum": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", - "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -19923,6 +16569,7 @@ "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": { @@ -19943,6 +16590,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -19998,17 +16646,6 @@ "node": "*" } }, - "node_modules/jsontokens": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsontokens/-/jsontokens-4.0.1.tgz", - "integrity": "sha512-+MO415LEN6M+3FGsRz4wU20g7N2JA+2j9d9+pGaNJHviG4L8N0qzavGyENw6fJqsq9CcrHOIL6iWX5yeTZ86+Q==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.2", - "@noble/secp256k1": "^1.6.3", - "base64-js": "^1.5.1" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -20061,16 +16698,11 @@ "@keyv/serialize": "^1.1.1" } }, - "node_modules/keyvaluestorage-interface": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", - "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", - "license": "MIT" - }, "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" @@ -20080,6 +16712,7 @@ "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" @@ -20100,9 +16733,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.42", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.42.tgz", - "integrity": "sha512-oKQFPTibqQwZZkChCDVMFVJXMZdyJNqDWZWYNn8BgyAaK/6yFJEowxCY0RVFirRyWP63hMRuKlkSEd9qlvbWXg==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.0.tgz", + "integrity": "sha512-N12qmdu0BM1wVNkMKYOoJR4fTOZDblrKNsOqGbKoUZrYsYLX2zx1O5X+vhK0WJPBU/+/kh9tCr8x0a7t1puGWg==", "license": "MIT" }, "node_modules/lilconfig": { @@ -20122,6 +16755,7 @@ "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/lint-staged": { @@ -20411,43 +17045,15 @@ "url": "https://buymeacoffee.com/borewit" } ], + "license": "MIT", "engines": { "node": ">=13.2.0" - "node_modules/lit": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", - "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", - "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", - "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" } }, "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==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "dev": true, "license": "MIT", "engines": { @@ -20486,12 +17092,6 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "license": "MIT" }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "license": "MIT" - }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -20665,6 +17265,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/log-symbols/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/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -20922,6 +17535,7 @@ "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" @@ -20944,6 +17558,7 @@ "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" @@ -20964,18 +17579,6 @@ "node": ">= 0.4" } }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "license": "MIT", - "optional": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -21036,6 +17639,7 @@ "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/merge2": { @@ -21082,16 +17686,19 @@ } }, "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], "license": "MIT", "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4.0.0" + "node": ">=16" } }, "node_modules/mime-db": { @@ -21123,6 +17730,7 @@ "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" @@ -21153,20 +17761,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC", - "optional": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "license": "MIT", - "optional": true - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -21201,26 +17795,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mipd": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz", - "integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wagmi-dev" - } - ], - "license": "MIT", - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -21245,21 +17819,6 @@ "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, - "node_modules/motion-dom": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", - "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.36.0" - } - }, - "node_modules/motion-utils": { - "version": "12.36.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", - "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -21267,9 +17826,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", - "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", + "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", "license": "MIT", "optionalDependencies": { "msgpackr-extract": "^3.0.2" @@ -21316,12 +17875,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, "node_modules/murmurhash-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", @@ -21329,35 +17882,13 @@ "license": "MIT" }, "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, - "license": "ISC" - }, - "node_modules/nan": { - "version": "2.26.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", - "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", - "license": "MIT", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, + "license": "ISC", "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/napi-build-utils": { @@ -21370,6 +17901,7 @@ "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/neo-async": { @@ -21383,84 +17915,15 @@ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", "dev": true, - "engines": { - "node": ">= 0.4.0" - "node_modules/next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", - "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", "license": "MIT", - "dependencies": { - "@next/env": "16.2.1", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.9.19", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.1", - "@next/swc-darwin-x64": "16.2.1", - "@next/swc-linux-arm64-gnu": "16.2.1", - "@next/swc-linux-arm64-musl": "16.2.1", - "@next/swc-linux-x64-gnu": "16.2.1", - "@next/swc-linux-x64-musl": "16.2.1", - "@next/swc-win32-arm64-msvc": "16.2.1", - "@next/swc-win32-x64-msvc": "16.2.1", - "sharp": "^0.34.5" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next-themes": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", - "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/next/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" + "node": ">= 0.4.0" } }, "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -21520,12 +17983,6 @@ } } }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "license": "MIT" - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -21556,18 +18013,14 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "license": "MIT" - }, - "node_modules/node-mock-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", - "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "dev": true, "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, "license": "MIT" }, "node_modules/nodemailer": { @@ -21592,6 +18045,7 @@ "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" @@ -21630,23 +18084,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ofetch": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", - "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", - "license": "MIT", - "dependencies": { - "destr": "^2.0.5", - "node-fetch-native": "^1.6.7", - "ufo": "^1.6.1" - } - }, - "node_modules/on-exit-leak-free": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", - "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", - "license": "MIT" - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -21681,6 +18118,7 @@ "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" @@ -21702,9 +18140,9 @@ } }, "node_modules/onnx-proto/node_modules/protobufjs": { - "version": "6.11.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.5.tgz", - "integrity": "sha512-OKjVH3hDoXdIZ/s5MLv8O2X0s+wOxGfV7ar6WFSKGaSAxi/6gYn3px5POS4vi+mc/0zCOdL7Jkwrj0oT1Yst2A==", + "version": "6.11.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.6.tgz", + "integrity": "sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -21762,12 +18200,6 @@ "platform": "^1.3.6" } }, - "node_modules/onnxruntime-web/node_modules/flatbuffers": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", - "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==", - "license": "SEE LICENSE IN LICENSE.txt" - }, "node_modules/opossum": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/opossum/-/opossum-9.0.0.tgz", @@ -21852,98 +18284,34 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "node_modules/ora/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", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ox": { - "version": "0.14.20", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.20.tgz", - "integrity": "sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.11.0", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "1.9.1", - "@noble/hashes": "^1.8.0", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "abitype": "^1.2.3", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ox/node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "has-flag": "^4.0.0" }, "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ox/node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=8" } }, - "node_modules/ox/node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ox/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, "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" @@ -22004,6 +18372,7 @@ "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" @@ -22014,6 +18383,7 @@ "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", "dev": true, + "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", @@ -22033,6 +18403,7 @@ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, + "license": "MIT", "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -22064,6 +18435,7 @@ "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", @@ -22158,6 +18530,7 @@ "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" @@ -22330,6 +18703,7 @@ "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": { @@ -22358,48 +18732,11 @@ "node": ">=0.10" } }, - "node_modules/pino": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", - "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.0.0", - "on-exit-leak-free": "^0.2.0", - "pino-abstract-transport": "v0.5.0", - "pino-std-serializers": "^4.0.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.1.0", - "safe-stable-stringify": "^2.1.0", - "sonic-boom": "^2.2.1", - "thread-stream": "^0.15.1" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", - "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.2", - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", - "license": "MIT" - }, "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" @@ -22409,6 +18746,7 @@ "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" @@ -22421,6 +18759,7 @@ "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", @@ -22434,6 +18773,7 @@ "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" @@ -22446,6 +18786,7 @@ "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" @@ -22461,6 +18802,7 @@ "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" @@ -22473,6 +18815,7 @@ "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" @@ -22494,15 +18837,6 @@ "node": ">=4" } }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, "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", @@ -22512,34 +18846,6 @@ "node": ">= 0.4" } }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -22677,6 +18983,7 @@ "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", @@ -22687,19 +18994,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT", - "optional": true - }, - "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", - "license": "MIT" - }, "node_modules/prom-client": { "version": "15.1.3", "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", @@ -22717,6 +19011,7 @@ "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", @@ -22727,22 +19022,22 @@ } }, "node_modules/protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.7.tgz", + "integrity": "sha512-NGnrxS/nLKUo5nkbVQxlC71sB4hdfImdYIbFeSCidxtwATx0AHRPcANSLd0q5Bb2BkoSWo2iisQhGg5/r+ihbA==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" }, @@ -22774,6 +19069,7 @@ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -22792,11 +19088,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - "node_modules/proxy-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", - "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==", + "dev": true, "license": "MIT" }, "node_modules/proxy-from-env": { @@ -22805,214 +19097,46 @@ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "license": "MIT", "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "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==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qrcode/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/qrcode/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qrcode/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/qrcode/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==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/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==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/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==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/qrcode/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==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/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==", - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/qrcode/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==", + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "node_modules/qrcode/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "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", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, "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.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -23028,24 +19152,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -23066,18 +19172,6 @@ ], "license": "MIT" }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/radix3": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", - "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", - "license": "MIT" - }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -23087,16 +19181,6 @@ "node": ">= 0.8" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -23151,157 +19235,13 @@ "node": ">=0.10.0" } }, - "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-chessboard": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/react-chessboard/-/react-chessboard-5.10.0.tgz", - "integrity": "sha512-Y3PgaCVhnDG3IaQfu86OzTSEIEAUtuU5XwmHWnx3tcFOX7lSoAq81ZFX3MBj6y5a6FzDMTczMVmkkrV2CzTrIw==", - "license": "MIT", - "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/modifiers": "^9.0.0" - }, - "engines": { - "node": ">=20.11.0", - "pnpm": ">=9.4.0" - }, - "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.5" - } - }, "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/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/reacts-cli": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/reacts-cli/-/reacts-cli-1.0.2.tgz", - "integrity": "sha512-hgfVlelkFrmTIuW7Z+od69Gj6AVmIUu2GHyUVUgiH2BFsUCC/N1U8FOwBfFKm4lXx5GSdsqWhv5przGPkgi4mA==", - "dependencies": { - "@jadonamite/chessify-sdk": "^1.0.4", - "@jadonamite/fundxagon-sdk": "^1.0.4", - "@jadonamite/stacks-core": "^1.0.4", - "@stacks/network": "^6.17.0", - "@stacks/transactions": "^6.17.0", - "chessify-protocol": "latest", - "jest": "^29.4.2", - "jest-cli": "^29.4.2", - "typescript": "^5.9.3" - }, - "peerDependencies": { - "@babel/preset-react": "^7.23.3", - "@babel/preset-typescript": "^7.26.0", - "@babel/traverse": "^7.11.0", - "cli-table": "^0.3.1" - } - }, - "node_modules/reacts-cli/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -23342,15 +19282,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/real-require": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", - "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, "node_modules/redis": { "version": "5.12.1", "resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz", @@ -23438,12 +19369,6 @@ "node": ">=8.6.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -23469,6 +19394,7 @@ "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" @@ -23481,6 +19407,7 @@ "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" @@ -23490,6 +19417,7 @@ "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" @@ -23612,28 +19540,6 @@ "node": "*" } }, - "node_modules/ripemd160": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", - "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", - "license": "MIT", - "optional": true, - "dependencies": { - "hash-base": "^3.1.2", - "inherits": "^2.0.4" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ripemd160-min": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/ripemd160-min/-/ripemd160-min-0.0.6.tgz", - "integrity": "sha512-+GcJgQivhs6S9qvLogusiTcS9kQUfgR75whKuy5jIhuiOfQuJ8fjqxV6EGD5duH1Y/FawFUMtMhyeq3Fbnib8A==", - "engines": { - "node": ">=8" - } - }, "node_modules/rndm": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", @@ -23667,9 +19573,9 @@ } }, "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", "dev": true, "license": "MIT", "engines": { @@ -23728,46 +19634,12 @@ ], "license": "MIT" }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true - }, - "node_modules/schema-inspector": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/schema-inspector/-/schema-inspector-2.1.0.tgz", - "integrity": "sha512-3bmQVhbA01/EW8cZin4vIpqlpNU2SIy4BhKCfCgogJ3T/L76dLx3QAE+++4o+dNT33sa+SN9vOJL7iHiHFjiNg==", - "license": "MIT", - "dependencies": { - "async": "~2.6.3" - } - }, - "node_modules/schema-inspector/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.14" - } - }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -23821,29 +19693,6 @@ "dev": true, "license": "MIT" }, - "node_modules/secp256k1": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz", - "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "elliptic": "^6.5.7", - "node-addon-api": "^5.0.0", - "node-gyp-build": "^4.2.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/secp256k1/node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT", - "optional": true - }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -23861,9 +19710,9 @@ "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -23917,12 +19766,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -24036,6 +19879,7 @@ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -24191,12 +20035,14 @@ "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" @@ -24237,6 +20083,7 @@ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -24348,10 +20195,11 @@ } }, "node_modules/socks": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", - "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", "dev": true, + "license": "MIT", "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" @@ -24366,6 +20214,7 @@ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -24373,13 +20222,6 @@ }, "engines": { "node": ">= 14" - "node_modules/sonic-boom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", - "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" } }, "node_modules/source-map": { @@ -24392,15 +20234,6 @@ "node": ">= 8" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -24422,15 +20255,6 @@ "node": ">=0.10.0" } }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -24444,6 +20268,7 @@ "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": { @@ -24466,6 +20291,7 @@ "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" @@ -24478,6 +20304,7 @@ "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" @@ -24498,12 +20325,6 @@ "node": ">= 0.8" } }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT" - }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -24517,19 +20338,10 @@ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" } }, "node_modules/string_decoder": { @@ -24555,6 +20367,7 @@ "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", @@ -24640,6 +20453,7 @@ "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" @@ -24649,6 +20463,7 @@ "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" @@ -24658,6 +20473,7 @@ "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" @@ -24687,9 +20503,9 @@ } }, "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -24714,29 +20530,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, "node_modules/subscriptions-transport-ws": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz", @@ -24811,6 +20604,19 @@ "node": ">=14.18.0" } }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/supertest": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", @@ -24837,15 +20643,19 @@ } }, "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==", + "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": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -24907,31 +20717,6 @@ "url": "https://opencollective.com/synckit" } }, - "node_modules/table-layout": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", - "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", - "license": "MIT", - "dependencies": { - "array-back": "^6.2.2", - "wordwrapjs": "^5.1.0" - }, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", @@ -24961,9 +20746,9 @@ } }, "node_modules/tar-stream": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", - "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", "license": "MIT", "dependencies": { "b4a": "^1.6.4", @@ -25000,9 +20785,9 @@ } }, "node_modules/terser": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", - "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz", + "integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -25019,9 +20804,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", "dev": true, "license": "MIT", "dependencies": { @@ -25041,12 +20826,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -25087,22 +20899,6 @@ "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", @@ -25114,6 +20910,7 @@ "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", @@ -25128,12 +20925,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/test-exclude/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", @@ -25145,6 +20944,7 @@ "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", @@ -25165,6 +20965,7 @@ "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" @@ -25202,15 +21003,6 @@ "dev": true, "license": "MIT" }, - "node_modules/thread-stream": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", - "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", - "license": "MIT", - "dependencies": { - "real-require": "^0.1.0" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -25219,9 +21011,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, "license": "MIT", "engines": { @@ -25273,6 +21065,7 @@ "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": { @@ -25477,6 +21270,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/ts-loader/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/ts-morph": { "version": "24.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-24.0.0.tgz", @@ -25595,6 +21401,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/tsconfig-paths-webpack-plugin/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/tsconfig-paths/node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -25632,13 +21451,6 @@ "node": "*" } }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense", - "optional": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -25656,15 +21468,17 @@ "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.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "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" @@ -25727,33 +21541,26 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, - "node_modules/typeforce": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==", - "license": "MIT", - "optional": true - }, "node_modules/typeorm": { - "version": "0.3.28", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", - "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.29.tgz", + "integrity": "sha512-wwPEX/df4l72gCmOsrs0otJZYLGA9lLQkUZCkukbsymEycV4zXv2KM7wU7v2r8L01TaCgY9ApSSqHQWBOUhEoQ==", "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", + "dayjs": "^1.11.20", "debug": "^4.4.3", - "dedent": "^1.7.0", + "dedent": "^1.7.2", "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", + "uuid": "^11.1.1", "yargs": "^17.7.2" }, "bin": { @@ -25924,9 +21731,9 @@ } }, "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==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -25937,9 +21744,9 @@ } }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -25950,21 +21757,6 @@ "node": ">=14.17" } }, - "node_modules/typical": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", - "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "license": "MIT" - }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -26014,205 +21806,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/uint8arrays": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", - "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", - "license": "MIT", - "dependencies": { - "multiformats": "^9.4.2" - } - }, - "node_modules/uncrypto": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", - "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", - "license": "MIT" - }, "node_modules/undici": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^5.0.0", - "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.5.1", - "ufo": "^1.6.3" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6 || ^7 || ^8", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1 || ^2 || ^3", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { - "optional": true - } + "engines": { + "node": ">=20.18.1" } }, - "node_modules/unstorage/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, "engines": { - "node": ">= 20.19.0" + "node": ">=18" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/unstorage/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", - "license": "BlueOak-1.0.0", + "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": "20 || >=22" + "node": ">= 10.0.0" } }, - "node_modules/unstorage/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "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": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "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", @@ -26249,58 +21894,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", - "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -26340,6 +21933,7 @@ "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", @@ -26359,30 +21953,6 @@ "node": ">= 0.10" } }, - "node_modules/valtio": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.5.tgz", - "integrity": "sha512-vsh1Ixu5mT0pJFZm+Jspvhga5GzHUTYv0/+Th203pLfh3/wbHwxhu/Z2OkZDXIgHfjnjBns7SN9HNcbDvPmaGw==", - "license": "MIT", - "dependencies": { - "proxy-compare": "^3.0.1" - }, - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "react": ">=18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, "node_modules/value-or-promise": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", @@ -26392,15 +21962,6 @@ "node": ">=12" } }, - "node_modules/varuint-bitcoin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", - "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.1" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -26410,128 +21971,11 @@ "node": ">= 0.8" } }, - "node_modules/viem": { - "version": "2.48.4", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.48.4.tgz", - "integrity": "sha512-mReP/rgY2P+WeeRSG4sUvccCLKfyAW1C73Y3KkobAqgzYmVna9qyUMNE44xIUkDtfvRuC33r24UhF4baBYovsg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.9.1", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0", - "abitype": "1.2.3", - "isows": "1.0.7", - "ox": "0.14.20", - "ws": "8.18.3" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/wagmi": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-3.6.5.tgz", - "integrity": "sha512-TBN/h26CX/FQROEk4zXCtRXGfL2erBEZ9BAbfRpn+sujMtQAoDzGM7LFAr4ODCiDcRAqJcMQWGJvk25DMEnFaQ==", - "license": "MIT", - "dependencies": { - "@wagmi/connectors": "8.0.5", - "@wagmi/core": "3.4.6", - "use-sync-external-store": "1.4.0" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@tanstack/react-query": ">=5.0.0", - "react": ">=18", - "typescript": ">=5.7.3", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "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" @@ -26627,9 +22071,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.0.tgz", - "integrity": "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz", + "integrity": "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==", "dev": true, "license": "MIT", "engines": { @@ -26717,12 +22161,6 @@ "node": ">= 8" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", @@ -26756,48 +22194,6 @@ "node": ">=8" } }, - "node_modules/wif": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", - "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "bs58check": "<3.0.0" - } - }, - "node_modules/wif/node_modules/base-x": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", - "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/wif/node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", - "license": "MIT", - "optional": true, - "dependencies": { - "base-x": "^3.0.2" - } - }, - "node_modules/wif/node_modules/bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", - "license": "MIT", - "optional": true, - "dependencies": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -26814,19 +22210,11 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, - "node_modules/wordwrapjs": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz", - "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, "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==", + "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", @@ -26834,10 +22222,7 @@ "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/wrap-ansi-cjs": { @@ -26877,6 +22262,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -26898,6 +22284,7 @@ "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", @@ -26911,6 +22298,7 @@ "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/ws": { @@ -26934,6 +22322,21 @@ } } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xss": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", @@ -26978,12 +22381,13 @@ "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/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", "dev": true, "license": "ISC", "bin": { @@ -27037,6 +22441,7 @@ "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" @@ -27044,53 +22449,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zone-file": { - "version": "2.0.0-beta.3", - "resolved": "https://registry.npmjs.org/zone-file/-/zone-file-2.0.0-beta.3.tgz", - "integrity": "sha512-6tE3PSRcpN5lbTTLlkLez40WkNPc9vw/u1J2j6DBiy0jcVX48nCkWrx2EC+bWHqC2SLp069Xw4AdnYn/qp/W5g==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/zustand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", - "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index 8ddb6446..9048172c 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,14 @@ "license": "UNLICENSED", "scripts": { "license:scan": "node scripts/scan-licenses.js", - "build": "nest build", + "build": "tsc -p tsconfig.build.json", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node dist/main.js", + "start:simple": "node simple-server.js", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint:ci": "eslint \"{src,apps,libs,test}/**/*.ts\" --max-warnings 0", "lint:typed": "eslint \"{src,apps,libs,test}/**/*.ts\" --parser-options=project:tsconfig.json --max-warnings 0", @@ -72,15 +73,15 @@ "@nestjs/axios": "^4.0.1", "@nestjs/bull": "^11.0.2", "@nestjs/cache-manager": "^3.0.1", - "@nestjs/common": "^10.4.22", + "@nestjs/common": "10.4.22", "@nestjs/config": "^4.0.3", - "@nestjs/core": "^10.4.22", + "@nestjs/core": "10.4.22", "@nestjs/elasticsearch": "^11.1.0", "@nestjs/event-emitter": "^3.1.0", "@nestjs/graphql": "^12.2.2", "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", - "@nestjs/platform-express": "^10.4.18", + "@nestjs/platform-express": "10.4.22", "@nestjs/platform-socket.io": "^10.4.22", "@nestjs/schedule": "^6.1.0", "@nestjs/swagger": "^7.4.2", @@ -131,7 +132,6 @@ "pdf-parse": "^1.1.1", "pg": "^8.17.2", "prom-client": "^15.1.3", - "reacts-cli": "^1.0.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "sharp": "^0.34.3", @@ -148,7 +148,7 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", - "@openapitools/openapi-generator-cli": "^2.32.0", + "@openapitools/openapi-generator-cli": "^2.31.0", "@types/babel__core": "^7.20.5", "@types/babel__generator": "^7.27.0", "@types/babel__template": "^7.4.4", @@ -188,7 +188,7 @@ "ts-loader": "^9.5.7", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", - "typescript": "^6.0.3" + "typescript": "^5.4.5" }, "engines": { "node": ">=18.0.0", diff --git a/simple-server.js b/simple-server.js new file mode 100644 index 00000000..9ffad0f3 --- /dev/null +++ b/simple-server.js @@ -0,0 +1,41 @@ +const express = require('express'); +const app = express(); + +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Basic health check endpoint +app.get('/', (req, res) => { + res.json({ + message: 'TeachLink API is running', + timestamp: new Date().toISOString(), + status: 'OK' + }); +}); + +// Basic API info endpoint +app.get('/api', (req, res) => { + res.json({ + name: 'TeachLink API', + version: '1.0.0', + status: 'Running', + endpoints: ['/', '/api', '/health'] + }); +}); + +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }); +}); + +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log(`🚀 Server is running on port ${PORT}`); + console.log(`📚 API available at http://localhost:${PORT}/api`); + console.log(`🏥 Health check at http://localhost:${PORT}/health`); +}); diff --git a/src/ab-testing/ab-testing.service.ts b/src/ab-testing/ab-testing.service.ts index 1e848256..f2019b54 100644 --- a/src/ab-testing/ab-testing.service.ts +++ b/src/ab-testing/ab-testing.service.ts @@ -41,8 +41,9 @@ export interface ICreateMetricDto { */ @Injectable() export class ABTestingService { - private readonly logger = new Logger(ABTestingService.name); - constructor( + private readonly logger = new Logger(ABTestingService.name); + + constructor( @InjectRepository(Experiment) private experimentRepository: Repository, @InjectRepository(IExperimentVariant) @@ -70,10 +71,8 @@ export class ABTestingService { experiment.exclusionCriteria = createExperimentDto.exclusionCriteria; experiment.status = ExperimentStatus.DRAFT; - // Save the experiment first const savedExperiment = await this.experimentRepository.save(experiment); - // Create variants const variants = createExperimentDto.variants.map((variantDto) => { const variant = new IExperimentVariant(); variant.name = variantDto.name; @@ -90,9 +89,6 @@ export class ABTestingService { return savedExperiment; } - /** - * Gets all experiments - */ async getAllExperiments(): Promise { return await this.experimentRepository.find({ relations: ['variants', 'metrics'], @@ -100,9 +96,6 @@ export class ABTestingService { }); } - /** - * Gets experiment by ID - */ async getExperimentById(id: string): Promise { const experiment = await this.experimentRepository.findOne({ where: { id }, @@ -112,63 +105,29 @@ export class ABTestingService { if (!experiment) { throw new Error(`Experiment with ID ${id} not found`); } - /** - * Gets all experiments - */ - async getAllExperiments(): Promise { - return await this.experimentRepository.find({ - relations: ['variants', 'metrics'], - order: { createdAt: 'DESC' }, - }); + return experiment; + } + + async startExperiment(id: string): Promise { + this.logger.log(`Starting experiment: ${id}`); + const experiment = await this.getExperimentById(id); + if (experiment.status !== ExperimentStatus.DRAFT) { + throw new Error('Only draft experiments can be started'); } - /** - * Gets experiment by ID - */ - async getExperimentById(id: string): Promise { - const experiment = await this.experimentRepository.findOne({ - where: { id }, - relations: ['variants', 'metrics', 'variants.metrics'], - }); - if (!experiment) { - throw new Error(`Experiment with ID ${id} not found`); - } - return experiment; + if (!experiment.variants || experiment.variants.length < 2) { + throw new Error('Experiment must have at least 2 variants'); } - /** - * Starts an experiment - */ - async startExperiment(id: string): Promise { - this.logger.log(`Starting experiment: ${id}`); - const experiment = await this.getExperimentById(id); - if (experiment.status !== ExperimentStatus.DRAFT) { - throw new Error('Only draft experiments can be started'); - } - if (!experiment.variants || experiment.variants.length < 2) { - throw new Error('Experiment must have at least 2 variants'); - } - // Validate that there's exactly one control variant - const controlVariants = experiment.variants.filter((v) => v.isControl); - if (controlVariants.length !== 1) { - throw new Error('Experiment must have exactly one control variant'); - } - experiment.status = ExperimentStatus.RUNNING; - experiment.startDate = new Date(); - const updatedExperiment = await this.experimentRepository.save(experiment); - this.logger.log(`Experiment started: ${updatedExperiment.name}`); - return updatedExperiment; + const controlVariants = experiment.variants.filter((v) => v.isControl); + if (controlVariants.length !== 1) { + throw new Error('Experiment must have exactly one control variant'); } - experiment.status = ExperimentStatus.RUNNING; experiment.startDate = new Date(); - const updatedExperiment = await this.experimentRepository.save(experiment); this.logger.log(`Experiment started: ${updatedExperiment.name}`); return updatedExperiment; } - /** - * Stops an experiment - */ async stopExperiment(id: string): Promise { this.logger.log(`Stopping experiment: ${id}`); @@ -181,62 +140,34 @@ export class ABTestingService { return updatedExperiment; } - /** - * Gets active experiments for a user - */ async getActiveExperimentsForUser(_userId: string): Promise { return await this.experimentRepository.find({ - where: { - status: ExperimentStatus.RUNNING, - startDate: new Date(), - }, + where: { status: ExperimentStatus.RUNNING }, relations: ['variants'], }); } - /** - * Assigns a user to a variant - */ async assignUserToVariant(experimentId: string, userId: string): Promise { const experiment = await this.getExperimentById(experimentId); if (experiment.status !== ExperimentStatus.RUNNING) { throw new Error('Experiment is not running'); } - /** - * Gets active experiments for a user - */ - async getActiveExperimentsForUser(_userId: string): Promise { - return await this.experimentRepository.find({ - where: { - status: ExperimentStatus.RUNNING, - startDate: new Date(), - }, - relations: ['variants'], - }); + if (!experiment.variants?.length) { + throw new Error('Experiment has no variants'); } - /** - * Assigns a user to a variant - */ - async assignUserToVariant(experimentId: string, userId: string): Promise { - const experiment = await this.getExperimentById(experimentId); - if (experiment.status !== ExperimentStatus.RUNNING) { - throw new Error('Experiment is not running'); - } - // Simple hash-based assignment for consistent user-to-variant mapping - const variantIndex = this.hashUserIdToVariant(userId, experiment.variants.length); - return experiment.variants[variantIndex]; - } - /** - * Hashes user ID to determine variant assignment - */ - private hashUserIdToVariant(userId: string, variantCount: number): number { - let hash = 0; - for (let i = 0; i < userId.length; i++) { - const char = userId.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32-bit integer - } - return Math.abs(hash) % variantCount; + + const variantIndex = this.hashUserIdToVariant(userId, experiment.variants.length); + return experiment.variants[variantIndex]; + } + + private hashUserIdToVariant(userId: string, variantCount: number): number { + let hash = 0; + for (let i = 0; i < userId.length; i++) { + const char = userId.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; } + return Math.abs(hash) % variantCount; + } } diff --git a/src/ab-testing/analysis/statistical-analysis.service.ts b/src/ab-testing/analysis/statistical-analysis.service.ts index d824dd0f..e95d521a 100644 --- a/src/ab-testing/analysis/statistical-analysis.service.ts +++ b/src/ab-testing/analysis/statistical-analysis.service.ts @@ -5,25 +5,39 @@ import { Experiment } from '../entities/experiment.entity'; import { IExperimentVariant } from '../entities/experiment-variant.entity'; import { VariantMetric } from '../entities/variant-metric.entity'; +export interface ISignificanceResults { + experimentId: string; + confidenceLevel: number; + variants: unknown[]; + statisticallySignificant: boolean; +} + +export interface IEffectSizeResult { + experimentId: string; + controlVariantId: string; + effectSizes: Array<{ + variantId: string; + variantName: string; + effectSize: number; + interpretation: string; + }>; +} + /** * Provides statistical Analysis operations. */ @Injectable() export class StatisticalAnalysisService { - private readonly logger = new Logger(StatisticalAnalysisService.name); - constructor( + private readonly logger = new Logger(StatisticalAnalysisService.name); + + constructor( @InjectRepository(Experiment) - private experimentRepository: Repository, - @InjectRepository(IExperimentVariant) - private variantRepository: Repository, + private readonly experimentRepository: Repository, @InjectRepository(VariantMetric) - private variantMetricRepository: Repository, + private readonly variantMetricRepository: Repository, ) {} - /** - * Calculates statistical significance for experiment results - */ - async calculateStatisticalSignificance(experimentId: string): Promise { + async calculateStatisticalSignificance(experimentId: string): Promise { this.logger.log(`Calculating statistical significance for experiment: ${experimentId}`); const experiment = await this.experimentRepository.findOne({ @@ -35,20 +49,18 @@ export class StatisticalAnalysisService { throw new Error(`Experiment with ID ${experimentId} not found`); } - const results = { + const results: ISignificanceResults = { experimentId: experiment.id, confidenceLevel: experiment.confidenceLevel, - variants: [] as any[], + variants: [], statisticallySignificant: false, }; - // Calculate statistics for each variant for (const variant of experiment.variants) { const variantAnalysis = await this.analyzeVariant(variant, experiment.confidenceLevel); results.variants.push(variantAnalysis); } - // Check if any variant is statistically significant compared to control const controlVariant = experiment.variants.find((v) => v.isControl); if (controlVariant) { const controlMetrics = await this.getVariantMetrics(controlVariant.id); @@ -62,17 +74,58 @@ export class StatisticalAnalysisService { return results; } - /** - * Analyzes a single variant's metrics - */ - private async analyzeVariant(variant: IExperimentVariant, confidenceLevel: number): Promise { + async calculateEffectSize(experimentId: string): Promise { + this.logger.log(`Calculating effect size for experiment: ${experimentId}`); + + const experiment = await this.experimentRepository.findOne({ + where: { id: experimentId }, + relations: ['variants'], + }); + + if (!experiment) { + throw new Error(`Experiment with ID ${experimentId} not found`); + } + + const controlVariant = experiment.variants.find((v) => v.isControl); + if (!controlVariant) { + throw new Error('No control variant found'); + } + + const effectSizes: IEffectSizeResult['effectSizes'] = []; + + for (const variant of experiment.variants) { + if (variant.id === controlVariant.id) continue; + + const controlMetrics = await this.getVariantMetrics(controlVariant.id); + const variantMetrics = await this.getVariantMetrics(variant.id); + const effectSize = this.calculateCohensD(controlMetrics, variantMetrics); + + effectSizes.push({ + variantId: variant.id, + variantName: variant.name, + effectSize, + interpretation: this.interpretEffectSize(effectSize), + }); + } + + return { + experimentId: experiment.id, + controlVariantId: controlVariant.id, + effectSizes, + }; + } + + private async analyzeVariant( + variant: IExperimentVariant, + confidenceLevel: number, + ): Promise> { const metrics = await this.getVariantMetrics(variant.id); const analysis = { variantId: variant.id, variantName: variant.name, isControl: variant.isControl, - metrics: [] as any[], + metrics: [] as unknown[], overallPerformance: 0, }; @@ -80,42 +133,34 @@ export class StatisticalAnalysisService { const statisticalData = await this.calculateMetricStatistics(metric, confidenceLevel); analysis.metrics.push(statisticalData); - // For overall performance, we'll use conversion rate or value depending on metric type - if (statisticalData.conversionRate) { - analysis.overallPerformance += statisticalData.conversionRate; - } else { - analysis.overallPerformance += statisticalData.value; + const rate = statisticalData.conversionRate; + const val = statisticalData.value as number | undefined; + if (typeof rate === 'number') { + analysis.overallPerformance += rate; + } else if (typeof val === 'number') { + analysis.overallPerformance += val; } } return analysis; } - /** - * Calculates statistics for a specific metric - */ - private async calculateMetricStatistics( - metric: VariantMetric, - confidenceLevel: number, - ): Promise { - // Calculate standard error + private async calculateMetricStatistics(metric: VariantMetric, confidenceLevel: number) { const standardError = - metric.standardDeviation && metric.sampleSize > 0 - ? metric.standardDeviation / Math.sqrt(metric.sampleSize) + metric.standardDeviation != null && metric.sampleSize > 0 + ? Number(metric.standardDeviation) / Math.sqrt(metric.sampleSize) : 0; - // Calculate confidence interval const zScore = this.getZScore(confidenceLevel); const marginOfError = zScore * standardError; - const confidenceIntervalLower = metric.value - marginOfError; - const confidenceIntervalUpper = metric.value + marginOfError; + const metricValue = Number(metric.value); + const confidenceIntervalLower = metricValue - marginOfError; + const confidenceIntervalUpper = metricValue + marginOfError; - // Simple p-value calculation (would be more complex in a real implementation) - const pValue = this.calculatePValue(metric.value, standardError); - - // Determine if statistically significant - const isStatisticallySignificant = pValue < 1 - confidenceLevel / 100; + const pValue = this.calculatePValue(metricValue, standardError); + const alpha = Math.max(0, Math.min(1, 1 - confidenceLevel / 100)); + const isStatisticallySignificant = pValue < alpha; return { metricId: metric.id, @@ -129,18 +174,12 @@ export class StatisticalAnalysisService { }; } - /** - * Gets metrics for a variant - */ private async getVariantMetrics(variantId: string): Promise { return await this.variantMetricRepository.find({ where: { variant: { id: variantId } }, }); } - /** - * Checks if any variant is significantly different from control - */ private async checkSignificanceAgainstControl( variants: IExperimentVariant[], controlMetrics: VariantMetric[], @@ -154,269 +193,84 @@ export class StatisticalAnalysisService { const variantMetrics = await this.getVariantMetrics(variant.id); - // Compare each metric for (let i = 0; i < controlMetrics.length && i < variantMetrics.length; i++) { const controlMetric = controlMetrics[i]; const variantMetric = variantMetrics[i]; - const isSignificant = await this.compareMetrics( controlMetric, variantMetric, confidenceLevel, ); - if (isSignificant) { return true; } - const results = { - experimentId: experiment.id, - confidenceLevel: experiment.confidenceLevel, - variants: [] as unknown[], - statisticallySignificant: false, - }; - // Calculate statistics for each variant - for (const variant of experiment.variants) { - const variantAnalysis = await this.analyzeVariant(variant, experiment.confidenceLevel); - results.variants.push(variantAnalysis); - } - // Check if any variant is statistically significant compared to control - const controlVariant = experiment.variants.find((v) => v.isControl); - if (controlVariant) { - const controlMetrics = await this.getVariantMetrics(controlVariant.id); - results.statisticallySignificant = await this.checkSignificanceAgainstControl(experiment.variants, controlMetrics, experiment.confidenceLevel); - } - return results; + } } return false; } - /** - * Compares two metrics for statistical significance - */ private async compareMetrics( metric1: VariantMetric, metric2: VariantMetric, confidenceLevel: number, ): Promise { - // Calculate pooled standard error for comparison const pooledSE = Math.sqrt( - Math.pow(metric1.standardDeviation || 0, 2) / (metric1.sampleSize || 1) + - Math.pow(metric2.standardDeviation || 0, 2) / (metric2.sampleSize || 1), + Math.pow(Number(metric1.standardDeviation) || 0, 2) / (metric1.sampleSize || 1) + + Math.pow(Number(metric2.standardDeviation) || 0, 2) / (metric2.sampleSize || 1), ); - // Calculate z-score for the difference - const difference = metric2.value - metric1.value; - const zScore = pooledSE > 0 ? Math.abs(difference / pooledSE) : 0; + const difference = Number(metric2.value) - Number(metric1.value); + const z = pooledSE > 0 ? Math.abs(difference / pooledSE) : 0; - // Get critical z-value for the confidence level const criticalZ = this.getZScore(confidenceLevel); - return zScore > criticalZ; + return z > criticalZ; } - /** - * Gets z-score for a given confidence level - */ private getZScore(confidenceLevel: number): number { - // Z-scores for common confidence levels const zScores: Record = { 90: 1.645, 95: 1.96, 99: 2.576, }; - return zScores[confidenceLevel] || 1.96; // Default to 95% confidence + return zScores[confidenceLevel] || 1.96; } - /** - * Calculates p-value (simplified implementation) - */ private calculatePValue(value: number, standardError: number): number { if (standardError === 0) return 1; - // Simplified p-value calculation const zScore = Math.abs(value / standardError); - // This is a very simplified approximation return Math.max(0, Math.min(1, 1 / (1 + Math.exp(-zScore + 2)))); } - /** - * Calculates effect size - */ - async calculateEffectSize(experimentId: string): Promise { - this.logger.log(`Calculating effect size for experiment: ${experimentId}`); + private calculateCohensD(controlMetrics: VariantMetric[], variantMetrics: VariantMetric[]): number { + if (controlMetrics.length === 0 || variantMetrics.length === 0) return 0; - const experiment = await this.experimentRepository.findOne({ - where: { id: experimentId }, - relations: ['variants'], - }); + const controlMean = + controlMetrics.reduce((sum, m) => sum + Number(m.value), 0) / controlMetrics.length; + const variantMean = + variantMetrics.reduce((sum, m) => sum + Number(m.value), 0) / variantMetrics.length; - if (!experiment) { - throw new Error(`Experiment with ID ${experimentId} not found`); - } - /** - * Calculates statistics for a specific metric - */ - private async calculateMetricStatistics(metric: VariantMetric, confidenceLevel: number): Promise { - // Calculate standard error - const standardError = metric.standardDeviation && metric.sampleSize > 0 - ? metric.standardDeviation / Math.sqrt(metric.sampleSize) - : 0; - // Calculate confidence interval - const zScore = this.getZScore(confidenceLevel); - const marginOfError = zScore * standardError; - const confidenceIntervalLower = metric.value - marginOfError; - const confidenceIntervalUpper = metric.value + marginOfError; - // Simple p-value calculation (would be more complex in a real implementation) - const pValue = this.calculatePValue(metric.value, standardError); - // Determine if statistically significant - const isStatisticallySignificant = pValue < 1 - confidenceLevel / 100; - return { - metricId: metric.id, - value: metric.value, - sampleSize: metric.sampleSize, - conversionRate: metric.conversionRate, - standardDeviation: metric.standardDeviation, - confidenceInterval: [confidenceIntervalLower, confidenceIntervalUpper], - pValue, - isStatisticallySignificant, - }; - } - /** - * Gets metrics for a variant - */ - private async getVariantMetrics(variantId: string): Promise { - return await this.variantMetricRepository.find({ - where: { variant: { id: variantId } }, - }); - } - /** - * Checks if any variant is significantly different from control - */ - private async checkSignificanceAgainstControl(variants: ExperimentVariant[], controlMetrics: VariantMetric[], confidenceLevel: number): Promise { - const controlVariant = variants.find((v) => v.isControl); - if (!controlVariant) - return false; - for (const variant of variants) { - if (variant.id === controlVariant.id) - continue; - const variantMetrics = await this.getVariantMetrics(variant.id); - // Compare each metric - for (let i = 0; i < controlMetrics.length && i < variantMetrics.length; i++) { - const controlMetric = controlMetrics[i]; - const variantMetric = variantMetrics[i]; - const isSignificant = await this.compareMetrics(controlMetric, variantMetric, confidenceLevel); - if (isSignificant) { - return true; - } - } - } - return false; - } - /** - * Compares two metrics for statistical significance - */ - private async compareMetrics(metric1: VariantMetric, metric2: VariantMetric, confidenceLevel: number): Promise { - // Calculate pooled standard error for comparison - const pooledSE = Math.sqrt(Math.pow(metric1.standardDeviation || 0, 2) / (metric1.sampleSize || 1) + - Math.pow(metric2.standardDeviation || 0, 2) / (metric2.sampleSize || 1)); - // Calculate z-score for the difference - const difference = metric2.value - metric1.value; - const zScore = pooledSE > 0 ? Math.abs(difference / pooledSE) : 0; - // Get critical z-value for the confidence level - const criticalZ = this.getZScore(confidenceLevel); - return zScore > criticalZ; - } - /** - * Gets z-score for a given confidence level - */ - private getZScore(confidenceLevel: number): number { - const confidence = confidenceLevel / 100; - const _alpha = 1 - confidence; - // Z-scores for common confidence levels - const zScores: Record = { - 90: 1.645, - 95: 1.96, - 99: 2.576, - }; - return zScores[confidenceLevel] || 1.96; // Default to 95% confidence - } - /** - * Calculates p-value (simplified implementation) - */ - private calculatePValue(value: number, standardError: number): number { - if (standardError === 0) - return 1; - // Simplified p-value calculation - const zScore = Math.abs(value / standardError); - // This is a very simplified approximation - return Math.max(0, Math.min(1, 1 / (1 + Math.exp(-zScore + 2)))); - } - /** - * Calculates effect size - */ - async calculateEffectSize(experimentId: string): Promise { - this.logger.log(`Calculating effect size for experiment: ${experimentId}`); - const experiment = await this.experimentRepository.findOne({ - where: { id: experimentId }, - relations: ['variants'], - }); - if (!experiment) { - throw new Error(`Experiment with ID ${experimentId} not found`); - } - const controlVariant = experiment.variants.find((v) => v.isControl); - if (!controlVariant) { - throw new Error('No control variant found'); - } - const effectSizes = []; - for (const variant of experiment.variants) { - if (variant.id === controlVariant.id) - continue; - const controlMetrics = await this.getVariantMetrics(controlVariant.id); - const variantMetrics = await this.getVariantMetrics(variant.id); - const effectSize = await this.calculateCohensD(controlMetrics, variantMetrics); - effectSizes.push({ - variantId: variant.id, - variantName: variant.name, - effectSize, - interpretation: this.interpretEffectSize(effectSize), - }); - } - return { - experimentId: experiment.id, - controlVariantId: controlVariant.id, - effectSizes, - }; - } - /** - * Calculates Cohen's d effect size - */ - private async calculateCohensD(controlMetrics: VariantMetric[], variantMetrics: VariantMetric[]): Promise { - if (controlMetrics.length === 0 || variantMetrics.length === 0) - return 0; - // Simplified Cohen's d calculation - const controlMean = controlMetrics.reduce((sum, m) => sum + m.value, 0) / controlMetrics.length; - const variantMean = variantMetrics.reduce((sum, m) => sum + m.value, 0) / variantMetrics.length; - const controlStdDev = Math.sqrt(controlMetrics.reduce((sum, m) => sum + Math.pow(m.value - controlMean, 2), 0) / - (controlMetrics.length - 1)); - const variantStdDev = Math.sqrt(variantMetrics.reduce((sum, m) => sum + Math.pow(m.value - variantMean, 2), 0) / - (variantMetrics.length - 1)); - const pooledStdDev = Math.sqrt(((controlMetrics.length - 1) * Math.pow(controlStdDev, 2) + - (variantMetrics.length - 1) * Math.pow(variantStdDev, 2)) / - (controlMetrics.length + variantMetrics.length - 2)); - return pooledStdDev > 0 ? Math.abs(variantMean - controlMean) / pooledStdDev : 0; - } - /** - * Interprets effect size magnitude - */ - private interpretEffectSize(effectSize: number): string { - if (effectSize < 0.2) - return 'negligible'; - if (effectSize < 0.5) - return 'small'; - if (effectSize < 0.8) - return 'medium'; - return 'large'; - } + const denomC = Math.max(controlMetrics.length - 1, 1); + const denomV = Math.max(variantMetrics.length - 1, 1); + + const controlVar = + controlMetrics.reduce((sum, m) => sum + Math.pow(Number(m.value) - controlMean, 2), 0) / denomC; + const variantVar = + variantMetrics.reduce((sum, m) => sum + Math.pow(Number(m.value) - variantMean, 2), 0) / denomV; + + const pooledStdDev = Math.sqrt(((controlMetrics.length - 1) * controlVar + (variantMetrics.length - 1) * variantVar) / + Math.max(controlMetrics.length + variantMetrics.length - 2, 1)); + + return pooledStdDev > 0 ? Math.abs(variantMean - controlMean) / pooledStdDev : 0; + } + + private interpretEffectSize(effectSize: number): string { + if (effectSize < 0.2) return 'negligible'; + if (effectSize < 0.5) return 'small'; + if (effectSize < 0.8) return 'medium'; + return 'large'; + } } diff --git a/src/ab-testing/automation/automated-decision.service.ts b/src/ab-testing/automation/automated-decision.service.ts index e216984e..c8bbbabe 100644 --- a/src/ab-testing/automation/automated-decision.service.ts +++ b/src/ab-testing/automation/automated-decision.service.ts @@ -10,7 +10,7 @@ export interface IWinnerSelectionCriteria { confidenceLevel: number; minimumSampleSize: number; effectSizeThreshold: number; - durationThreshold: number; // in days + durationThreshold: number; } /** @@ -18,19 +18,20 @@ export interface IWinnerSelectionCriteria { */ @Injectable() export class AutomatedDecisionService { - private readonly logger = new Logger(AutomatedDecisionService.name); - constructor( + private readonly logger = new Logger(AutomatedDecisionService.name); + + constructor( @InjectRepository(Experiment) - private experimentRepository: Repository, + private readonly experimentRepository: Repository, @InjectRepository(IExperimentVariant) - private variantRepository: Repository, - private statisticalAnalysisService: StatisticalAnalysisService, + private readonly variantRepository: Repository, + private readonly statisticalAnalysisService: StatisticalAnalysisService, ) {} - /** - * Automatically selects winner for an experiment - */ - async autoSelectWinner(experimentId: string, criteria?: IWinnerSelectionCriteria): Promise { + async autoSelectWinner( + experimentId: string, + criteria?: Partial, + ): Promise> { this.logger.log(`Auto-selecting winner for experiment: ${experimentId}`); const experiment = await this.experimentRepository.findOne({ @@ -47,15 +48,15 @@ export class AutomatedDecisionService { } const defaultCriteria: IWinnerSelectionCriteria = { - confidenceLevel: experiment.confidenceLevel || AB_TESTING_CONSTANTS.DEFAULT_CONFIDENCE_LEVEL, - minimumSampleSize: experiment.minimumSampleSize || AB_TESTING_CONSTANTS.MINIMUM_SAMPLE_SIZE, + confidenceLevel: experiment.confidenceLevel ?? AB_TESTING_CONSTANTS.DEFAULT_CONFIDENCE_LEVEL, + minimumSampleSize: + experiment.minimumSampleSize ?? AB_TESTING_CONSTANTS.MINIMUM_SAMPLE_SIZE, effectSizeThreshold: AB_TESTING_CONSTANTS.EFFECT_SIZE_THRESHOLD, durationThreshold: AB_TESTING_CONSTANTS.DURATION_THRESHOLD_DAYS, }; const selectionCriteria = { ...defaultCriteria, ...criteria }; - // Check if experiment meets duration threshold const experimentDuration = this.calculateExperimentDuration(experiment); if (experimentDuration < selectionCriteria.durationThreshold) { return { @@ -65,11 +66,9 @@ export class AutomatedDecisionService { }; } - // Perform statistical analysis const statisticalResults = await this.statisticalAnalysisService.calculateStatisticalSignificance(experimentId); - // Check if results are statistically significant if (!statisticalResults.statisticallySignificant) { return { experimentId: experiment.id, @@ -78,15 +77,12 @@ export class AutomatedDecisionService { }; } - // Find the winning variant const winner = await this.determineWinner(experiment, statisticalResults, selectionCriteria); if (winner) { - // Mark winner winner.isWinner = true; await this.variantRepository.save(winner); - // Update experiment status experiment.status = ExperimentStatus.COMPLETED; experiment.endDate = new Date(); await this.experimentRepository.save(experiment); @@ -99,21 +95,18 @@ export class AutomatedDecisionService { confidenceLevel: statisticalResults.confidenceLevel, effectSize: await this.calculateEffectSizeForWinner(experiment.id, winner.id), }; - } else { - return { - experimentId: experiment.id, - decision: 'no_winner', - reason: 'No clear winner could be determined', - }; } + + return { + experimentId: experiment.id, + decision: 'no_winner', + reason: 'No clear winner could be determined', + }; } - /** - * Determines the winning variant based on analysis results - */ private async determineWinner( experiment: Experiment, - statisticalResults: any, + statisticalResults: { variants: any[] }, criteria: IWinnerSelectionCriteria, ): Promise { const controlVariant = experiment.variants.find((v) => v.isControl); @@ -122,26 +115,27 @@ export class AutomatedDecisionService { let bestVariant: IExperimentVariant | null = null; let bestPerformance = -Infinity; - // Find the variant with the best performance that meets criteria for (const variantAnalysis of statisticalResults.variants) { const variant = experiment.variants.find((v) => v.id === variantAnalysis.variantId); if (!variant || variant.isControl) continue; - // Check minimum sample size - const hasSufficientSample = variantAnalysis.metrics.every( - (metric: any) => metric.sampleSize >= criteria.minimumSampleSize, - ); + const hasSufficientSample = + Array.isArray(variantAnalysis.metrics) && + variantAnalysis.metrics.every( + (metric: { sampleSize?: number }) => + (metric.sampleSize ?? 0) >= criteria.minimumSampleSize, + ); if (!hasSufficientSample) continue; - // Check if statistically significant - const isSignificant = variantAnalysis.metrics.some( - (metric: any) => metric.isStatisticallySignificant, - ); + const isSignificant = + Array.isArray(variantAnalysis.metrics) && + variantAnalysis.metrics.some( + (metric: { isStatisticallySignificant?: boolean }) => metric.isStatisticallySignificant, + ); if (!isSignificant) continue; - // Check effect size const effectSize = await this.calculateEffectSizeForVariant( experiment.id, variant.id, @@ -149,8 +143,10 @@ export class AutomatedDecisionService { ); if (effectSize < criteria.effectSizeThreshold) continue; - // Compare performance (simplified - would be more complex in real implementation) - const performance = variantAnalysis.overallPerformance; + const performance = + typeof variantAnalysis.overallPerformance === 'number' + ? variantAnalysis.overallPerformance + : 0; if (performance > bestPerformance) { bestPerformance = performance; bestVariant = variant; @@ -160,22 +156,14 @@ export class AutomatedDecisionService { return bestVariant; } - /** - * Calculates effect size for a specific variant compared to control - */ private async calculateEffectSizeForVariant( _experimentId: string, _variantId: string, _controlId: string, ): Promise { - // This would use the statistical analysis service to calculate effect size - // For now, returning a placeholder value return 0.25; } - /** - * Calculates effect size for the winning variant - */ private async calculateEffectSizeForWinner( experimentId: string, winnerId: string, @@ -191,9 +179,6 @@ export class AutomatedDecisionService { return await this.calculateEffectSizeForVariant(experimentId, winnerId, controlVariant.id); } - /** - * Calculates experiment duration in days - */ private calculateExperimentDuration(experiment: Experiment): number { const startDate = new Date(experiment.startDate); const endDate = experiment.endDate ? new Date(experiment.endDate) : new Date(); @@ -201,9 +186,6 @@ export class AutomatedDecisionService { return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } - /** - * Checks if an experiment is ready for winner selection - */ async isReadyForWinnerSelection(experimentId: string): Promise { const experiment = await this.experimentRepository.findOne({ where: { id: experimentId }, @@ -214,38 +196,27 @@ export class AutomatedDecisionService { return false; } - // Check if experiment has run for minimum duration const duration = this.calculateExperimentDuration(experiment); - const minimumDuration = AB_TESTING_CONSTANTS.DURATION_THRESHOLD_DAYS; // 7 days minimum - - if (duration < minimumDuration) { + if (duration < AB_TESTING_CONSTANTS.DURATION_THRESHOLD_DAYS) { return false; } - // Check if all variants have sufficient sample size const minimumSampleSize = - experiment.minimumSampleSize || AB_TESTING_CONSTANTS.MINIMUM_SAMPLE_SIZE; + experiment.minimumSampleSize ?? AB_TESTING_CONSTANTS.MINIMUM_SAMPLE_SIZE; for (const variant of experiment.variants) { - const variantSampleSize = variant.metrics?.reduce( - (sum, metric) => sum + (metric.sampleSize || 0), - 0, - ); + const variantSampleSize = + variant.metrics?.reduce((sum, metric) => sum + (metric.sampleSize ?? 0), 0) ?? 0; if (variantSampleSize < minimumSampleSize) { return false; } } - void minimumSampleSize; - return true; } - /** - * Gets automated decision recommendations - */ - async getDecisionRecommendations(experimentId: string): Promise { + async getDecisionRecommendations(experimentId: string): Promise> { this.logger.log(`Getting decision recommendations for experiment: ${experimentId}`); const experiment = await this.experimentRepository.findOne({ @@ -279,276 +250,70 @@ export class AutomatedDecisionService { if (ready) { recommendations.recommendations.push('Experiment is ready for winner selection'); - // Get potential winner const statisticalResults = await this.statisticalAnalysisService.calculateStatisticalSignificance(experimentId); if (statisticalResults.statisticallySignificant) { const winner = await this.determineWinner(experiment, statisticalResults, { confidenceLevel: - experiment.confidenceLevel || AB_TESTING_CONSTANTS.DEFAULT_CONFIDENCE_LEVEL, + experiment.confidenceLevel ?? AB_TESTING_CONSTANTS.DEFAULT_CONFIDENCE_LEVEL, minimumSampleSize: - experiment.minimumSampleSize || AB_TESTING_CONSTANTS.MINIMUM_SAMPLE_SIZE, + experiment.minimumSampleSize ?? AB_TESTING_CONSTANTS.MINIMUM_SAMPLE_SIZE, effectSizeThreshold: AB_TESTING_CONSTANTS.EFFECT_SIZE_THRESHOLD, durationThreshold: AB_TESTING_CONSTANTS.DURATION_THRESHOLD_DAYS, }); - if (!experiment) { - throw new Error(`Experiment with ID ${experimentId} not found`); - } - if (experiment.status !== ExperimentStatus.RUNNING) { - throw new Error('Only running experiments can have winners selected'); - } - const defaultCriteria: WinnerSelectionCriteria = { - confidenceLevel: experiment.confidenceLevel || 95, - minimumSampleSize: experiment.minimumSampleSize || 100, - effectSizeThreshold: 0.1, - durationThreshold: 7, - }; - const selectionCriteria = { ...defaultCriteria, ...criteria }; - // Check if experiment meets duration threshold - const experimentDuration = this.calculateExperimentDuration(experiment); - if (experimentDuration < selectionCriteria.durationThreshold) { - return { - experimentId: experiment.id, - decision: 'no_winner', - reason: `Experiment duration (${experimentDuration} days) below threshold (${selectionCriteria.durationThreshold} days)`, - }; - } - // Perform statistical analysis - const statisticalResults = await this.statisticalAnalysisService.calculateStatisticalSignificance(experimentId); - // Check if results are statistically significant - if (!statisticalResults.statisticallySignificant) { - return { - experimentId: experiment.id, - decision: 'no_winner', - reason: 'No statistically significant results found', - }; - } - // Find the winning variant - const winner = await this.determineWinner(experiment, statisticalResults, selectionCriteria); if (winner) { - // Mark winner - winner.isWinner = true; - await this.variantRepository.save(winner); - // Update experiment status - experiment.status = ExperimentStatus.COMPLETED; - experiment.endDate = new Date(); - await this.experimentRepository.save(experiment); - return { - experimentId: experiment.id, - decision: 'winner_selected', - winnerId: winner.id, - winnerName: winner.name, - confidenceLevel: statisticalResults.confidenceLevel, - effectSize: await this.calculateEffectSizeForWinner(experiment.id, winner.id), - }; - } - else { - return { - experimentId: experiment.id, - decision: 'no_winner', - reason: 'No clear winner could be determined', - }; + recommendations.winnerCandidate = winner.id; + recommendations.recommendations.push(`Variant "${winner.name}" is the recommended winner`); } } } else { - const remainingDays = Math.max(0, AB_TESTING_CONSTANTS.DURATION_THRESHOLD_DAYS - duration); + const remainingDays = Math.max( + 0, + AB_TESTING_CONSTANTS.DURATION_THRESHOLD_DAYS - duration, + ); recommendations.recommendations.push( `Wait ${remainingDays} more days before making decision`, ); } - /** - * Determines the winning variant based on analysis results - */ - private async determineWinner(experiment: Experiment, statisticalResults: unknown, criteria: WinnerSelectionCriteria): Promise { - const controlVariant = experiment.variants.find((v) => v.isControl); - if (!controlVariant) - return null; - let bestVariant: ExperimentVariant | null = null; - let bestPerformance = -Infinity; - // Find the variant with the best performance that meets criteria - for (const variantAnalysis of statisticalResults.variants) { - const variant = experiment.variants.find((v) => v.id === variantAnalysis.variantId); - if (!variant || variant.isControl) - continue; - // Check minimum sample size - const hasSufficientSample = variantAnalysis.metrics.every((metric: unknown) => metric.sampleSize >= criteria.minimumSampleSize); - if (!hasSufficientSample) - continue; - // Check if statistically significant - const isSignificant = variantAnalysis.metrics.some((metric: unknown) => metric.isStatisticallySignificant); - if (!isSignificant) - continue; - // Check effect size - const effectSize = await this.calculateEffectSizeForVariant(experiment.id, variant.id, controlVariant.id); - if (effectSize < criteria.effectSizeThreshold) - continue; - // Compare performance (simplified - would be more complex in real implementation) - const performance = variantAnalysis.overallPerformance; - if (performance > bestPerformance) { - bestPerformance = performance; - bestVariant = variant; - } - } - return bestVariant; - } - /** - * Calculates effect size for a specific variant compared to control - */ - private async calculateEffectSizeForVariant(_experimentId: string, _variantId: string, _controlId: string): Promise { - // This would use the statistical analysis service to calculate effect size - // For now, returning a placeholder value - return 0.25; - } - /** - * Calculates effect size for the winning variant - */ - private async calculateEffectSizeForWinner(experimentId: string, winnerId: string): Promise { - const experiment = await this.experimentRepository.findOne({ - where: { id: experimentId }, - relations: ['variants'], - }); - const controlVariant = experiment?.variants.find((v) => v.isControl); - if (!controlVariant) - return 0; - return await this.calculateEffectSizeForVariant(experimentId, winnerId, controlVariant.id); - } - /** - * Calculates experiment duration in days - */ - private calculateExperimentDuration(experiment: Experiment): number { - const startDate = new Date(experiment.startDate); - const endDate = experiment.endDate ? new Date(experiment.endDate) : new Date(); - const diffTime = Math.abs(endDate.getTime() - startDate.getTime()); - return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - } - /** - * Checks if an experiment is ready for winner selection - */ - async isReadyForWinnerSelection(experimentId: string): Promise { - const experiment = await this.experimentRepository.findOne({ - where: { id: experimentId }, - relations: ['variants'], - }); - if (!experiment || experiment.status !== ExperimentStatus.RUNNING) { - return false; - } - // Check if experiment has run for minimum duration - const duration = this.calculateExperimentDuration(experiment); - const minimumDuration = 7; // 7 days minimum - if (duration < minimumDuration) { - return false; - } - // Check if all variants have sufficient sample size - const _minimumSampleSize = experiment.minimumSampleSize || 100; - for (const _variant of experiment.variants) { - // This would check actual sample sizes from metrics - // For now, we'll assume variants are ready - } - return true; - } - /** - * Gets automated decision recommendations - */ - async getDecisionRecommendations(experimentId: string): Promise { - this.logger.log(`Getting decision recommendations for experiment: ${experimentId}`); - const experiment = await this.experimentRepository.findOne({ - where: { id: experimentId }, - relations: ['variants'], - }); - if (!experiment) { - throw new Error(`Experiment with ID ${experimentId} not found`); - } - const recommendations = { - experimentId: experiment.id, - status: experiment.status, - readyForDecision: false, - recommendations: [] as string[], - winnerCandidate: null as string | null, - }; - if (experiment.status !== ExperimentStatus.RUNNING) { - recommendations.recommendations.push('Experiment is not running'); - return recommendations; - } - const duration = this.calculateExperimentDuration(experiment); - recommendations.recommendations.push(`Experiment has been running for ${duration} days`); - const ready = await this.isReadyForWinnerSelection(experimentId); - recommendations.readyForDecision = ready; - if (ready) { - recommendations.recommendations.push('Experiment is ready for winner selection'); - // Get potential winner - const statisticalResults = await this.statisticalAnalysisService.calculateStatisticalSignificance(experimentId); - if (statisticalResults.statisticallySignificant) { - const winner = await this.determineWinner(experiment, statisticalResults, { - confidenceLevel: experiment.confidenceLevel || 95, - minimumSampleSize: experiment.minimumSampleSize || 100, - effectSizeThreshold: 0.1, - durationThreshold: 7, - }); - if (winner) { - recommendations.winnerCandidate = winner.id; - recommendations.recommendations.push(`Variant "${winner.name}" is the recommended winner`); - } - } - } - else { - const remainingDays = Math.max(0, 7 - duration); - recommendations.recommendations.push(`Wait ${remainingDays} more days before making decision`); - } - return recommendations; - } - /** - * Auto-allocates traffic based on performance - */ - async autoAllocateTraffic(experimentId: string): Promise { - this.logger.log(`Auto-allocating traffic for experiment: ${experimentId}`); - const experiment = await this.experimentRepository.findOne({ - where: { id: experimentId }, - relations: ['variants'], - }); - if (!experiment || !experiment.autoAllocateTraffic) { - return; - } - // This would implement multi-armed bandit algorithm or similar - // For now, we'll implement a simple performance-based allocation - const variants = experiment.variants; - if (variants.length < 2) - return; - // Calculate performance scores for each variant - const performanceScores = await this.calculateVariantPerformanceScores(variants); - // Allocate traffic proportionally to performance scores - const totalScore = performanceScores.reduce((sum, score) => sum + score.score, 0); - for (let i = 0; i < variants.length; i++) { - const variant = variants[i]; - const score = performanceScores[i]; - variant.trafficAllocation = totalScore > 0 ? score.score / totalScore : 1 / variants.length; - await this.variantRepository.save(variant); - } - this.logger.log(`Traffic auto-allocated for experiment: ${experiment.name}`); + + return recommendations; + } + + async autoAllocateTraffic(experimentId: string): Promise { + this.logger.log(`Auto-allocating traffic for experiment: ${experimentId}`); + + const experiment = await this.experimentRepository.findOne({ + where: { id: experimentId }, + relations: ['variants'], + }); + + if (!experiment || !experiment.autoAllocateTraffic) { + return; } - /** - * Calculates performance scores for variants - */ - private async calculateVariantPerformanceScores(variants: ExperimentVariant[]): Promise { - // This would fetch actual performance data - // For now, returning equal scores - return variants.map((variant) => ({ - variantId: variant.id, - score: 1.0, // Placeholder score - })); + + const variants = experiment.variants; + if (variants.length < 2) return; + + const performanceScores = await this.calculateVariantPerformanceScores(variants); + const totalScore = performanceScores.reduce((sum, score) => sum + score.score, 0); + + for (let i = 0; i < variants.length; i++) { + const variant = variants[i]; + const score = performanceScores[i]; + variant.trafficAllocation = + totalScore > 0 ? score.score / totalScore : 1 / variants.length; + await this.variantRepository.save(variant); } this.logger.log(`Traffic auto-allocated for experiment: ${experiment.name}`); } - /** - * Calculates performance scores for variants - */ - private async calculateVariantPerformanceScores(variants: IExperimentVariant[]): Promise { - // This would fetch actual performance data - // For now, returning equal scores + private async calculateVariantPerformanceScores( + variants: IExperimentVariant[], + ): Promise> { return variants.map((variant) => ({ variantId: variant.id, - score: 1.0, // Placeholder score + score: 1.0, })); } } diff --git a/src/ab-testing/experiments/experiment.service.ts b/src/ab-testing/experiments/experiment.service.ts index 7ddc4176..6afb1ed5 100644 --- a/src/ab-testing/experiments/experiment.service.ts +++ b/src/ab-testing/experiments/experiment.service.ts @@ -11,43 +11,33 @@ import { VariantMetric } from '../entities/variant-metric.entity'; */ @Injectable() export class ExperimentService { - private readonly logger = new Logger(ExperimentService.name); - constructor( + private readonly logger = new Logger(ExperimentService.name); + + constructor( @InjectRepository(Experiment) private experimentRepository: Repository, @InjectRepository(IExperimentVariant) private variantRepository: Repository, @InjectRepository(ExperimentMetric) - private experimentMetricRepository: Repository, + private experimentMetricRepository: Repository, @InjectRepository(VariantMetric) - private variantMetricRepository: Repository) { } - /** - * Updates experiment configuration - */ - async updateExperiment(id: string, updateData: Partial): Promise { - this.logger.log(`Updating experiment: ${id}`); - const experiment = await this.experimentRepository.findOne({ - where: { id }, - }); - if (!experiment) { - throw new Error(`Experiment with ID ${id} not found`); - } - Object.assign(experiment, updateData); - const updatedExperiment = await this.experimentRepository.save(experiment); - this.logger.log(`Experiment updated: ${updatedExperiment.name}`); - return updatedExperiment; - } + private variantMetricRepository: Repository, + ) {} + async updateExperiment(id: string, updateData: Partial): Promise { + this.logger.log(`Updating experiment: ${id}`); + const experiment = await this.experimentRepository.findOne({ + where: { id }, + }); + if (!experiment) { + throw new Error(`Experiment with ID ${id} not found`); + } Object.assign(experiment, updateData); const updatedExperiment = await this.experimentRepository.save(experiment); - this.logger.log(`Experiment updated: ${updatedExperiment.name}`); return updatedExperiment; } - /** - * Adds a variant to an experiment - */ async addVariant( experimentId: string, variantData: Partial, @@ -71,18 +61,12 @@ export class ExperimentService { return savedVariant; } - /** - * Removes a variant from an experiment - */ async removeVariant(variantId: string): Promise { this.logger.log(`Removing variant: ${variantId}`); await this.variantRepository.softDelete(variantId); this.logger.log(`Variant removed: ${variantId}`); } - /** - * Updates traffic allocation for variants - */ async updateTrafficAllocation( experimentId: string, allocations: Record, @@ -97,123 +81,103 @@ export class ExperimentService { if (!experiment) { throw new Error(`Experiment with ID ${experimentId} not found`); } - /** - * Updates traffic allocation for variants - */ - async updateTrafficAllocation(experimentId: string, allocations: Record): Promise { - this.logger.log(`Updating traffic allocation for experiment: ${experimentId}`); - const experiment = await this.experimentRepository.findOne({ - where: { id: experimentId }, - relations: ['variants'], - }); - if (!experiment) { - throw new Error(`Experiment with ID ${experimentId} not found`); - } - // Validate that allocations sum to 100% - const totalAllocation = Object.values(allocations).reduce((sum, alloc) => sum + alloc, 0); - if (Math.abs(totalAllocation - 1) > 0.01) { - throw new Error('Traffic allocations must sum to 100%'); - } - // Update each variant's allocation - for (const variant of experiment.variants) { - if (allocations[variant.id] !== undefined) { - variant.trafficAllocation = allocations[variant.id]; - await this.variantRepository.save(variant); - } - } - this.logger.log(`Traffic allocation updated for experiment: ${experiment.name}`); + + const totalAllocation = Object.values(allocations).reduce((sum, alloc) => sum + alloc, 0); + if (Math.abs(totalAllocation - 1) > 0.01) { + throw new Error('Traffic allocations must sum to 1 (e.g. 0.5 + 0.5)'); + } + + for (const variant of experiment.variants) { + if (allocations[variant.id] !== undefined) { + variant.trafficAllocation = allocations[variant.id]; + await this.variantRepository.save(variant); + } + } + this.logger.log(`Traffic allocation updated for experiment: ${experiment.name}`); + } + + async getExperimentResults(experimentId: string): Promise { + this.logger.log(`Getting results for experiment: ${experimentId}`); + const experiment = await this.experimentRepository.findOne({ + where: { id: experimentId }, + relations: ['variants', 'metrics', 'variants.metrics'], + }); + if (!experiment) { + throw new Error(`Experiment with ID ${experimentId} not found`); + } + + const results = { + experiment: { + id: experiment.id, + name: experiment.name, + status: experiment.status, + type: experiment.type, + }, + variants: experiment.variants.map((variant) => ({ + id: variant.id, + name: variant.name, + isControl: variant.isControl, + isWinner: variant.isWinner, + trafficAllocation: variant.trafficAllocation, + metrics: variant.metrics.map((metric) => ({ + id: metric.id, + value: metric.value, + sampleSize: metric.sampleSize, + conversionRate: metric.conversionRate, + confidenceInterval: [metric.confidenceIntervalLower, metric.confidenceIntervalUpper], + pValue: metric.pValue, + isStatisticallySignificant: metric.isStatisticallySignificant, + })), + })), + }; + return results; + } + + async archiveExperiment(id: string): Promise { + this.logger.log(`Archiving experiment: ${id}`); + const experiment = await this.experimentRepository.findOne({ + where: { id }, + }); + if (!experiment) { + throw new Error(`Experiment with ID ${id} not found`); } - /** - * Gets experiment results - */ - async getExperimentResults(experimentId: string): Promise { - this.logger.log(`Getting results for experiment: ${experimentId}`); - const experiment = await this.experimentRepository.findOne({ - where: { id: experimentId }, - relations: ['variants', 'metrics', 'variants.metrics'], - }); - if (!experiment) { - throw new Error(`Experiment with ID ${experimentId} not found`); - } - // Calculate results for each variant - const results = { - experiment: { - id: experiment.id, - name: experiment.name, - status: experiment.status, - type: experiment.type, - }, - variants: experiment.variants.map((variant) => ({ - id: variant.id, - name: variant.name, - isControl: variant.isControl, - isWinner: variant.isWinner, - trafficAllocation: variant.trafficAllocation, - metrics: variant.metrics.map((metric) => ({ - id: metric.id, - value: metric.value, - sampleSize: metric.sampleSize, - conversionRate: metric.conversionRate, - confidenceInterval: [metric.confidenceIntervalLower, metric.confidenceIntervalUpper], - pValue: metric.pValue, - isStatisticallySignificant: metric.isStatisticallySignificant, - })), - })), - }; - return results; + experiment.status = ExperimentStatus.ARCHIVED; + const archivedExperiment = await this.experimentRepository.save(experiment); + this.logger.log(`Experiment archived: ${archivedExperiment.name}`); + return archivedExperiment; + } + + async pauseExperiment(id: string): Promise { + this.logger.log(`Pausing experiment: ${id}`); + const experiment = await this.experimentRepository.findOne({ + where: { id }, + }); + if (!experiment) { + throw new Error(`Experiment with ID ${id} not found`); } - /** - * Archives an experiment - */ - async archiveExperiment(id: string): Promise { - this.logger.log(`Archiving experiment: ${id}`); - const experiment = await this.experimentRepository.findOne({ - where: { id }, - }); - if (!experiment) { - throw new Error(`Experiment with ID ${id} not found`); - } - experiment.status = ExperimentStatus.ARCHIVED; - const archivedExperiment = await this.experimentRepository.save(experiment); - this.logger.log(`Experiment archived: ${archivedExperiment.name}`); - return archivedExperiment; + if (experiment.status !== ExperimentStatus.RUNNING) { + throw new Error('Only running experiments can be paused'); } - /** - * Pauses an experiment - */ - async pauseExperiment(id: string): Promise { - this.logger.log(`Pausing experiment: ${id}`); - const experiment = await this.experimentRepository.findOne({ - where: { id }, - }); - if (!experiment) { - throw new Error(`Experiment with ID ${id} not found`); - } - if (experiment.status !== ExperimentStatus.RUNNING) { - throw new Error('Only running experiments can be paused'); - } - experiment.status = ExperimentStatus.PAUSED; - const pausedExperiment = await this.experimentRepository.save(experiment); - this.logger.log(`Experiment paused: ${pausedExperiment.name}`); - return pausedExperiment; + experiment.status = ExperimentStatus.PAUSED; + const pausedExperiment = await this.experimentRepository.save(experiment); + this.logger.log(`Experiment paused: ${pausedExperiment.name}`); + return pausedExperiment; + } + + async resumeExperiment(id: string): Promise { + this.logger.log(`Resuming experiment: ${id}`); + const experiment = await this.experimentRepository.findOne({ + where: { id }, + }); + if (!experiment) { + throw new Error(`Experiment with ID ${id} not found`); } - /** - * Resumes a paused experiment - */ - async resumeExperiment(id: string): Promise { - this.logger.log(`Resuming experiment: ${id}`); - const experiment = await this.experimentRepository.findOne({ - where: { id }, - }); - if (!experiment) { - throw new Error(`Experiment with ID ${id} not found`); - } - if (experiment.status !== ExperimentStatus.PAUSED) { - throw new Error('Only paused experiments can be resumed'); - } - experiment.status = ExperimentStatus.RUNNING; - const resumedExperiment = await this.experimentRepository.save(experiment); - this.logger.log(`Experiment resumed: ${resumedExperiment.name}`); - return resumedExperiment; + if (experiment.status !== ExperimentStatus.PAUSED) { + throw new Error('Only paused experiments can be resumed'); } + experiment.status = ExperimentStatus.RUNNING; + const resumedExperiment = await this.experimentRepository.save(experiment); + this.logger.log(`Experiment resumed: ${resumedExperiment.name}`); + return resumedExperiment; + } } diff --git a/src/ab-testing/reporting/ab-testing-reports.service.ts b/src/ab-testing/reporting/ab-testing-reports.service.ts index 5f24e60e..a7400040 100644 --- a/src/ab-testing/reporting/ab-testing-reports.service.ts +++ b/src/ab-testing/reporting/ab-testing-reports.service.ts @@ -242,234 +242,76 @@ export class ABTestingReportsService { duration: this.calculateExperimentDuration(experiment), confidenceLevel: experiment.confidenceLevel, }); - if (!experiment) { - throw new Error(`Experiment with ID ${experimentId} not found`); - } - const statisticalAnalysis = await this.statisticalAnalysisService.calculateStatisticalSignificance(experimentId); - const decisionRecommendations = await this.automatedDecisionService.getDecisionRecommendations(experimentId); - const report = { - experiment: { - id: experiment.id, - name: experiment.name, - description: experiment.description, - type: experiment.type, - status: experiment.status, - startDate: experiment.startDate, - endDate: experiment.endDate, - duration: this.calculateExperimentDuration(experiment), - hypothesis: experiment.hypothesis, - confidenceLevel: experiment.confidenceLevel, - minimumSampleSize: experiment.minimumSampleSize, - trafficAllocation: experiment.trafficAllocation, - }, - variants: experiment.variants.map((variant) => ({ - id: variant.id, - name: variant.name, - description: variant.description, - isControl: variant.isControl, - isWinner: variant.isWinner, - trafficAllocation: variant.trafficAllocation, - configuration: variant.configuration, - metrics: variant.metrics.map((metric) => ({ - id: metric.id, - value: metric.value, - sampleSize: metric.sampleSize, - conversionRate: metric.conversionRate, - standardDeviation: metric.standardDeviation, - confidenceInterval: [metric.confidenceIntervalLower, metric.confidenceIntervalUpper], - pValue: metric.pValue, - isStatisticallySignificant: metric.isStatisticallySignificant, - })), - })), - statisticalAnalysis, - decisionRecommendations, - summary: this.generateSummary(experiment, statisticalAnalysis), - }; - return report; + } } - /** - * Generates summary statistics for the experiment - */ - private generateSummary(experiment: Experiment, statisticalAnalysis: unknown): unknown { - const controlVariant = experiment.variants.find((v) => v.isControl); - const winnerVariant = experiment.variants.find((v) => v.isWinner); - return { - totalVariants: experiment.variants.length, - controlVariant: controlVariant ? controlVariant.name : null, - winner: winnerVariant ? winnerVariant.name : null, - isStatisticallySignificant: statisticalAnalysis.statisticallySignificant, - duration: this.calculateExperimentDuration(experiment), - status: experiment.status, - recommendations: statisticalAnalysis.statisticallySignificant - ? 'Statistically significant results found' - : 'Continue running experiment for more data', - }; - } - /** - * Gets dashboard summary of all experiments - */ - async getDashboardSummary(filters?: ReportFilters): Promise { - this.logger.log('Generating dashboard summary'); - const experiments = await this.getFilteredExperiments(filters); - const summary = { - totalExperiments: experiments.length, - experimentsByStatus: this.groupExperimentsByStatus(experiments), - experimentsByType: this.groupExperimentsByType(experiments), - runningExperiments: experiments.filter((e) => e.status === ExperimentStatus.RUNNING).length, - completedExperiments: experiments.filter((e) => e.status === ExperimentStatus.COMPLETED) - .length, - recentExperiments: experiments.slice(0, 5), // Last 5 experiments - upcomingExperiments: experiments - .filter((e) => e.status === ExperimentStatus.DRAFT && e.startDate > new Date()) - .slice(0, 5), - }; - return summary; - } - /** - * Gets filtered experiments based on criteria - */ - private async getFilteredExperiments(filters?: ReportFilters): Promise { - const queryBuilder = this.experimentRepository.createQueryBuilder('experiment'); - if (filters?.status) { - queryBuilder.andWhere('experiment.status = :status', { status: filters.status }); - } - if (filters?.type) { - queryBuilder.andWhere('experiment.type = :type', { type: filters.type }); - } - if (filters?.startDate) { - queryBuilder.andWhere('experiment.startDate >= :startDate', { startDate: filters.startDate }); - } - if (filters?.endDate) { - queryBuilder.andWhere('experiment.startDate <= :endDate', { endDate: filters.endDate }); - } - if (!filters?.includeArchived) { - queryBuilder.andWhere('experiment.status != :archived', { - archived: ExperimentStatus.ARCHIVED, - }); - } - queryBuilder.orderBy('experiment.createdAt', 'DESC'); - return await queryBuilder.getMany(); - } - /** - * Groups experiments by status - */ - private groupExperimentsByStatus(experiments: Experiment[]): Record { - const statusGroups: Record = {}; - for (const experiment of experiments) { - const status = experiment.status; - statusGroups[status] = (statusGroups[status] || 0) + 1; - } - return statusGroups; - } - /** - * Groups experiments by type - */ - private groupExperimentsByType(experiments: Experiment[]): Record { - const typeGroups: Record = {}; - for (const experiment of experiments) { - const type = experiment.type; - typeGroups[type] = (typeGroups[type] || 0) + 1; - } - return typeGroups; - } - /** - * Calculates experiment duration in days - */ - private calculateExperimentDuration(experiment: Experiment): number { - const startDate = new Date(experiment.startDate); - const endDate = experiment.endDate ? new Date(experiment.endDate) : new Date(); - const diffTime = Math.abs(endDate.getTime() - startDate.getTime()); - return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - } - /** - * Generates performance comparison report - */ - async generatePerformanceComparisonReport(): Promise { - this.logger.log('Generating performance comparison report'); - const experiments = await this.experimentRepository.find({ - where: { status: ExperimentStatus.COMPLETED }, - relations: ['variants'], - order: { createdAt: 'DESC' }, - take: 20, - }); - const performanceData = []; - for (const experiment of experiments) { - const winner = experiment.variants.find((v) => v.isWinner); - const control = experiment.variants.find((v) => v.isControl); - if (winner && control && winner.id !== control.id) { - const improvement = await this.calculateImprovementPercentage(experiment.id, winner.id, control.id); - performanceData.push({ - experimentId: experiment.id, - experimentName: experiment.name, - experimentType: experiment.type, - winnerVariant: winner.name, - controlVariant: control.name, - improvementPercentage: improvement, - duration: this.calculateExperimentDuration(experiment), - confidenceLevel: experiment.confidenceLevel, - }); - } - } - return { - reportTitle: 'Performance Comparison Report', - generatedAt: new Date(), - totalComparisons: performanceData.length, - averageImprovement: performanceData.length > 0 - ? performanceData.reduce((sum, data) => sum + data.improvementPercentage, 0) / - performanceData.length - : 0, - bestPerforming: performanceData.length > 0 - ? [...performanceData].sort((a, b) => b.improvementPercentage - a.improvementPercentage)[0] - : null, - performanceData, - }; - } - /** - * Calculates improvement percentage between winner and control - */ - private async calculateImprovementPercentage(_experimentId: string, _winnerId: string, _controlId: string): Promise { - // This would fetch actual metric data and calculate improvement - // For now, returning a placeholder value - return 15.5; // 15.5% improvement - } - /** - * Exports experiment data in CSV format - */ - async exportExperimentData(experimentId: string): Promise { - this.logger.log(`Exporting data for experiment: ${experimentId}`); - const report = await this.generateExperimentReport(experimentId); - // Convert report to CSV format - let csv = 'Metric,Variant,Value,Sample Size,Conversion Rate,Confidence Interval,P-Value,Statistically Significant\n'; - for (const variant of report.variants) { - for (const metric of variant.metrics) { - csv += `${metric.id},${variant.name},${metric.value},${metric.sampleSize},${metric.conversionRate || ''},`; - csv += `"${metric.confidenceInterval ? `[${metric.confidenceInterval[0]}, ${metric.confidenceInterval[1]}]` : ''}",`; - csv += `${metric.pValue || ''},${metric.isStatisticallySignificant}\n`; - } - } - return csv; - } - /** - * Gets experiment timeline data - */ - async getExperimentTimeline(): Promise { - const experiments = await this.experimentRepository.find({ - order: { startDate: 'ASC' }, - }); - const timeline = experiments.map((experiment) => ({ - id: experiment.id, - name: experiment.name, - startDate: experiment.startDate, - endDate: experiment.endDate || new Date(), - status: experiment.status, - type: experiment.type, - duration: this.calculateExperimentDuration(experiment), - })); - return { - timeline, - totalExperiments: timeline.length, - startDate: timeline.length > 0 ? timeline[0].startDate : null, - endDate: timeline.length > 0 ? timeline[timeline.length - 1].endDate : null, - }; + + return { + reportTitle: 'Performance Comparison Report', + generatedAt: new Date(), + totalComparisons: performanceData.length, + averageImprovement: + performanceData.length > 0 + ? performanceData.reduce((sum, data) => sum + data.improvementPercentage, 0) / + performanceData.length + : 0, + bestPerforming: + performanceData.length > 0 + ? [...performanceData].sort((a, b) => b.improvementPercentage - a.improvementPercentage)[0] + : null, + performanceData, + }; + } + + /** + * Calculates improvement percentage between winner and control + */ + private async calculateImprovementPercentage( + _experimentId: string, + _winnerId: string, + _controlId: string, + ): Promise { + return 15.5; + } + + /** + * Exports experiment data in CSV format + */ + async exportExperimentData(experimentId: string): Promise { + this.logger.log(`Exporting data for experiment: ${experimentId}`); + const report = await this.generateExperimentReport(experimentId); + let csv = + 'Metric,Variant,Value,Sample Size,Conversion Rate,Confidence Interval,P-Value,Statistically Significant\n'; + for (const variant of report.variants) { + for (const metric of variant.metrics) { + csv += `${metric.id},${variant.name},${metric.value},${metric.sampleSize},${metric.conversionRate || ''},`; + csv += `"${metric.confidenceInterval ? `[${metric.confidenceInterval[0]}, ${metric.confidenceInterval[1]}]` : ''}",`; + csv += `${metric.pValue || ''},${metric.isStatisticallySignificant}\n`; + } } + return csv; + } + + /** + * Gets experiment timeline data + */ + async getExperimentTimeline(): Promise { + const experiments = await this.experimentRepository.find({ + order: { startDate: 'ASC' }, + }); + const timeline = experiments.map((experiment) => ({ + id: experiment.id, + name: experiment.name, + startDate: experiment.startDate, + endDate: experiment.endDate || new Date(), + status: experiment.status, + type: experiment.type, + duration: this.calculateExperimentDuration(experiment), + })); + return { + timeline, + totalExperiments: timeline.length, + startDate: timeline.length > 0 ? timeline[0].startDate : null, + endDate: timeline.length > 0 ? timeline[timeline.length - 1].endDate : null, + }; + } } diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index 2908bafc..00000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -describe('AppController', () => { - let appController: AppController; - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - appController = app.get(AppController); - }); - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts index 95614359..d59fe228 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,34 +1,13 @@ -import { Controller, Get, HttpStatus } from '@nestjs/common'; -import { ApiResponse, ApiTags } from '@nestjs/swagger'; -import { ApiTags, ApiResponse } from '@nestjs/swagger'; -import { AppService } from './app.service'; -import { AnalyticsService } from './analytics/analytics.service'; +import { Controller, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; -/** - * Exposes app endpoints. - */ -@ApiTags('app') +@ApiTags('App') @Controller() export class AppController { - constructor( - private readonly appService: AppService, - private readonly analyticsService: AnalyticsService, - ) {} - - /** - * Returns hello. - * @returns The resulting string value. - */ @Get() - @ApiResponse({ status: HttpStatus.OK, description: 'Root endpoint response' }) - getHello(): string { - // Record a lightweight analytics event for root endpoint hits - try { - this.analyticsService.recordEvent('endpoint', 'root_hit'); - } catch { - // swallow analytics errors to avoid impacting the root endpoint - } - - return this.appService.getHello(); + @ApiOperation({ summary: 'Get app status' }) + @ApiResponse({ status: 200, description: 'App is running' }) + getStatus() { + return { message: 'TeachLink API is running', timestamp: new Date().toISOString() }; } } diff --git a/src/app.module.ts b/src/app.module.ts index 8e944cc4..5b256790 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,468 +1,12 @@ -import { Module, DynamicModule, Type, Logger, Global } from '@nestjs/common'; -import { APP_INTERCEPTOR, APP_GUARD } from '@nestjs/core'; -import { ConfigModule } from '@nestjs/config'; -import { TypeOrmModule } from '@nestjs/typeorm'; +import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { MonitoringModule } from './monitoring/monitoring.module'; -import { MonitoringInterceptor } from './common/interceptors/monitoring.interceptor'; -import { TypeOrmMonitoringLogger } from './monitoring/logging/typeorm-logger'; -import { MetricsCollectionService } from './monitoring/metrics/metrics-collection.service'; -import { DatabaseModule } from './common/database/database.module'; -import { BullModule } from '@nestjs/bull'; -import { EventEmitterModule } from '@nestjs/event-emitter'; -import { ScheduleModule } from '@nestjs/schedule'; -import { CacheModule } from '@nestjs/cache-manager'; -import { envValidationSchema } from './config/env.validation'; -import { cacheConfig } from './config/cache.config'; -import { HealthModule } from './health/health.module'; -import { SessionModule } from './session/session.module'; -import { createBullRedisClient } from './common/utils/bull-redis.util'; -import { ThrottlerModule } from '@nestjs/throttler'; -import { CustomThrottleGuard } from './common/guards/throttle.guard'; -import { loadFeatureFlags } from './config/feature-flags.config'; -import { StartupLogger } from './common/lazy-loading/startup-logger.service'; -import { TimeoutInterceptor } from './common/interceptors/timeout.interceptor'; -import { ApiVersioningModule } from './common/modules/api-versioning.module'; -// Feature modules - conditionally loaded based on feature flags -import { SyncModule } from './sync/sync.module'; -import { MediaModule } from './media/media.module'; -import { BackupModule } from './backup/backup.module'; -import { CollaborationModule } from './collaboration/collaboration.module'; -import { DataWarehouseModule } from './data-warehouse/data-warehouse.module'; -import { QueueModule } from './queues/queue.module'; -import { WorkersModule } from './workers/workers.module'; -import { GraphQLModule } from './graphql/graphql.module'; -import { MigrationModule } from './migrations/migration.module'; -import { ABTestingModule } from './ab-testing/ab-testing.module'; -import { ObservabilityModule } from './observability/observability.module'; -import { RateLimitingModule } from './rate-limiting/services/rate-limiting.module'; -import { CachingModule } from './caching/caching.module'; -import { FeatureFlagsModule } from './feature-flags/feature-flags.module'; -import { AnalyticsModule } from './analytics/analytics.module'; import { SearchModule } from './search/search.module'; -import { NotificationsModule } from './notifications/notifications.module'; -import { EmailMarketingModule } from './email-marketing/email-marketing.module'; -import { GamificationModule } from './gamification/gamification.module'; -import { AssessmentsModule } from './assessment/assessment.module'; -import { LearningPathsModule } from './learning-paths/learning-paths.module'; -import { ModerationModule } from './moderation/moderation.module'; -import { OrchestrationModule } from './orchestration/orchestration.module'; -import { SecurityModule } from './security/security.module'; -import { TenancyModule } from './tenancy/tenancy.module'; -import { CdnModule } from './cdn/cdn.module'; -import { AuthModule } from './auth/auth.module'; -import { PaymentsModule } from './payments/payments.module'; -import { LocalizationModule } from './localization/localization.module'; -import { OnboardingModule } from './onboarding/onboarding.module'; -import { CsrfModule } from './common/csrf/csrf.module'; -import { TimeoutModule } from './common/timeout/timeout.module'; -import { ShutdownStateService } from './common/services/shutdown-state.service'; -import { LogShipperService } from './common/services/log-shipper.service'; -import { LoggingInterceptor } from './common/interceptors/logging.interceptor'; -@Global() -@Module({}) -export class AppModule { - /** - * Creates the root application module. - * @returns The resulting dynamic module. - */ - static async forRoot(): Promise { - const flags = loadFeatureFlags(); - const startupLogger = new StartupLogger(); - - // Core modules - always loaded - const coreModules = [ - ConfigModule.forRoot({ - isGlobal: true, - validationSchema: envValidationSchema, - }), - AuditLogModule, - TypeOrmModule.forRootAsync({ - imports: [MonitoringModule], - inject: [MetricsCollectionService], - useFactory: (metricsService: MetricsCollectionService) => { - // Tune postgres pool to avoid connection exhaustion in high-traffic workloads. - // Values can be overridden with DATABASE_POOL_* environment variables. - const poolMax = parseInt(process.env.DATABASE_POOL_MAX || '30', 10); - const poolMin = parseInt(process.env.DATABASE_POOL_MIN || '5', 10); - const poolAcquireTimeoutMs = parseInt( - process.env.DATABASE_POOL_ACQUIRE_TIMEOUT_MS || '10000', - 10, - ); - const poolIdleTimeoutMs = parseInt( - process.env.DATABASE_POOL_IDLE_TIMEOUT_MS || '30000', - 10, - ); - const replicaPort = parseInt(process.env.DATABASE_REPLICA_PORT || '5432', 10); - const replicaHosts = (process.env.DATABASE_REPLICA_HOSTS || '') - .split(',') - .map((host) => host.trim()) - .filter((host) => host.length > 0); - - // Log pool configuration at startup for observability (#274) - const poolLogger = new Logger('DatabasePool'); - poolLogger.log( - `DB pool config — max: ${poolMax}, min: ${poolMin}, ` + - `acquireTimeout: ${poolAcquireTimeoutMs}ms, idleTimeout: ${poolIdleTimeoutMs}ms`, - ); - - return { - type: 'postgres', - replication: { - master: { - host: process.env.DATABASE_HOST || 'localhost', - port: parseInt(process.env.DATABASE_PORT || '5432', 10), - }, - slaves: replicaHosts.map((host) => ({ - host, - port: replicaPort, - })), - }, - username: process.env.DATABASE_USER || 'postgres', - password: process.env.DATABASE_PASSWORD || 'postgres', - database: process.env.DATABASE_NAME || 'teachlink', - autoLoadEntities: true, - synchronize: false, - logging: true, - logger: new TypeOrmMonitoringLogger(metricsService), - maxQueryExecutionTime: 1000, - extra: { - // pg Pool options used by TypeORM postgres driver - max: poolMax, - min: poolMin, - connectionTimeoutMillis: poolAcquireTimeoutMs, - idleTimeoutMillis: poolIdleTimeoutMs, - // Pool event hooks for Prometheus metrics (#274). - // pg fires these on the underlying Pool instance after each - // acquire/release so we can track connection churn over time. - afterPoolConnect: (_client: unknown, _eventCount: number) => { - metricsService.dbPoolConnectionsAcquired.inc(); - metricsService.dbPoolSize.inc(); - }, - afterPoolRelease: (_client: unknown, _eventCount: number) => { - metricsService.dbPoolConnectionsReleased.inc(); - metricsService.dbPoolSize.dec(); - }, - }, - }; - }, - }), - MonitoringModule, - EventEmitterModule.forRoot(), - ScheduleModule.forRoot(), - BullModule.forRoot({ - redis: { - host: process.env.REDIS_HOST || 'localhost', - port: parseInt(process.env.REDIS_PORT || '6379', 10), - }, - createClient: createBullRedisClient, - }), - CacheModule.register(cacheConfig), - SessionModule, - ThrottlerModule.forRoot([ - { - ttl: parseInt(process.env.THROTTLE_TTL || '60'), - limit: parseInt(process.env.THROTTLE_LIMIT || '60'), - }, - ]), - ApiVersioningModule, - HealthModule, - DatabaseModule, - CsrfModule, - TimeoutModule, - ]; - - // Feature modules - conditionally loaded based on feature flags - const featureModules: Array> = []; - - // Auth Module - if (flags.ENABLE_AUTH) { - const startTime = Date.now(); - featureModules.push(AuthModule); - startupLogger.recordModuleLoaded('AuthModule', startTime); - } else { - startupLogger.recordModuleSkipped('AuthModule', 'ENABLE_AUTH=false'); - } - - // Payments Module - if (flags.ENABLE_PAYMENTS) { - const startTime = Date.now(); - featureModules.push(PaymentsModule); - startupLogger.recordModuleLoaded('PaymentsModule', startTime); - } else { - startupLogger.recordModuleSkipped('PaymentsModule', 'ENABLE_PAYMENTS=false'); - } - - // AB Testing Module - if (flags.ENABLE_AB_TESTING) { - const startTime = Date.now(); - featureModules.push(ABTestingModule); - startupLogger.recordModuleLoaded('ABTestingModule', startTime); - } else { - startupLogger.recordModuleSkipped('ABTestingModule', 'ENABLE_AB_TESTING=false'); - } - - // Data Warehouse Module - if (flags.ENABLE_DATA_WAREHOUSE) { - const startTime = Date.now(); - featureModules.push(DataWarehouseModule); - startupLogger.recordModuleLoaded('DataWarehouseModule', startTime); - } else { - startupLogger.recordModuleSkipped('DataWarehouseModule', 'ENABLE_DATA_WAREHOUSE=false'); - } - - // Collaboration Module - if (flags.ENABLE_COLLABORATION) { - const startTime = Date.now(); - featureModules.push(CollaborationModule); - startupLogger.recordModuleLoaded('CollaborationModule', startTime); - } else { - startupLogger.recordModuleSkipped('CollaborationModule', 'ENABLE_COLLABORATION=false'); - } - - // Media Module - if (flags.ENABLE_MEDIA_PROCESSING) { - const startTime = Date.now(); - featureModules.push(MediaModule); - startupLogger.recordModuleLoaded('MediaModule', startTime); - } else { - startupLogger.recordModuleSkipped('MediaModule', 'ENABLE_MEDIA_PROCESSING=false'); - } - - // Backup Module - if (flags.ENABLE_BACKUP) { - const startTime = Date.now(); - featureModules.push(BackupModule); - startupLogger.recordModuleLoaded('BackupModule', startTime); - } else { - startupLogger.recordModuleSkipped('BackupModule', 'ENABLE_BACKUP=false'); - } - - // GraphQL Module - if (flags.ENABLE_GRAPHQL) { - const startTime = Date.now(); - featureModules.push(GraphQLModule); - startupLogger.recordModuleLoaded('GraphQLModule', startTime); - } else { - startupLogger.recordModuleSkipped('GraphQLModule', 'ENABLE_GRAPHQL=false'); - } - - // Sync Module - if (flags.ENABLE_SYNC) { - const startTime = Date.now(); - featureModules.push(SyncModule); - startupLogger.recordModuleLoaded('SyncModule', startTime); - } else { - startupLogger.recordModuleSkipped('SyncModule', 'ENABLE_SYNC=false'); - } - - // Migration Module - if (flags.ENABLE_MIGRATIONS) { - const startTime = Date.now(); - featureModules.push(MigrationModule); - startupLogger.recordModuleLoaded('MigrationModule', startTime); - } else { - startupLogger.recordModuleSkipped('MigrationModule', 'ENABLE_MIGRATIONS=false'); - } - - // Rate Limiting Module - if (flags.ENABLE_RATE_LIMITING) { - const startTime = Date.now(); - featureModules.push(RateLimitingModule); - startupLogger.recordModuleLoaded('RateLimitingModule', startTime); - } else { - startupLogger.recordModuleSkipped('RateLimitingModule', 'ENABLE_RATE_LIMITING=false'); - } - - // Observability Module - if (flags.ENABLE_OBSERVABILITY) { - const startTime = Date.now(); - featureModules.push(ObservabilityModule); - startupLogger.recordModuleLoaded('ObservabilityModule', startTime); - } else { - startupLogger.recordModuleSkipped('ObservabilityModule', 'ENABLE_OBSERVABILITY=false'); - } - - // Caching Module - if (flags.ENABLE_CACHING) { - const startTime = Date.now(); - featureModules.push(CachingModule); - startupLogger.recordModuleLoaded('CachingModule', startTime); - } else { - startupLogger.recordModuleSkipped('CachingModule', 'ENABLE_CACHING=false'); - } - - // Feature Flags Module - if (flags.ENABLE_FEATURE_FLAGS) { - const startTime = Date.now(); - featureModules.push(FeatureFlagsModule); - startupLogger.recordModuleLoaded('FeatureFlagsModule', startTime); - } else { - startupLogger.recordModuleSkipped('FeatureFlagsModule', 'ENABLE_FEATURE_FLAGS=false'); - } - - // Search Module - if (flags.ENABLE_SEARCH) { - const startTime = Date.now(); - featureModules.push(SearchModule); - startupLogger.recordModuleLoaded('SearchModule', startTime); - } else { - startupLogger.recordModuleSkipped('SearchModule', 'ENABLE_SEARCH=false'); - } - - // Notifications Module - if (flags.ENABLE_NOTIFICATIONS) { - const startTime = Date.now(); - featureModules.push(NotificationsModule); - startupLogger.recordModuleLoaded('NotificationsModule', startTime); - } else { - startupLogger.recordModuleSkipped('NotificationsModule', 'ENABLE_NOTIFICATIONS=false'); - } - - // Email Marketing Module - if (flags.ENABLE_EMAIL_MARKETING) { - const startTime = Date.now(); - featureModules.push(EmailMarketingModule); - startupLogger.recordModuleLoaded('EmailMarketingModule', startTime); - } else { - startupLogger.recordModuleSkipped('EmailMarketingModule', 'ENABLE_EMAIL_MARKETING=false'); - } - - // Gamification Module - if (flags.ENABLE_GAMIFICATION) { - const startTime = Date.now(); - featureModules.push(GamificationModule); - startupLogger.recordModuleLoaded('GamificationModule', startTime); - } else { - startupLogger.recordModuleSkipped('GamificationModule', 'ENABLE_GAMIFICATION=false'); - } - - // Assessment Module - if (flags.ENABLE_ASSESSMENT) { - const startTime = Date.now(); - featureModules.push(AssessmentsModule); - startupLogger.recordModuleLoaded('AssessmentModule', startTime); - } else { - startupLogger.recordModuleSkipped('AssessmentModule', 'ENABLE_ASSESSMENT=false'); - } - - // Learning Paths Module - if (flags.ENABLE_LEARNING_PATHS) { - const startTime = Date.now(); - featureModules.push(LearningPathsModule); - startupLogger.recordModuleLoaded('LearningPathsModule', startTime); - } else { - startupLogger.recordModuleSkipped('LearningPathsModule', 'ENABLE_LEARNING_PATHS=false'); - } - - // Moderation Module - if (flags.ENABLE_MODERATION) { - const startTime = Date.now(); - featureModules.push(ModerationModule); - startupLogger.recordModuleLoaded('ModerationModule', startTime); - } else { - startupLogger.recordModuleSkipped('ModerationModule', 'ENABLE_MODERATION=false'); - } - - // Orchestration Module - if (flags.ENABLE_ORCHESTRATION) { - const startTime = Date.now(); - featureModules.push(OrchestrationModule); - startupLogger.recordModuleLoaded('OrchestrationModule', startTime); - } else { - startupLogger.recordModuleSkipped('OrchestrationModule', 'ENABLE_ORCHESTRATION=false'); - } - - // Security Module - if (flags.ENABLE_SECURITY) { - const startTime = Date.now(); - featureModules.push(SecurityModule); - startupLogger.recordModuleLoaded('SecurityModule', startTime); - } else { - startupLogger.recordModuleSkipped('SecurityModule', 'ENABLE_SECURITY=false'); - } - - // Tenancy Module - if (flags.ENABLE_TENANCY) { - const startTime = Date.now(); - featureModules.push(TenancyModule); - startupLogger.recordModuleLoaded('TenancyModule', startTime); - } else { - startupLogger.recordModuleSkipped('TenancyModule', 'ENABLE_TENANCY=false'); - } - - // CDN Module - if (flags.ENABLE_CDN) { - const startTime = Date.now(); - featureModules.push(CdnModule); - startupLogger.recordModuleLoaded('CDNModule', startTime); - } else { - startupLogger.recordModuleSkipped('CDNModule', 'ENABLE_CDN=false'); - } - - // Analytics Module - if (flags.ENABLE_ANALYTICS) { - const startTime = Date.now(); - featureModules.push(AnalyticsModule); - startupLogger.recordModuleLoaded('AnalyticsModule', startTime); - } else { - startupLogger.recordModuleSkipped('AnalyticsModule', 'ENABLE_ANALYTICS=false'); - } - - // Localization Module - if (flags.ENABLE_LOCALIZATION) { - const startTime = Date.now(); - featureModules.push(LocalizationModule); - startupLogger.recordModuleLoaded('LocalizationModule', startTime); - } else { - startupLogger.recordModuleSkipped('LocalizationModule', 'ENABLE_LOCALIZATION=false'); - } - - // Onboarding Module - if (flags.ENABLE_ONBOARDING) { - const startTime = Date.now(); - featureModules.push(OnboardingModule); - startupLogger.recordModuleLoaded('OnboardingModule', startTime); - } else { - startupLogger.recordModuleSkipped('OnboardingModule', 'ENABLE_ONBOARDING=false'); - } - - // Queue Module (always loaded for Bull) - featureModules.push(QueueModule); - - return { - module: AppModule, - imports: [...coreModules, ...featureModules], - controllers: [AppController], - providers: [ - AppService, - StartupLogger, - ShutdownStateService, - LogShipperService, - { - provide: APP_INTERCEPTOR, - useClass: LoggingInterceptor, - }, - { - provide: APP_INTERCEPTOR, - useClass: MonitoringInterceptor, - }, - { - provide: APP_INTERCEPTOR, - useClass: TimeoutInterceptor, - }, - { - provide: APP_INTERCEPTOR, - useClass: SensitiveOperationInterceptor, - }, - { - provide: APP_GUARD, - useClass: CustomThrottleGuard, - }, - ], - exports: [ShutdownStateService, LogShipperService], - }; - } -} +@Module({ + imports: [ + SearchModule, + ], + controllers: [AppController], + providers: [], +}) +export class AppModule {} diff --git a/src/app.service.spec.ts b/src/app.service.spec.ts deleted file mode 100644 index d04c02e0..00000000 --- a/src/app.service.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppService } from './app.service'; -describe('AppService', () => { - let service: AppService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AppService], - }).compile(); - service = module.get(AppService); - }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); - describe('getHello', () => { - it('should return "Hello World!"', () => { - const result = service.getHello(); - expect(result).toBe('Hello World!'); - }); - it('should return a string', () => { - const result = service.getHello(); - expect(typeof result).toBe('string'); - }); - it('should return the expected greeting message', () => { - const result = service.getHello(); - expect(result).toMatch(/^Hello/); - }); - }); - }); -}); diff --git a/src/assessment/assessments.service.spec.ts b/src/assessment/assessments.service.spec.ts deleted file mode 100644 index df81b688..00000000 --- a/src/assessment/assessments.service.spec.ts +++ /dev/null @@ -1,530 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { AssessmentsService } from './assessments.service'; -import { Assessment } from './entities/assessment.entity'; -import { AssessmentAttempt } from './entities/assessment-attempt.entity'; -import { Answer } from './entities/answer.entity'; -import { Question } from './entities/question.entity'; -import { AssessmentStatus } from './enums/assessment-status.enum'; -import { ScoreCalculationService } from './scoring/score-calculation.service'; -import { FeedbackGenerationService } from './feedback/feedback-generation.service'; -import { createMockRepository, createMockQueryBuilder } from 'test/utils/mock-factories'; -import { Repository } from 'typeorm'; -describe('AssessmentsService', () => { - // ───────────────────────────────────────────────────────────────────────── - // DECLARATIONS - // ───────────────────────────────────────────────────────────────────────── - let service: AssessmentsService; - let mockAssessmentRepo: jest.Mocked>; - let mockAttemptRepo: jest.Mocked>; - let mockAnswerRepo: jest.Mocked>; - let mockScoringService: jest.Mocked; - let mockFeedbackService: jest.Mocked; - // ───────────────────────────────────────────────────────────────────────── - // SETUP & TEARDOWN - // ───────────────────────────────────────────────────────────────────────── - beforeEach(async () => { - // Initialize all dependency mocks - mockAssessmentRepo = createMockRepository(); - mockAttemptRepo = createMockRepository(); - mockAnswerRepo = createMockRepository(); - mockScoringService = { - calculate: jest.fn(), - } as jest.Mocked; - mockFeedbackService = { - generate: jest.fn(), - } as jest.Mocked; - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AssessmentsService, - { - provide: getRepositoryToken(Assessment), - useValue: mockAssessmentRepo, - }, - { - provide: getRepositoryToken(AssessmentAttempt), - useValue: mockAttemptRepo, - }, - { - provide: getRepositoryToken(Answer), - useValue: mockAnswerRepo, - }, - { - provide: ScoreCalculationService, - useValue: mockScoringService, - }, - { - provide: FeedbackGenerationService, - useValue: mockFeedbackService, - }, - ], - }).compile(); - service = module.get(AssessmentsService); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - // ───────────────────────────────────────────────────────────────────────── - // TEST SUITES - // ───────────────────────────────────────────────────────────────────────── - describe('startAssessment', () => { - const studentId = 'student-1'; - const assessmentId = 'assessment-1'; - const mockAssessment = { - id: assessmentId, - title: 'Test Assessment', - questions: [ - { id: 'q1', question: 'Question 1' }, - { id: 'q2', question: 'Question 2' }, - ], - }; - beforeEach(() => { - mockAssessmentRepo.findOne.mockResolvedValue(mockAssessment); - mockAttemptRepo.save.mockImplementation(async (attempt) => attempt); - }); - it('should start an assessment and create an attempt', async () => { - const result = await service.startAssessment(studentId, assessmentId); - expect(result).toEqual({ - studentId, - assessment: mockAssessment, - status: AssessmentStatus.IN_PROGRESS, - startedAt: expect.any(Date), - }); - expect(mockAssessmentRepo.findOne).toHaveBeenCalledWith({ - where: { id: assessmentId }, - relations: ['questions'], - }); - expect(mockAttemptRepo.save).toHaveBeenCalled(); - }); - it('should load assessment with questions relation', async () => { - await service.startAssessment(studentId, assessmentId); - expect(mockAssessmentRepo.findOne).toHaveBeenCalledWith({ - where: { id: assessmentId }, - relations: ['questions'], - }); - }); - }); - describe('findAll', () => { - const mockAssessments = [ - { id: '1', title: 'Assessment 1' }, - { id: '2', title: 'Assessment 2' }, - ]; - beforeEach(() => { - mockAssessmentRepo.find.mockResolvedValue(mockAssessments); - }); - it('should return all assessments with questions relation', async () => { - const result = await service.findAll(); - expect(result).toEqual(mockAssessments); - expect(mockAssessmentRepo.find).toHaveBeenCalledWith({ - relations: ['questions'], - }); - }); - }); - describe('findOne', () => { - const assessmentId = 'assessment-1'; - const mockAssessment = { - id: assessmentId, - title: 'Test Assessment', - questions: [], - }; - beforeEach(() => { - mockAssessmentRepo.findOne.mockResolvedValue(mockAssessment); - }); - it('should return assessment by id with questions relation', async () => { - const result = await service.findOne(assessmentId); - expect(result).toEqual(mockAssessment); - expect(mockAssessmentRepo.findOne).toHaveBeenCalledWith({ - where: { id: assessmentId }, - relations: ['questions'], - }); - }); - }); - describe('findByIds', () => { - const ids = ['1', '2', '3']; - const mockAssessments = [ - { id: '1', title: 'Assessment 1' }, - { id: '2', title: 'Assessment 2' }, - { id: '3', title: 'Assessment 3' }, - ]; - beforeEach(() => { - mockAssessmentRepo.findByIds.mockResolvedValue(mockAssessments); - }); - it('should return assessments by ids', async () => { - const result = await service.findByIds(ids); - expect(result).toEqual(mockAssessments); - expect(mockAssessmentRepo.findByIds).toHaveBeenCalledWith(ids); - }); - it('should return empty array for empty ids', async () => { - const result = await service.findByIds([]); - expect(result).toEqual([]); - expect(mockAssessmentRepo.findByIds).not.toHaveBeenCalled(); - }); - }); - describe('create', () => { - const createData = { - title: 'New Assessment', - description: 'Test assessment', - durationMinutes: 60, - }; - const mockAssessment = { - id: 'new-assessment-1', - ...createData, - }; - beforeEach(() => { - mockAssessmentRepo.create.mockReturnValue(mockAssessment as Assessment); - mockAssessmentRepo.save.mockResolvedValue(mockAssessment as Assessment); - }); - it('should create and save a new assessment', async () => { - const result = await service.create(createData); - expect(result).toEqual(mockAssessment); - expect(mockAssessmentRepo.create).toHaveBeenCalledWith(createData); - expect(mockAssessmentRepo.save).toHaveBeenCalledWith(mockAssessment); - }); - it('should handle array return from save', async () => { - const savedArray = [mockAssessment]; - mockAssessmentRepo.save.mockResolvedValue(savedArray as unknown); - const result = await service.create(createData); - expect(result).toEqual(mockAssessment); - }); - }); - describe('update', () => { - const assessmentId = 'assessment-1'; - const updateData = { title: 'Updated Title' }; - const mockAssessment = { - id: assessmentId, - title: 'Updated Title', - description: 'Original description', - }; - beforeEach(() => { - mockAssessmentRepo.update.mockResolvedValue({ affected: 1, raw: {}, generatedMaps: [] }); - mockAssessmentRepo.findOne.mockResolvedValue(mockAssessment); - }); - it('should update assessment and return updated entity', async () => { - const result = await service.update(assessmentId, updateData); - expect(result).toEqual(mockAssessment); - expect(mockAssessmentRepo.update).toHaveBeenCalledWith(assessmentId, updateData); - expect(mockAssessmentRepo.findOne).toHaveBeenCalledWith({ - where: { id: assessmentId }, - relations: ['questions'], - }); - }); - }); - describe('remove', () => { - const assessmentId = 'assessment-1'; - const mockAssessment = { - id: assessmentId, - title: 'Test Assessment', - }; - beforeEach(() => { - mockAssessmentRepo.findOne.mockResolvedValue(mockAssessment); - mockAssessmentRepo.manager = { - transaction: jest.fn().mockImplementation(async (fn) => fn({ - getRepository: jest.fn().mockReturnValue({ - createQueryBuilder: jest.fn().mockReturnValue({ - softDelete: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - execute: jest.fn().mockResolvedValue(undefined), - }), - }), - })), - } as unknown; - }); - it('should soft delete assessment and its questions', async () => { - await service.remove(assessmentId); - expect(mockAssessmentRepo.findOne).toHaveBeenCalledWith({ - where: { id: assessmentId }, - relations: ['questions'], - }); - expect(mockAssessmentRepo.manager.transaction).toHaveBeenCalled(); - }); - it('should do nothing if assessment not found', async () => { - mockAssessmentRepo.findOne.mockResolvedValue(null); - await service.remove(assessmentId); - expect(mockAssessmentRepo.manager.transaction).not.toHaveBeenCalled(); - }); - }); - describe('submitAssessment', () => { - const attemptId = 'attempt-1'; - const answers = [ - { questionId: 'q1', response: 'Answer 1' }, - { questionId: 'q2', response: 'Answer 2' }, - ]; - const mockAttempt = { - id: attemptId, - startedAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago - assessment: { - id: 'assessment-1', - durationMinutes: 60, - questions: [ - { id: 'q1', points: 10 }, - { id: 'q2', points: 15 }, - ], - }, - }; - beforeEach(() => { - mockAttemptRepo.findOne.mockResolvedValue(mockAttempt as unknown); - mockScoringService.calculate - .mockReturnValueOnce(8) // Question 1: 8/10 points - .mockReturnValueOnce(12); // Question 2: 12/15 points - mockAnswerRepo.save.mockResolvedValue({} as unknown); - mockAttemptRepo.save.mockImplementation(async (attempt) => attempt); - mockFeedbackService.generate.mockReturnValue({ - overall: 'Good performance', - strengths: ['Good understanding'], - improvements: ['Need more practice'], - }); - }); - it('should submit assessment and calculate score', async () => { - const result = await service.submitAssessment(attemptId, answers); - expect(result).toEqual({ - attempt: { - ...mockAttempt, - score: 20, // 8 + 12 - status: AssessmentStatus.GRADED, - submittedAt: expect.any(Date), - }, - feedback: { - overall: 'Good performance', - strengths: ['Good understanding'], - improvements: ['Need more practice'], - }, - }); - expect(mockAttemptRepo.findOne).toHaveBeenCalledWith({ - where: { id: attemptId }, - relations: ['assessment', 'assessment.questions'], - }); - expect(mockScoringService.calculate).toHaveBeenCalledTimes(2); - expect(mockAnswerRepo.save).toHaveBeenCalledTimes(2); - expect(mockFeedbackService.generate).toHaveBeenCalledWith(20, 25); // totalScore, maxScore - }); - it('should mark as timed out if submitted after duration', async () => { - // Set startedAt to 2 hours ago, duration is 60 minutes - const oldAttempt = { - ...mockAttempt, - startedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago - }; - mockAttemptRepo.findOne.mockResolvedValue(oldAttempt as unknown); - const result = await service.submitAssessment(attemptId, answers); - expect(result.status).toBe(AssessmentStatus.TIMED_OUT); - expect(mockScoringService.calculate).not.toHaveBeenCalled(); - }); - }); - describe('getResults', () => { - const attemptId = 'attempt-1'; - const mockResults = { - id: attemptId, - score: 85, - answers: [ - { - id: 'answer-1', - question: { id: 'q1', question: 'Question 1' }, - response: 'Answer 1', - awardedPoints: 8, - }, - ], - }; - beforeEach(() => { - mockAttemptRepo.findOne.mockResolvedValue(mockResults as unknown); - }); - it('should return assessment results with answers and questions', async () => { - const result = await service.getResults(attemptId); - expect(result).toEqual(mockResults); - expect(mockAttemptRepo.findOne).toHaveBeenCalledWith({ - where: { id: attemptId }, - relations: ['answers', 'answers.question'], - }); - }); - }); - - it('should create and save a new assessment', async () => { - const result = await service.create(createData); - - expect(result).toEqual(mockAssessment); - expect(mockAssessmentRepo.create).toHaveBeenCalledWith(createData); - expect(mockAssessmentRepo.save).toHaveBeenCalledWith(mockAssessment); - }); - - it('should handle array return from save', async () => { - const savedArray = [mockAssessment]; - mockAssessmentRepo.save.mockResolvedValue(savedArray as any); - - const result = await service.create(createData); - - expect(result).toEqual(mockAssessment); - }); - }); - - describe('update', () => { - const assessmentId = 'assessment-1'; - const updateData = { title: 'Updated Title' }; - const mockAssessment = { - id: assessmentId, - title: 'Updated Title', - description: 'Original description', - }; - - beforeEach(() => { - mockAssessmentRepo.update.mockResolvedValue({ affected: 1, raw: {}, generatedMaps: [] }); - mockAssessmentRepo.findOne.mockResolvedValue(mockAssessment); - }); - - it('should update assessment and return updated entity', async () => { - const result = await service.update(assessmentId, updateData); - - expect(result).toEqual(mockAssessment); - expect(mockAssessmentRepo.update).toHaveBeenCalledWith(assessmentId, updateData); - expect(mockAssessmentRepo.findOne).toHaveBeenCalledWith({ - where: { id: assessmentId }, - relations: ['questions'], - }); - }); - }); - - describe('remove', () => { - const assessmentId = 'assessment-1'; - const mockAssessment = { - id: assessmentId, - title: 'Test Assessment', - }; - - beforeEach(() => { - mockAssessmentRepo.findOne.mockResolvedValue(mockAssessment); - mockAssessmentRepo.manager = { - transaction: jest.fn().mockImplementation(async (fn) => - fn({ - getRepository: jest.fn().mockReturnValue({ - createQueryBuilder: jest.fn().mockReturnValue({ - softDelete: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - execute: jest.fn().mockResolvedValue(undefined), - }), - }), - }), - ), - } as any; - }); - - it('should soft delete assessment and its questions', async () => { - await service.remove(assessmentId); - - expect(mockAssessmentRepo.findOne).toHaveBeenCalledWith({ - where: { id: assessmentId }, - relations: ['questions'], - }); - expect(mockAssessmentRepo.manager.transaction).toHaveBeenCalled(); - }); - - it('should do nothing if assessment not found', async () => { - mockAssessmentRepo.findOne.mockResolvedValue(null); - - await service.remove(assessmentId); - - expect(mockAssessmentRepo.manager.transaction).not.toHaveBeenCalled(); - }); - }); - - describe('submitAssessment', () => { - const attemptId = 'attempt-1'; - const answers = [ - { questionId: 'q1', response: 'Answer 1' }, - { questionId: 'q2', response: 'Answer 2' }, - ]; - - const mockAttempt = { - id: attemptId, - startedAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago - assessment: { - id: 'assessment-1', - durationMinutes: 60, - questions: [ - { id: 'q1', points: 10 }, - { id: 'q2', points: 15 }, - ], - }, - }; - - beforeEach(() => { - mockAttemptRepo.findOne.mockResolvedValue(mockAttempt as any); - mockScoringService.calculate - .mockReturnValueOnce(8) // Question 1: 8/10 points - .mockReturnValueOnce(12); // Question 2: 12/15 points - mockAnswerRepo.save.mockResolvedValue({} as any); - mockAttemptRepo.save.mockImplementation(async (attempt) => attempt); - mockFeedbackService.generate.mockReturnValue({ - overall: 'Good performance', - strengths: ['Good understanding'], - improvements: ['Need more practice'], - }); - }); - - it('should submit assessment and calculate score', async () => { - const result = await service.submitAssessment(attemptId, answers); - - expect(result).toEqual({ - attempt: { - ...mockAttempt, - score: 20, // 8 + 12 - status: AssessmentStatus.GRADED, - submittedAt: expect.any(Date), - }, - feedback: { - overall: 'Good performance', - strengths: ['Good understanding'], - improvements: ['Need more practice'], - }, - }); - - expect(mockAttemptRepo.findOne).toHaveBeenCalledWith({ - where: { id: attemptId }, - relations: ['assessment', 'assessment.questions'], - }); - expect(mockScoringService.calculate).toHaveBeenCalledTimes(2); - expect(mockAnswerRepo.save).toHaveBeenCalledTimes(2); - expect(mockFeedbackService.generate).toHaveBeenCalledWith(20, 25); // totalScore, maxScore - }); - - it('should mark as timed out if submitted after duration', async () => { - // Set startedAt to 2 hours ago, duration is 60 minutes - const oldAttempt = { - ...mockAttempt, - startedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago - }; - mockAttemptRepo.findOne.mockResolvedValue(oldAttempt as any); - - const result = await service.submitAssessment(attemptId, answers); - - expect(result.status).toBe(AssessmentStatus.TIMED_OUT); - expect(mockScoringService.calculate).not.toHaveBeenCalled(); - }); - }); - - describe('getResults', () => { - const attemptId = 'attempt-1'; - const mockResults = { - id: attemptId, - score: 85, - answers: [ - { - id: 'answer-1', - question: { id: 'q1', question: 'Question 1' }, - response: 'Answer 1', - awardedPoints: 8, - }, - ], - }; - - beforeEach(() => { - mockAttemptRepo.findOne.mockResolvedValue(mockResults as any); - }); - - it('should return assessment results with answers and questions', async () => { - const result = await service.getResults(attemptId); - - expect(result).toEqual(mockResults); - expect(mockAttemptRepo.findOne).toHaveBeenCalledWith({ - where: { id: attemptId }, - relations: ['answers', 'answers.question'], - }); - }); - }); -}); diff --git a/src/assessment/assessments.service.ts b/src/assessment/assessments.service.ts index 59f01ee9..5ee1f242 100644 --- a/src/assessment/assessments.service.ts +++ b/src/assessment/assessments.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { AssessmentStatus } from './enums/assessment-status.enum'; @@ -132,6 +132,10 @@ export class AssessmentsService { relations: ['assessment', 'assessment.questions'], }); + if (!attempt?.assessment?.questions) { + throw new NotFoundException(`Attempt ${attemptId} not found`); + } + const endTime = new Date(attempt.startedAt).getTime() + attempt.assessment.durationMinutes * 60000; @@ -139,79 +143,20 @@ export class AssessmentsService { attempt.status = AssessmentStatus.TIMED_OUT; return this.attemptRepo.save(attempt); } - async findOne(id: string): Promise { - return await this.assessmentRepo.findOne({ - where: { id }, - relations: ['questions'], - }); - } - async findByIds(ids: string[]): Promise { - if (ids.length === 0) - return []; - return await this.assessmentRepo.findByIds(ids); - } - async create(data: unknown): Promise { - const assessment = this.assessmentRepo.create(data); - const saved = await this.assessmentRepo.save(assessment); - return Array.isArray(saved) ? saved[0] : saved; - } - async update(id: string, data: unknown): Promise { - await this.assessmentRepo.update(id, data); - return this.findOne(id); - } - async remove(id: string): Promise { - const assessment = await this.findOne(id); - if (!assessment) { - return; - } - await this.assessmentRepo.manager.transaction(async (manager) => { - await manager - .getRepository(Question) - .createQueryBuilder() - .softDelete() - .where('"assessmentId" = :assessmentId', { assessmentId: id }) - .execute(); - await manager.getRepository(Assessment).softDelete(id); - }); - } - async submitAssessment(attemptId: string, answers: unknown[]) { - const attempt = await this.attemptRepo.findOne({ - where: { id: attemptId }, - relations: ['assessment', 'assessment.questions'], - }); - const endTime = new Date(attempt.startedAt).getTime() + attempt.assessment.durationMinutes * 60000; - if (Date.now() > endTime) { - attempt.status = AssessmentStatus.TIMED_OUT; - return this.attemptRepo.save(attempt); - } - let totalScore = 0; - let maxScore = 0; - for (const question of attempt.assessment.questions) { - const response = answers.find((a) => a.questionId === question.id)?.response; - const score = this.scoringService.calculate(question, response); - maxScore += question.points; - totalScore += score; - await this.answerRepo.save({ - attempt, - question, - response, - awardedPoints: score, - }); - } - attempt.score = totalScore; - attempt.status = AssessmentStatus.GRADED; - attempt.submittedAt = new Date(); - const feedback = this.feedbackService.generate(totalScore, maxScore); - return { - attempt: await this.attemptRepo.save(attempt), - feedback, - }; - } - getResults(attemptId: string) { - return this.attemptRepo.findOne({ - where: { id: attemptId }, - relations: ['answers', 'answers.question'], - }); + + let totalScore = 0; + let maxScore = 0; + for (const question of attempt.assessment.questions) { + const response = answers.find((a) => a.questionId === question.id)?.response; + const score = this.scoringService.calculate(question, response); + maxScore += question.points; + totalScore += score; + await this.answerRepo.save({ + attempt, + question, + response, + awardedPoints: score, + }); } attempt.score = totalScore; diff --git a/src/assessment/scoring/score-calculation.service.ts b/src/assessment/scoring/score-calculation.service.ts index 881f0c56..2de06ebb 100644 --- a/src/assessment/scoring/score-calculation.service.ts +++ b/src/assessment/scoring/score-calculation.service.ts @@ -26,4 +26,5 @@ export class ScoreCalculationService { default: return 0; } + } } diff --git a/src/audit-log/audit-log.controller.ts b/src/audit-log/audit-log.controller.ts deleted file mode 100644 index e91dd98a..00000000 --- a/src/audit-log/audit-log.controller.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { Controller, Get, Post, Query, Param, Res, UseGuards, HttpException, HttpStatus, Logger, ParseIntPipe, DefaultValuePipe, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiQuery, ApiParam, ApiBearerAuth, } from '@nestjs/swagger'; -import { Response } from 'express'; -import { AuditLogService, IAuditLogSearchFilters } from './audit-log.service'; -import { AuditLog } from './audit-log.entity'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { AuditAction, AuditCategory, AuditSeverity } from './enums/audit-action.enum'; -import { SensitiveOperationsService } from './services/sensitive-operations.service'; - -/** - * Exposes audit Log endpoints. - */ -@ApiTags('Audit Logs') -@ApiBearerAuth() -@UseGuards(JwtAuthGuard) -@Controller('audit-logs') -export class AuditLogController { - private readonly logger = new Logger(AuditLogController.name); - - constructor( - private readonly auditLogService: AuditLogService, - private readonly sensitiveOpsService: SensitiveOperationsService, - ) {} - - /** - * Returns search. - * @param userId The user identifier. - * @param userEmail The email address. - * @param actions The actions. - * @param categories The categories. - * @param severities The severities. - * @param entityType The entity type. - * @param entityId The entity identifier. - * @param ipAddress The ip address. - * @param sessionId The session identifier. - * @param tenantId The tenant identifier. - * @param apiEndpoint The api endpoint. - * @param httpMethod The http method. - * @param statusCode The status value. - * @param startDate The start date. - * @param endDate The end date. - * @param page The page number. - * @param limit The maximum number of results. - * @returns The operation result. - */ - @Get() - @ApiOperation({ summary: 'Search audit logs with filters' }) - @ApiQuery({ name: 'userId', required: false, description: 'Filter by user ID' }) - @ApiQuery({ name: 'userEmail', required: false, description: 'Filter by user email' }) - @ApiQuery({ - name: 'actions', - required: false, - description: 'Filter by actions (comma-separated)', - }) - @ApiQuery({ - name: 'categories', - required: false, - description: 'Filter by categories (comma-separated)', - }) - @ApiQuery({ - name: 'severities', - required: false, - description: 'Filter by severities (comma-separated)', - }) - @ApiQuery({ name: 'entityType', required: false, description: 'Filter by entity type' }) - @ApiQuery({ name: 'entityId', required: false, description: 'Filter by entity ID' }) - @ApiQuery({ name: 'ipAddress', required: false, description: 'Filter by IP address' }) - @ApiQuery({ name: 'sessionId', required: false, description: 'Filter by session ID' }) - @ApiQuery({ name: 'tenantId', required: false, description: 'Filter by tenant ID' }) - @ApiQuery({ name: 'apiEndpoint', required: false, description: 'Filter by API endpoint' }) - @ApiQuery({ name: 'httpMethod', required: false, description: 'Filter by HTTP method' }) - @ApiQuery({ name: 'statusCode', required: false, description: 'Filter by HTTP status code' }) - @ApiQuery({ name: 'startDate', required: false, description: 'Start date (ISO 8601)' }) - @ApiQuery({ name: 'endDate', required: false, description: 'End date (ISO 8601)' }) - @ApiQuery({ name: 'page', required: false, description: 'Page number', type: Number }) - @ApiQuery({ name: 'limit', required: false, description: 'Items per page', type: Number }) - @ApiResponse({ status: 200, description: 'Search results' }) - async search( - @Query('userId') userId?: string, - @Query('userEmail') userEmail?: string, - @Query('actions') actions?: string, - @Query('categories') categories?: string, - @Query('severities') severities?: string, - @Query('entityType') entityType?: string, - @Query('entityId') entityId?: string, - @Query('ipAddress') ipAddress?: string, - @Query('sessionId') sessionId?: string, - @Query('tenantId') tenantId?: string, - @Query('apiEndpoint') apiEndpoint?: string, - @Query('httpMethod') httpMethod?: string, - @Query('statusCode') statusCode?: string, - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - @Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number, - @Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number, - ) { - const filters: IAuditLogSearchFilters = {}; - - if (userId) filters.userId = userId; - if (userEmail) filters.userEmail = userEmail; - if (actions) filters.actions = actions.split(',') as AuditAction[]; - if (categories) filters.categories = categories.split(',') as AuditCategory[]; - if (severities) filters.severities = severities.split(',') as AuditSeverity[]; - if (entityType) filters.entityType = entityType; - if (entityId) filters.entityId = entityId; - if (ipAddress) filters.ipAddress = ipAddress; - if (sessionId) filters.sessionId = sessionId; - if (tenantId) filters.tenantId = tenantId; - if (apiEndpoint) filters.apiEndpoint = apiEndpoint; - if (httpMethod) filters.httpMethod = httpMethod; - if (statusCode) filters.statusCode = parseInt(statusCode, 10); - if (startDate) filters.startDate = new Date(startDate); - if (endDate) filters.endDate = new Date(endDate); - - return this.auditLogService.search(filters, page, limit); - } - - /** - * Returns recent. - * @param limit The maximum number of results. - * @returns The matching results. - */ - @Get('recent') - @ApiOperation({ summary: 'Get recent audit logs' }) - @ApiQuery({ - name: 'limit', - required: false, - description: 'Number of logs to return', - type: Number, - }) - @ApiResponse({ status: 200, description: 'Recent audit logs' }) - async getRecent( - @Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit?: number, - ): Promise { - return this.auditLogService.findAll(limit); - } - - /** - * Returns by User. - * @param userId The user identifier. - * @param limit The maximum number of results. - * @returns The matching results. - */ - @Get('user/:userId') - @ApiOperation({ summary: 'Get audit logs for a specific user' }) - @ApiParam({ name: 'userId', description: 'User ID' }) - @ApiQuery({ - name: 'limit', - required: false, - description: 'Number of logs to return', - type: Number, - }) - @ApiResponse({ status: 200, description: 'User audit logs' }) - async getByUser( - @Param('userId') userId: string, - @Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit?: number, - ): Promise { - return this.auditLogService.findByUser(userId, limit); - } - - /** - * Returns by Entity. - * @param entityType The entity type. - * @param entityId The entity identifier. - * @param limit The maximum number of results. - * @returns The matching results. - */ - @Get('entity/:entityType/:entityId') - @ApiOperation({ summary: 'Get audit logs for a specific entity' }) - @ApiParam({ name: 'entityType', description: 'Entity type (e.g., user, course)' }) - @ApiParam({ name: 'entityId', description: 'Entity ID' }) - @ApiQuery({ - name: 'limit', - required: false, - description: 'Number of logs to return', - type: Number, - }) - @ApiResponse({ status: 200, description: 'Entity audit logs' }) - async getByEntity( - @Param('entityType') entityType: string, - @Param('entityId') entityId: string, - @Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit?: number, - ): Promise { - return this.auditLogService.findByEntity(entityType, entityId, limit); - } - - /** - * Returns by Ip Address. - * @param ipAddress The ip address. - * @param limit The maximum number of results. - * @returns The matching results. - */ - @Get('ip/:ipAddress') - @ApiOperation({ summary: 'Get audit logs by IP address' }) - @ApiParam({ name: 'ipAddress', description: 'IP address' }) - @ApiQuery({ - name: 'limit', - required: false, - description: 'Number of logs to return', - type: Number, - }) - @ApiResponse({ status: 200, description: 'IP audit logs' }) - async getByIpAddress( - @Param('ipAddress') ipAddress: string, - @Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit?: number, - ): Promise { - return this.auditLogService.findByIpAddress(ipAddress, limit); - } - - /** - * Returns statistics. - * @returns The operation result. - */ - @Get('statistics') - @ApiOperation({ summary: 'Get audit log statistics' }) - @ApiResponse({ status: 200, description: 'Statistics' }) - async getStatistics() { - return this.auditLogService.getStatistics(); - } - - /** - * Generates report. - * @param startDate The start date. - * @param endDate The end date. - * @returns The operation result. - */ - @Get('report') - @ApiOperation({ summary: 'Generate audit report' }) - @ApiQuery({ name: 'startDate', required: true, description: 'Start date (ISO 8601)' }) - @ApiQuery({ name: 'endDate', required: true, description: 'End date (ISO 8601)' }) - @ApiResponse({ status: 200, description: 'Audit report' }) - async generateReport(@Query('startDate') startDate: string, @Query('endDate') endDate: string) { - if (!startDate || !endDate) { - throw new HttpException('Start date and end date are required', HttpStatus.BAD_REQUEST); - } - - return this.auditLogService.generateReport(new Date(startDate), new Date(endDate)); - } - - /** - * Exports to Json. - * @param res The res. - * @param userId The user identifier. - * @param actions The actions. - * @param startDate The start date. - * @param endDate The end date. - * @returns The operation result. - */ - @Post('export/json') - @ApiOperation({ summary: 'Export audit logs to JSON' }) - @ApiQuery({ name: 'userId', required: false }) - @ApiQuery({ name: 'actions', required: false }) - @ApiQuery({ name: 'startDate', required: false }) - @ApiQuery({ name: 'endDate', required: false }) - async exportToJson( - @Res() res: Response, - @Query('userId') userId?: string, - @Query('actions') actions?: string, - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - ) { - const filters: IAuditLogSearchFilters = {}; - if (userId) filters.userId = userId; - if (actions) filters.actions = actions.split(',') as AuditAction[]; - if (startDate) filters.startDate = new Date(startDate); - if (endDate) filters.endDate = new Date(endDate); - - const json = await this.auditLogService.exportToJson(filters); - - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Content-Disposition', 'attachment; filename=audit-logs.json'); - res.send(json); - } - - /** - * Exports to Csv. - * @param res The res. - * @param userId The user identifier. - * @param actions The actions. - * @param startDate The start date. - * @param endDate The end date. - * @returns The operation result. - */ - @Post('export/csv') - @ApiOperation({ summary: 'Export audit logs to CSV' }) - @ApiQuery({ name: 'userId', required: false }) - @ApiQuery({ name: 'actions', required: false }) - @ApiQuery({ name: 'startDate', required: false }) - @ApiQuery({ name: 'endDate', required: false }) - async exportToCsv( - @Res() res: Response, - @Query('userId') userId?: string, - @Query('actions') actions?: string, - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - ) { - const filters: IAuditLogSearchFilters = {}; - if (userId) filters.userId = userId; - if (actions) filters.actions = actions.split(',') as AuditAction[]; - if (startDate) filters.startDate = new Date(startDate); - if (endDate) filters.endDate = new Date(endDate); - - const csv = await this.auditLogService.exportToCsv(filters); - - res.setHeader('Content-Type', 'text/csv'); - res.setHeader('Content-Disposition', 'attachment; filename=audit-logs.csv'); - res.send(csv); - } - - /** - * Applies retention Policy. - * @returns The operation result. - */ - @Post('retention/apply') - @ApiOperation({ summary: 'Apply retention policy (delete old logs)' }) - @ApiResponse({ status: 200, description: 'Retention policy applied' }) - async applyRetentionPolicy() { - const deletedCount = await this.auditLogService.applyRetentionPolicy(); - return { - message: 'Retention policy applied successfully', - deletedCount, - }; - } - - @Get('sensitive-operations') - @ApiOperation({ summary: 'Get sensitive operations audit logs' }) - @ApiQuery({ - name: 'severities', - required: false, - description: 'Filter by severities (comma-separated)', - }) - @ApiQuery({ name: 'startDate', required: false, description: 'Start date (ISO 8601)' }) - @ApiQuery({ name: 'endDate', required: false, description: 'End date (ISO 8601)' }) - @ApiQuery({ name: 'page', required: false, description: 'Page number', type: Number }) - @ApiQuery({ name: 'limit', required: false, description: 'Items per page', type: Number }) - @IApiResponse({ status: 200, description: 'Sensitive operations' }) - async getSensitiveOperations( - @Query('severities') severities?: string, - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - @Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number, - @Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number, - ) { - const filters: IAuditLogSearchFilters = { - severities: severities - ? (severities.split(',') as AuditSeverity[]) - : [AuditSeverity.WARNING, AuditSeverity.CRITICAL], - }; - - if (startDate) filters.startDate = new Date(startDate); - if (endDate) filters.endDate = new Date(endDate); - - return this.auditLogService.search(filters, page, limit); - } - - @Get('sensitive-operations/by-action/:action') - @ApiOperation({ summary: 'Get sensitive operations by action type' }) - @ApiParam({ name: 'action', description: 'Audit action' }) - @ApiQuery({ name: 'startDate', required: false, description: 'Start date (ISO 8601)' }) - @ApiQuery({ name: 'endDate', required: false, description: 'End date (ISO 8601)' }) - @ApiQuery({ name: 'limit', required: false, description: 'Items per page', type: Number }) - @IApiResponse({ status: 200, description: 'Sensitive operations by action' }) - async getSensitiveOperationsByAction( - @Param('action') action: AuditAction, - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - @Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit?: number, - ): Promise { - const filters: IAuditLogSearchFilters = { - actions: [action], - }; - - if (startDate) filters.startDate = new Date(startDate); - if (endDate) filters.endDate = new Date(endDate); - - const result = await this.auditLogService.search(filters, 1, limit); - return result.logs; - } - - @Get('sensitive-operations/critical') - @ApiOperation({ summary: 'Get critical operations' }) - @ApiQuery({ name: 'startDate', required: false, description: 'Start date (ISO 8601)' }) - @ApiQuery({ name: 'endDate', required: false, description: 'End date (ISO 8601)' }) - @ApiQuery({ name: 'limit', required: false, description: 'Items per page', type: Number }) - @IApiResponse({ status: 200, description: 'Critical operations' }) - async getCriticalOperations( - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - @Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit?: number, - ): Promise { - const filters: IAuditLogSearchFilters = { - severities: [AuditSeverity.CRITICAL], - }; - - if (startDate) filters.startDate = new Date(startDate); - if (endDate) filters.endDate = new Date(endDate); - - const result = await this.auditLogService.search(filters, 1, limit); - return result.logs; - } - - @Get('sensitive-operations/user-changes/:userId') - @ApiOperation({ summary: 'Get user-related sensitive operations' }) - @ApiParam({ name: 'userId', description: 'User ID' }) - @ApiQuery({ name: 'startDate', required: false, description: 'Start date (ISO 8601)' }) - @ApiQuery({ name: 'endDate', required: false, description: 'End date (ISO 8601)' }) - @ApiQuery({ name: 'limit', required: false, description: 'Items per page', type: Number }) - @IApiResponse({ status: 200, description: 'User-related sensitive operations' }) - async getUserSensitiveOperations( - @Param('userId') userId: string, - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - @Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit?: number, - ): Promise { - const filters: IAuditLogSearchFilters = { - entityType: 'User', - entityId: userId, - severities: [AuditSeverity.WARNING, AuditSeverity.CRITICAL], - }; - - if (startDate) filters.startDate = new Date(startDate); - if (endDate) filters.endDate = new Date(endDate); - - const result = await this.auditLogService.search(filters, 1, limit); - return result.logs; - } - - @Post('sensitive-operations/export') - @ApiOperation({ summary: 'Export sensitive operations to CSV' }) - @ApiQuery({ name: 'startDate', required: false }) - @ApiQuery({ name: 'endDate', required: false }) - async exportSensitiveOperations( - @Res() res: Response, - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - ) { - const filters: IAuditLogSearchFilters = { - severities: [AuditSeverity.WARNING, AuditSeverity.CRITICAL], - }; - - if (startDate) filters.startDate = new Date(startDate); - if (endDate) filters.endDate = new Date(endDate); - - const csv = await this.auditLogService.exportToCsv(filters); - - res.setHeader('Content-Type', 'text/csv'); - res.setHeader('Content-Disposition', 'attachment; filename=sensitive-operations.csv'); - res.send(csv); - } -} diff --git a/src/audit-log/audit-log.module.ts b/src/audit-log/audit-log.module.ts deleted file mode 100644 index 73982704..00000000 --- a/src/audit-log/audit-log.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { ConfigModule } from '@nestjs/config'; -import { ScheduleModule } from '@nestjs/schedule'; -import { AuditLog } from './audit-log.entity'; -import { AuditLogService } from './audit-log.service'; -import { AuditLogController } from './audit-log.controller'; -import { AuditLogInterceptor } from './interceptors/audit-log.interceptor'; -import { AuditRetentionTask } from './tasks/audit-retention.task'; -import { SensitiveOperationsService } from './services/sensitive-operations.service'; - -/** - * Registers the audit Log module. - */ -@Module({ - imports: [TypeOrmModule.forFeature([AuditLog]), ConfigModule, ScheduleModule.forRoot()], - controllers: [AuditLogController], - providers: [AuditLogService, AuditLogInterceptor, AuditRetentionTask, SensitiveOperationsService], - exports: [AuditLogService, AuditLogInterceptor, SensitiveOperationsService], -}) -export class AuditLogModule { -} diff --git a/src/audit-log/audit-log.service.spec.ts b/src/audit-log/audit-log.service.spec.ts deleted file mode 100644 index 8fa7150d..00000000 --- a/src/audit-log/audit-log.service.spec.ts +++ /dev/null @@ -1,777 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { AuditLogService } from './audit-log.service'; -import { AuditLog } from './audit-log.entity'; -import { AuditAction, AuditSeverity, AuditCategory } from './enums/audit-action.enum'; -import { createMockRepository, createMockConfigService, createMockQueryBuilder, } from 'test/utils/mock-factories'; -import { Repository } from 'typeorm'; -describe('AuditLogService', () => { - // ───────────────────────────────────────────────────────────────────────── - // DECLARATIONS - // ───────────────────────────────────────────────────────────────────────── - let service: AuditLogService; - let mockAuditRepo: jest.Mocked>; - let mockConfigService: jest.Mocked; - // ───────────────────────────────────────────────────────────────────────── - // SETUP & TEARDOWN - // ───────────────────────────────────────────────────────────────────────── - beforeEach(async () => { - // Initialize dependency mocks - mockAuditRepo = createMockRepository(); - mockConfigService = createMockConfigService({ - AUDIT_LOG_RETENTION_DAYS: 365, - }); - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AuditLogService, - { - provide: getRepositoryToken(AuditLog), - useValue: mockAuditRepo, - }, - { - provide: 'ConfigService', - useValue: mockConfigService, - }, - ], - }).compile(); - service = module.get(AuditLogService); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - // ───────────────────────────────────────────────────────────────────────── - // TEST SUITES - // ───────────────────────────────────────────────────────────────────────── - describe('constructor', () => { - it('should initialize with default retention days', () => { - const defaultService = new AuditLogService(mockAuditRepo, { - get: jest.fn().mockReturnValue(undefined), - } as unknown); - expect((defaultService as unknown).retentionDays).toBe(365); - }); - it('should use configured retention days', () => { - expect((service as unknown).retentionDays).toBe(365); - }); - }); - }); - - describe('log', () => { - const auditEntry = { - userId: 'user-1', - userEmail: 'test@example.com', - action: AuditAction.LOGIN, - category: AuditCategory.AUTHENTICATION, - severity: AuditSeverity.INFO, - description: 'User logged in', - ipAddress: '127.0.0.1', - userAgent: 'TestAgent', - metadata: { sessionId: 'session-1' }, - }; - - const mockSavedLog = { - id: 'log-1', - ...auditEntry, - timestamp: new Date(), - retentionUntil: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), - }; - - beforeEach(() => { - mockAuditRepo.create.mockReturnValue(mockSavedLog as AuditLog); - mockAuditRepo.save.mockResolvedValue(mockSavedLog as AuditLog); - }); - - it('should create and save audit log entry', async () => { - const result = await service.log(auditEntry); - - expect(result).toEqual(mockSavedLog); - expect(mockAuditRepo.create).toHaveBeenCalledWith({ - ...auditEntry, - severity: AuditSeverity.INFO, - retentionUntil: expect.any(Date), - }); - expect(mockAuditRepo.save).toHaveBeenCalledWith(mockSavedLog); - }); - - it('should use default severity when not provided', async () => { - const entryWithoutSeverity = { ...auditEntry }; - delete entryWithoutSeverity.severity; - - await service.log(entryWithoutSeverity); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - severity: AuditSeverity.INFO, - }), - ); - }); - - it('should handle save errors gracefully', async () => { - const error = new Error('Database error'); - mockAuditRepo.save.mockRejectedValue(error); - - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - - const result = await service.log(auditEntry); - - expect(result).toEqual(mockSavedLog); // Returns created log even on error - expect(consoleSpy).toHaveBeenCalledWith('Failed to create audit log:', error); - - consoleSpy.mockRestore(); - }); - - it('should set retention date correctly', async () => { - const retentionDays = 365; - const expectedRetentionDate = new Date(); - expectedRetentionDate.setDate(expectedRetentionDate.getDate() + retentionDays); - - await service.log(auditEntry); - - const createCall = mockAuditRepo.create.mock.calls[0][0]; - expect(createCall.retentionUntil).toBeInstanceOf(Date); - expect(createCall.retentionUntil.getDate()).toBe(expectedRetentionDate.getDate()); - }); - }); - - describe('logAuth', () => { - const authParams = { - action: AuditAction.LOGIN, - userId: 'user-1', - userEmail: 'test@example.com', - ipAddress: '127.0.0.1', - userAgent: 'TestAgent', - metadata: { sessionId: 'session-1' }, - }; - - beforeEach(() => { - mockAuditRepo.create.mockReturnValue({} as AuditLog); - mockAuditRepo.save.mockResolvedValue({} as AuditLog); - }); - - it('should log authentication event with correct category', async () => { - await service.logAuth( - authParams.action, - authParams.userId, - authParams.userEmail, - authParams.ipAddress, - authParams.userAgent, - authParams.metadata, - ); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - userId: authParams.userId, - userEmail: authParams.userEmail, - action: authParams.action, - category: AuditCategory.AUTHENTICATION, - severity: AuditSeverity.INFO, - ipAddress: authParams.ipAddress, - userAgent: authParams.userAgent, - metadata: authParams.metadata, - }), - ); - }); - - it('should handle null userId and userEmail', async () => { - await service.logAuth(AuditAction.LOGIN_FAILED, null, null, '127.0.0.1', 'TestAgent'); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - userId: undefined, - userEmail: undefined, - action: AuditAction.LOGIN_FAILED, - category: AuditCategory.AUTHENTICATION, - }), - ); - }); - - it('should use provided severity', async () => { - await service.logAuth( - AuditAction.LOGIN_FAILED, - 'user-1', - 'test@example.com', - '127.0.0.1', - 'TestAgent', - undefined, - AuditSeverity.WARNING, - ); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - severity: AuditSeverity.WARNING, - }), - ); - }); - }); - - describe('logDataChange', () => { - const dataChangeParams = { - action: AuditAction.UPDATE, - userId: 'user-1', - userEmail: 'test@example.com', - entityType: 'User', - entityId: 'user-1', - oldValues: { name: 'Old Name' }, - newValues: { name: 'New Name' }, - ipAddress: '127.0.0.1', - description: 'User profile updated', - }; - - beforeEach(() => { - mockAuditRepo.create.mockReturnValue({} as AuditLog); - mockAuditRepo.save.mockResolvedValue({} as AuditLog); - }); - - it('should log data change event with correct category', async () => { - await service.logDataChange( - dataChangeParams.action, - dataChangeParams.userId, - dataChangeParams.userEmail, - dataChangeParams.entityType, - dataChangeParams.entityId, - dataChangeParams.oldValues, - dataChangeParams.newValues, - dataChangeParams.ipAddress, - dataChangeParams.description, - ); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - userId: dataChangeParams.userId, - userEmail: dataChangeParams.userEmail, - action: dataChangeParams.action, - category: AuditCategory.DATA_MODIFICATION, - severity: AuditSeverity.INFO, - entityType: dataChangeParams.entityType, - entityId: dataChangeParams.entityId, - oldValues: dataChangeParams.oldValues, - newValues: dataChangeParams.newValues, - ipAddress: dataChangeParams.ipAddress, - description: dataChangeParams.description, - }), - ); - }); - }); - - describe('logApiAccess', () => { - const apiAccessParams = { - userId: 'user-1', - userEmail: 'test@example.com', - apiEndpoint: '/api/users', - httpMethod: 'GET', - statusCode: 200, - responseTimeMs: 150, - ipAddress: '127.0.0.1', - userAgent: 'TestAgent', - requestId: 'req-123', - }; - - beforeEach(() => { - mockAuditRepo.create.mockReturnValue({} as AuditLog); - mockAuditRepo.save.mockResolvedValue({} as AuditLog); - }); - - it('should log API access with INFO severity for 2xx status', async () => { - await service.logApiAccess( - apiAccessParams.userId, - apiAccessParams.userEmail, - apiAccessParams.apiEndpoint, - apiAccessParams.httpMethod, - apiAccessParams.statusCode, - apiAccessParams.responseTimeMs, - apiAccessParams.ipAddress, - apiAccessParams.userAgent, - apiAccessParams.requestId, - ); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - userId: apiAccessParams.userId, - userEmail: apiAccessParams.userEmail, - action: AuditAction.API_CALLED, - category: AuditCategory.DATA_ACCESS, - severity: AuditSeverity.INFO, - apiEndpoint: apiAccessParams.apiEndpoint, - httpMethod: apiAccessParams.httpMethod, - statusCode: apiAccessParams.statusCode, - responseTimeMs: apiAccessParams.responseTimeMs, - ipAddress: apiAccessParams.ipAddress, - userAgent: apiAccessParams.userAgent, - requestId: apiAccessParams.requestId, - }), - ); - }); - - it('should log API access with WARNING severity for 4xx status', async () => { - await service.logApiAccess( - apiAccessParams.userId, - apiAccessParams.userEmail, - apiAccessParams.apiEndpoint, - apiAccessParams.httpMethod, - 404, - apiAccessParams.responseTimeMs, - apiAccessParams.ipAddress, - apiAccessParams.userAgent, - ); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - severity: AuditSeverity.WARNING, - statusCode: 404, - }), - ); - }); - - it('should log API access with ERROR severity for 5xx status', async () => { - await service.logApiAccess( - apiAccessParams.userId, - apiAccessParams.userEmail, - apiAccessParams.apiEndpoint, - apiAccessParams.httpMethod, - 500, - apiAccessParams.responseTimeMs, - apiAccessParams.ipAddress, - apiAccessParams.userAgent, - ); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - severity: AuditSeverity.ERROR, - statusCode: 500, - }), - ); - }); - - it('should handle null userId and userEmail', async () => { - await service.logApiAccess( - null, - null, - apiAccessParams.apiEndpoint, - apiAccessParams.httpMethod, - apiAccessParams.statusCode, - apiAccessParams.responseTimeMs, - apiAccessParams.ipAddress, - apiAccessParams.userAgent, - ); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - userId: undefined, - userEmail: undefined, - }), - ); - }); - }); - - describe('logSecurityEvent', () => { - const securityParams = { - action: AuditAction.SECURITY_ALERT, - userId: 'user-1', - userEmail: 'test@example.com', - ipAddress: '127.0.0.1', - userAgent: 'TestAgent', - description: 'Suspicious activity detected', - metadata: { threatLevel: 'high' }, - }; - - beforeEach(() => { - mockAuditRepo.create.mockReturnValue({} as AuditLog); - mockAuditRepo.save.mockResolvedValue({} as AuditLog); - }); - - it('should log security event with WARNING severity', async () => { - await service.logSecurityEvent( - securityParams.action, - securityParams.userId, - securityParams.userEmail, - securityParams.ipAddress, - securityParams.userAgent, - securityParams.description, - securityParams.metadata, - ); - - expect(mockAuditRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - userId: securityParams.userId, - userEmail: securityParams.userEmail, - action: securityParams.action, - category: AuditCategory.SECURITY, - severity: AuditSeverity.WARNING, - ipAddress: securityParams.ipAddress, - userAgent: securityParams.userAgent, - description: securityParams.description, - metadata: securityParams.metadata, - }), - ); - }); - }); - - describe('search', () => { - const mockQueryBuilder = createMockQueryBuilder(); - const mockLogs = [ - { id: 'log-1', action: AuditAction.LOGIN }, - { id: 'log-2', action: AuditAction.LOGOUT }, - ]; - - beforeEach(() => { - mockAuditRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any); - mockQueryBuilder.getCount.mockResolvedValue(2); - mockQueryBuilder.getMany.mockResolvedValue(mockLogs as AuditLog[]); - }); - - it('should search audit logs with filters', async () => { - const filters = { - userId: 'user-1', - actions: [AuditAction.LOGIN], - startDate: new Date('2024-01-01'), - endDate: new Date('2024-01-31'), - }; - - const result = await service.search(filters, 1, 10); - - expect(result).toEqual({ - logs: mockLogs, - total: 2, - page: 1, - limit: 10, - totalPages: 1, - }); - - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.userId = :userId', { - userId: 'user-1', - }); - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.action IN (:...actions)', { - actions: [AuditAction.LOGIN], - }); - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( - 'audit.timestamp BETWEEN :startDate AND :endDate', - { startDate: filters.startDate, endDate: filters.endDate }, - ); - }); - - it('should apply pagination correctly', async () => { - await service.search({}, 2, 20); - - expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20); // (page-1) * limit - expect(mockQueryBuilder.take).toHaveBeenCalledWith(20); - }); - - it('should handle empty filters', async () => { - await service.search({}); - - expect(mockQueryBuilder.andWhere).not.toHaveBeenCalled(); - }); - }); - - describe('findAll', () => { - const mockLogs = [ - { id: 'log-1', timestamp: new Date() }, - { id: 'log-2', timestamp: new Date() }, - ]; - - beforeEach(() => { - mockAuditRepo.find.mockResolvedValue(mockLogs as AuditLog[]); - }); - - it('should return all logs ordered by timestamp desc', async () => { - const result = await service.findAll(50); - - expect(result).toEqual(mockLogs); - expect(mockAuditRepo.find).toHaveBeenCalledWith({ - order: { timestamp: 'DESC' }, - take: 50, - }); - }); - - it('should use default limit of 100', async () => { - await service.findAll(); - - expect(mockAuditRepo.find).toHaveBeenCalledWith({ - order: { timestamp: 'DESC' }, - take: 100, - }); - }); - }); - - describe('findByUser', () => { - const userId = 'user-1'; - const mockLogs = [{ id: 'log-1', userId }]; - - beforeEach(() => { - mockAuditRepo.find.mockResolvedValue(mockLogs as AuditLog[]); - }); - - it('should find logs by user ID', async () => { - const result = await service.findByUser(userId, 25); - - expect(result).toEqual(mockLogs); - expect(mockAuditRepo.find).toHaveBeenCalledWith({ - where: { userId }, - order: { timestamp: 'DESC' }, - take: 25, - }); - }); - }); - - describe('findByAction', () => { - const action = AuditAction.LOGIN; - const mockLogs = [{ id: 'log-1', action }]; - - beforeEach(() => { - mockAuditRepo.find.mockResolvedValue(mockLogs as AuditLog[]); - }); - - it('should find logs by action', async () => { - const result = await service.findByAction(action, 30); - - expect(result).toEqual(mockLogs); - expect(mockAuditRepo.find).toHaveBeenCalledWith({ - where: { action }, - order: { timestamp: 'DESC' }, - take: 30, - }); - }); - }); - - describe('findByEntity', () => { - const entityType = 'User'; - const entityId = 'user-1'; - const mockLogs = [{ id: 'log-1', entityType, entityId }]; - - beforeEach(() => { - mockAuditRepo.find.mockResolvedValue(mockLogs as AuditLog[]); - }); - - it('should find logs by entity', async () => { - const result = await service.findByEntity(entityType, entityId, 40); - - expect(result).toEqual(mockLogs); - expect(mockAuditRepo.find).toHaveBeenCalledWith({ - where: { entityType, entityId }, - order: { timestamp: 'DESC' }, - take: 40, - }); - }); - }); - - describe('findByIpAddress', () => { - const ipAddress = '127.0.0.1'; - const mockLogs = [{ id: 'log-1', ipAddress }]; - - beforeEach(() => { - mockAuditRepo.find.mockResolvedValue(mockLogs as AuditLog[]); - }); - - it('should find logs by IP address', async () => { - const result = await service.findByIpAddress(ipAddress, 35); - - expect(result).toEqual(mockLogs); - expect(mockAuditRepo.find).toHaveBeenCalledWith({ - where: { ipAddress }, - order: { timestamp: 'DESC' }, - take: 35, - }); - }); - }); - - describe('findByDateRange', () => { - const startDate = new Date('2024-01-01'); - const endDate = new Date('2024-01-31'); - const mockLogs = [{ id: 'log-1', timestamp: new Date('2024-01-15') }]; - - beforeEach(() => { - mockAuditRepo.find.mockResolvedValue(mockLogs as AuditLog[]); - }); - - it('should find logs by date range', async () => { - const result = await service.findByDateRange(startDate, endDate, 1000); - - expect(result).toEqual(mockLogs); - expect(mockAuditRepo.find).toHaveBeenCalledWith({ - where: { - timestamp: expect.any(Object), // Between operator - }, - order: { timestamp: 'DESC' }, - take: 1000, - }); - }); - }); - - describe('generateReport', () => { - const startDate = new Date('2024-01-01'); - const endDate = new Date('2024-01-31'); - - const mockCategoryStats = [ - { category: AuditCategory.AUTHENTICATION, count: '10' }, - { category: AuditCategory.DATA_MODIFICATION, count: '5' }, - ]; - - const mockActionStats = [ - { action: AuditAction.LOGIN, count: '8' }, - { action: AuditAction.LOGOUT, count: '2' }, - ]; - - const mockSeverityStats = [ - { severity: AuditSeverity.INFO, count: '12' }, - { severity: AuditSeverity.WARNING, count: '3' }, - ]; - - const mockTopUsers = [{ userId: 'user-1', userEmail: 'test@example.com', count: '5' }]; - - const mockTopEndpoints = [{ endpoint: '/api/users', count: '10' }]; - - const mockFailedActions = [{ action: AuditAction.API_CALLED, count: '2' }]; - - beforeEach(() => { - // Mock the main query builder - const mainQueryBuilder = createMockQueryBuilder(); - mainQueryBuilder.where.mockReturnThis(); - mainQueryBuilder.getCount.mockResolvedValue(15); - - // Mock category stats query - const categoryQueryBuilder = createMockQueryBuilder(); - categoryQueryBuilder.select.mockReturnThis(); - categoryQueryBuilder.addSelect.mockReturnThis(); - categoryQueryBuilder.where.mockReturnThis(); - categoryQueryBuilder.groupBy.mockReturnThis(); - categoryQueryBuilder.getRawMany.mockResolvedValue(mockCategoryStats); - - // Mock action stats query - const actionQueryBuilder = createMockQueryBuilder(); - actionQueryBuilder.select.mockReturnThis(); - actionQueryBuilder.addSelect.mockReturnThis(); - actionQueryBuilder.where.mockReturnThis(); - actionQueryBuilder.groupBy.mockReturnThis(); - actionQueryBuilder.getRawMany.mockResolvedValue(mockActionStats); - - // Mock severity stats query - const severityQueryBuilder = createMockQueryBuilder(); - severityQueryBuilder.select.mockReturnThis(); - severityQueryBuilder.addSelect.mockReturnThis(); - severityQueryBuilder.where.mockReturnThis(); - severityQueryBuilder.groupBy.mockReturnThis(); - severityQueryBuilder.getRawMany.mockResolvedValue(mockSeverityStats); - - // Mock top users query - const topUsersQueryBuilder = createMockQueryBuilder(); - topUsersQueryBuilder.select.mockReturnThis(); - topUsersQueryBuilder.addSelect.mockReturnThis(); - topUsersQueryBuilder.where.mockReturnThis(); - topUsersQueryBuilder.andWhere.mockReturnThis(); - topUsersQueryBuilder.groupBy.mockReturnThis(); - topUsersQueryBuilder.addGroupBy.mockReturnThis(); - topUsersQueryBuilder.orderBy.mockReturnThis(); - topUsersQueryBuilder.limit.mockReturnThis(); - topUsersQueryBuilder.getRawMany.mockResolvedValue(mockTopUsers); - - // Mock top endpoints query - const topEndpointsQueryBuilder = createMockQueryBuilder(); - topEndpointsQueryBuilder.select.mockReturnThis(); - topEndpointsQueryBuilder.addSelect.mockReturnThis(); - topEndpointsQueryBuilder.where.mockReturnThis(); - topEndpointsQueryBuilder.andWhere.mockReturnThis(); - topEndpointsQueryBuilder.groupBy.mockReturnThis(); - topEndpointsQueryBuilder.orderBy.mockReturnThis(); - topEndpointsQueryBuilder.limit.mockReturnThis(); - topEndpointsQueryBuilder.getRawMany.mockResolvedValue(mockTopEndpoints); - - // Mock failed actions query - const failedActionsQueryBuilder = createMockQueryBuilder(); - failedActionsQueryBuilder.select.mockReturnThis(); - failedActionsQueryBuilder.addSelect.mockReturnThis(); - failedActionsQueryBuilder.where.mockReturnThis(); - failedActionsQueryBuilder.andWhere.mockReturnThis(); - failedActionsQueryBuilder.groupBy.mockReturnThis(); - failedActionsQueryBuilder.orderBy.mockReturnThis(); - failedActionsQueryBuilder.limit.mockReturnThis(); - failedActionsQueryBuilder.getRawMany.mockResolvedValue(mockFailedActions); - - // Set up the mock to return different query builders for different calls - mockAuditRepo.createQueryBuilder - .mockReturnValueOnce(mainQueryBuilder as any) - .mockReturnValueOnce(categoryQueryBuilder as any) - .mockReturnValueOnce(actionQueryBuilder as any) - .mockReturnValueOnce(severityQueryBuilder as any) - .mockReturnValueOnce(topUsersQueryBuilder as any) - .mockReturnValueOnce(topEndpointsQueryBuilder as any) - .mockReturnValueOnce(failedActionsQueryBuilder as any); - }); - - it('should generate comprehensive audit report', async () => { - const result = await service.generateReport(startDate, endDate); - - expect(result).toEqual({ - period: { start: startDate, end: endDate }, - totalEvents: 15, - eventsByCategory: { - [AuditCategory.AUTHENTICATION]: 10, - [AuditCategory.DATA_MODIFICATION]: 5, - }, - eventsByAction: { - [AuditAction.LOGIN]: 8, - [AuditAction.LOGOUT]: 2, - }, - eventsBySeverity: { - [AuditSeverity.INFO]: 12, - [AuditSeverity.WARNING]: 3, - }, - topUsers: [ - { - userId: 'user-1', - userEmail: 'test@example.com', - action: AuditAction.LOGIN, - category: AuditCategory.AUTHENTICATION, - severity: AuditSeverity.INFO, - description: 'User logged in', - ipAddress: '127.0.0.1', - userAgent: 'TestAgent', - metadata: { sessionId: 'session-1' }, - }; - const mockSavedLog = { - id: 'log-1', - ...auditEntry, - timestamp: new Date(), - retentionUntil: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), - }; - beforeEach(() => { - mockAuditRepo.create.mockReturnValue(mockSavedLog as AuditLog); - mockAuditRepo.save.mockResolvedValue(mockSavedLog as AuditLog); - }); - it('should create and save audit log entry', async () => { - const result = await service.log(auditEntry); - expect(result).toEqual(mockSavedLog); - expect(mockAuditRepo.create).toHaveBeenCalledWith({ - ...auditEntry, - severity: AuditSeverity.INFO, - retentionUntil: expect.any(Date), - }); - expect(mockAuditRepo.save).toHaveBeenCalledWith(mockSavedLog); - }); - it('should use default severity when not provided', async () => { - const entryWithoutSeverity = { ...auditEntry }; - delete entryWithoutSeverity.severity; - await service.log(entryWithoutSeverity); - expect(mockAuditRepo.create).toHaveBeenCalledWith(expect.objectContaining({ - severity: AuditSeverity.INFO, - })); - }); - it('should handle save errors gracefully', async () => { - const error = new Error('Database error'); - mockAuditRepo.save.mockRejectedValue(error); - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - const result = await service.log(auditEntry); - expect(result).toEqual(mockSavedLog); // Returns created log even on error - expect(consoleSpy).toHaveBeenCalledWith('Failed to create audit log:', error); - consoleSpy.mockRestore(); - }); - it('should set retention date correctly', async () => { - const retentionDays = 365; - const expectedRetentionDate = new Date(); - expectedRetentionDate.setDate(expectedRetentionDate.getDate() + retentionDays); - await service.log(auditEntry); - const createCall = mockAuditRepo.create.mock.calls[0][0]; - expect(createCall.retentionUntil).toBeInstanceOf(Date); - expect(createCall.retentionUntil.getDate()).toBe(expectedRetentionDate.getDate()); - }); - }); - }); -}); diff --git a/src/audit-log/audit-log.service.ts b/src/audit-log/audit-log.service.ts index 89c2caf6..83c38c43 100644 --- a/src/audit-log/audit-log.service.ts +++ b/src/audit-log/audit-log.service.ts @@ -242,434 +242,65 @@ export class AuditLogService { if (filters.userId) { queryBuilder.andWhere('audit.userId = :userId', { userId: filters.userId }); } - /** - * Log authentication event - */ - async logAuth(action: AuditAction, userId: string | null, userEmail: string | null, ipAddress: string, userAgent: string, metadata?: Record, severity: AuditSeverity = AuditSeverity.INFO): Promise { - return this.log({ - userId: userId || undefined, - userEmail: userEmail || undefined, - action, - category: AuditCategory.AUTHENTICATION, - severity, - ipAddress, - userAgent, - metadata, - }); - } - /** - * Log data modification - */ - async logDataChange(action: AuditAction, userId: string, userEmail: string, entityType: string, entityId: string, oldValues: Record, newValues: Record, ipAddress?: string, description?: string): Promise { - return this.log({ - userId, - userEmail, - action, - category: AuditCategory.DATA_MODIFICATION, - severity: AuditSeverity.INFO, - entityType, - entityId, - description, - oldValues, - newValues, - ipAddress, - }); - } - /** - * Log API access - */ - async logApiAccess(userId: string | null, userEmail: string | null, apiEndpoint: string, httpMethod: string, statusCode: number, responseTimeMs: number, ipAddress: string, userAgent: string, requestId?: string): Promise { - const severity = statusCode >= 500 - ? AuditSeverity.ERROR - : statusCode >= 400 - ? AuditSeverity.WARNING - : AuditSeverity.INFO; - return this.log({ - userId: userId || undefined, - userEmail: userEmail || undefined, - action: AuditAction.API_CALLED, - category: AuditCategory.DATA_ACCESS, - severity, - apiEndpoint, - httpMethod, - statusCode, - responseTimeMs, - ipAddress, - userAgent, - requestId, - }); - } - /** - * Log security event - */ - async logSecurityEvent(action: AuditAction, userId: string | null, userEmail: string | null, ipAddress: string, userAgent: string, description: string, metadata?: Record): Promise { - return this.log({ - userId: userId || undefined, - userEmail: userEmail || undefined, - action, - category: AuditCategory.SECURITY, - severity: AuditSeverity.WARNING, - ipAddress, - userAgent, - description, - metadata, - }); + + if (filters.userEmail) { + queryBuilder.andWhere('audit.userEmail = :userEmail', { userEmail: filters.userEmail }); } - /** - * Search audit logs with filters - */ - async search(filters: AuditLogSearchFilters, page: number = 1, limit: number = 50): Promise { - const queryBuilder = this.auditRepo.createQueryBuilder('audit'); - // Apply filters - if (filters.userId) { - queryBuilder.andWhere('audit.userId = :userId', { userId: filters.userId }); - } - if (filters.userEmail) { - queryBuilder.andWhere('audit.userEmail = :userEmail', { userEmail: filters.userEmail }); - } - if (filters.actions && filters.actions.length > 0) { - queryBuilder.andWhere('audit.action IN (:...actions)', { actions: filters.actions }); - } - if (filters.categories && filters.categories.length > 0) { - queryBuilder.andWhere('audit.category IN (:...categories)', { - categories: filters.categories, - }); - } - if (filters.severities && filters.severities.length > 0) { - queryBuilder.andWhere('audit.severity IN (:...severities)', { - severities: filters.severities, - }); - } - if (filters.entityType) { - queryBuilder.andWhere('audit.entityType = :entityType', { entityType: filters.entityType }); - } - if (filters.entityId) { - queryBuilder.andWhere('audit.entityId = :entityId', { entityId: filters.entityId }); - } - if (filters.ipAddress) { - queryBuilder.andWhere('audit.ipAddress = :ipAddress', { ipAddress: filters.ipAddress }); - } - if (filters.sessionId) { - queryBuilder.andWhere('audit.sessionId = :sessionId', { sessionId: filters.sessionId }); - } - if (filters.tenantId) { - queryBuilder.andWhere('audit.tenantId = :tenantId', { tenantId: filters.tenantId }); - } - if (filters.apiEndpoint) { - queryBuilder.andWhere('audit.apiEndpoint LIKE :apiEndpoint', { - apiEndpoint: `%${filters.apiEndpoint}%`, - }); - } - if (filters.httpMethod) { - queryBuilder.andWhere('audit.httpMethod = :httpMethod', { httpMethod: filters.httpMethod }); - } - if (filters.statusCode) { - queryBuilder.andWhere('audit.statusCode = :statusCode', { statusCode: filters.statusCode }); - } - if (filters.startDate && filters.endDate) { - queryBuilder.andWhere('audit.timestamp BETWEEN :startDate AND :endDate', { - startDate: filters.startDate, - endDate: filters.endDate, - }); - } - else if (filters.startDate) { - queryBuilder.andWhere('audit.timestamp >= :startDate', { startDate: filters.startDate }); - } - else if (filters.endDate) { - queryBuilder.andWhere('audit.timestamp <= :endDate', { endDate: filters.endDate }); - } - // Order by timestamp desc - queryBuilder.orderBy('audit.timestamp', 'DESC'); - // Get total count - const total = await queryBuilder.getCount(); - // Apply pagination - const skip = (page - 1) * limit; - queryBuilder.skip(skip).take(limit); - const logs = await queryBuilder.getMany(); - return { - logs, - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }; + if (filters.actions && filters.actions.length > 0) { + queryBuilder.andWhere('audit.action IN (:...actions)', { actions: filters.actions }); } - /** - * Find all logs (with limit) - */ - async findAll(limit: number = 100): Promise { - return this.auditRepo.find({ - order: { timestamp: 'DESC' }, - take: limit, - }); + if (filters.categories && filters.categories.length > 0) { + queryBuilder.andWhere('audit.category IN (:...categories)', { + categories: filters.categories, + }); } - /** - * Find logs by user - */ - async findByUser(userId: string, limit: number = 100): Promise { - return this.auditRepo.find({ - where: { userId }, - order: { timestamp: 'DESC' }, - take: limit, - }); + if (filters.severities && filters.severities.length > 0) { + queryBuilder.andWhere('audit.severity IN (:...severities)', { + severities: filters.severities, + }); } - /** - * Find logs by action - */ - async findByAction(action: AuditAction, limit: number = 100): Promise { - return this.auditRepo.find({ - where: { action }, - order: { timestamp: 'DESC' }, - take: limit, - }); + if (filters.entityType) { + queryBuilder.andWhere('audit.entityType = :entityType', { entityType: filters.entityType }); } - /** - * Find logs by entity - */ - async findByEntity(entityType: string, entityId: string, limit: number = 100): Promise { - return this.auditRepo.find({ - where: { entityType, entityId }, - order: { timestamp: 'DESC' }, - take: limit, - }); + if (filters.entityId) { + queryBuilder.andWhere('audit.entityId = :entityId', { entityId: filters.entityId }); } - /** - * Find logs by IP address - */ - async findByIpAddress(ipAddress: string, limit: number = 100): Promise { - return this.auditRepo.find({ - where: { ipAddress }, - order: { timestamp: 'DESC' }, - take: limit, - }); + if (filters.ipAddress) { + queryBuilder.andWhere('audit.ipAddress = :ipAddress', { ipAddress: filters.ipAddress }); } - /** - * Find logs by date range - */ - async findByDateRange(startDate: Date, endDate: Date, limit: number = 1000): Promise { - return this.auditRepo.find({ - where: { - timestamp: Between(startDate, endDate), - }, - order: { timestamp: 'DESC' }, - take: limit, - }); + if (filters.sessionId) { + queryBuilder.andWhere('audit.sessionId = :sessionId', { sessionId: filters.sessionId }); } - /** - * Generate audit report - */ - async generateReport(startDate: Date, endDate: Date): Promise { - const queryBuilder = this.auditRepo.createQueryBuilder('audit'); - queryBuilder.where('audit.timestamp BETWEEN :startDate AND :endDate', { - startDate, - endDate, - }); - const totalEvents = await queryBuilder.getCount(); - // Events by category - const categoryStats = await this.auditRepo - .createQueryBuilder('audit') - .select('audit.category', 'category') - .addSelect('COUNT(*)', 'count') - .where('audit.timestamp BETWEEN :startDate AND :endDate', { startDate, endDate }) - .groupBy('audit.category') - .getRawMany(); - const eventsByCategory: Record = {}; - categoryStats.forEach((stat) => { - eventsByCategory[stat.category] = parseInt(stat.count, 10); - }); - // Events by action - const actionStats = await this.auditRepo - .createQueryBuilder('audit') - .select('audit.action', 'action') - .addSelect('COUNT(*)', 'count') - .where('audit.timestamp BETWEEN :startDate AND :endDate', { startDate, endDate }) - .groupBy('audit.action') - .getRawMany(); - const eventsByAction: Record = {}; - actionStats.forEach((stat) => { - eventsByAction[stat.action] = parseInt(stat.count, 10); - }); - // Events by severity - const severityStats = await this.auditRepo - .createQueryBuilder('audit') - .select('audit.severity', 'severity') - .addSelect('COUNT(*)', 'count') - .where('audit.timestamp BETWEEN :startDate AND :endDate', { startDate, endDate }) - .groupBy('audit.severity') - .getRawMany(); - const eventsBySeverity: Record = {}; - severityStats.forEach((stat) => { - eventsBySeverity[stat.severity] = parseInt(stat.count, 10); - }); - // Top users - const topUsers = await this.auditRepo - .createQueryBuilder('audit') - .select('audit.userId', 'userId') - .addSelect('audit.userEmail', 'userEmail') - .addSelect('COUNT(*)', 'count') - .where('audit.timestamp BETWEEN :startDate AND :endDate', { startDate, endDate }) - .andWhere('audit.userId IS NOT NULL') - .groupBy('audit.userId') - .addGroupBy('audit.userEmail') - .orderBy('count', 'DESC') - .limit(10) - .getRawMany(); - // Top endpoints - const topEndpoints = await this.auditRepo - .createQueryBuilder('audit') - .select('audit.apiEndpoint', 'endpoint') - .addSelect('COUNT(*)', 'count') - .where('audit.timestamp BETWEEN :startDate AND :endDate', { startDate, endDate }) - .andWhere('audit.apiEndpoint IS NOT NULL') - .groupBy('audit.apiEndpoint') - .orderBy('count', 'DESC') - .limit(10) - .getRawMany(); - // Failed actions (status code >= 400) - const failedActions = await this.auditRepo - .createQueryBuilder('audit') - .select('audit.action', 'action') - .addSelect('COUNT(*)', 'count') - .where('audit.timestamp BETWEEN :startDate AND :endDate', { startDate, endDate }) - .andWhere('audit.statusCode >= 400') - .groupBy('audit.action') - .orderBy('count', 'DESC') - .limit(10) - .getRawMany(); - return { - period: { start: startDate, end: endDate }, - totalEvents, - eventsByCategory, - eventsByAction, - eventsBySeverity, - topUsers: topUsers.map((u) => ({ - userId: u.userId, - userEmail: u.userEmail || 'Unknown', - count: parseInt(u.count, 10), - })), - topEndpoints: topEndpoints.map((e) => ({ - endpoint: e.endpoint, - count: parseInt(e.count, 10), - })), - failedActions: failedActions.map((f) => ({ - action: f.action, - count: parseInt(f.count, 10), - })), - }; + if (filters.tenantId) { + queryBuilder.andWhere('audit.tenantId = :tenantId', { tenantId: filters.tenantId }); } - /** - * Apply retention policy - delete old logs - */ - async applyRetentionPolicy(): Promise { - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - this.retentionDays); - const result = await this.auditRepo - .createQueryBuilder() - .delete() - .where('timestamp < :cutoffDate', { cutoffDate }) - .orWhere('retentionUntil < NOW()') - .execute(); - const deletedCount = result.affected || 0; - this.logger.log(`Applied retention policy: deleted ${deletedCount} old audit logs`); - return deletedCount; + if (filters.apiEndpoint) { + queryBuilder.andWhere('audit.apiEndpoint LIKE :apiEndpoint', { + apiEndpoint: `%${filters.apiEndpoint}%`, + }); } - /** - * Export logs to JSON - */ - async exportToJson(filters: AuditLogSearchFilters): Promise { - const { logs } = await this.search(filters, 1, 10000); - return JSON.stringify(logs, null, 2); + if (filters.httpMethod) { + queryBuilder.andWhere('audit.httpMethod = :httpMethod', { httpMethod: filters.httpMethod }); } - /** - * Export logs to CSV - */ - async exportToCsv(filters: AuditLogSearchFilters): Promise { - const { logs } = await this.search(filters, 1, 10000); - const headers = [ - 'timestamp', - 'userId', - 'userEmail', - 'action', - 'category', - 'severity', - 'entityType', - 'entityId', - 'description', - 'ipAddress', - 'userAgent', - 'apiEndpoint', - 'httpMethod', - 'statusCode', - ]; - const rows = logs.map((log) => [ - log.timestamp.toISOString(), - log.userId || '', - log.userEmail || '', - log.action, - log.category, - log.severity, - log.entityType || '', - log.entityId || '', - log.description || '', - log.ipAddress || '', - log.userAgent || '', - log.apiEndpoint || '', - log.httpMethod || '', - log.statusCode || '', - ]); - const escapeCsv = (value: string | number) => { - const str = String(value); - if (str.includes(',') || str.includes('"') || str.includes('\n')) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; - }; - const csvContent = [headers.join(','), ...rows.map((row) => row.map(escapeCsv).join(','))].join('\n'); - return csvContent; + if (filters.statusCode) { + queryBuilder.andWhere('audit.statusCode = :statusCode', { statusCode: filters.statusCode }); } - /** - * Get statistics - */ - async getStatistics(): Promise<{ - totalLogs: number; - logsToday: number; - logsThisWeek: number; - logsThisMonth: number; - criticalEvents: number; - errorEvents: number; - }> { - const now = new Date(); - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - const [totalLogs, logsToday, logsThisWeek, logsThisMonth, criticalEvents, errorEvents] = await Promise.all([ - this.auditRepo.count(), - this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(today) } }), - this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(weekAgo) } }), - this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(monthAgo) } }), - this.auditRepo.count({ where: { severity: AuditSeverity.CRITICAL } }), - this.auditRepo.count({ where: { severity: AuditSeverity.ERROR } }), - ]); - return { - totalLogs, - logsToday, - logsThisWeek, - logsThisMonth, - criticalEvents, - errorEvents, - }; + if (filters.startDate && filters.endDate) { + queryBuilder.andWhere('audit.timestamp BETWEEN :startDate AND :endDate', { + startDate: filters.startDate, + endDate: filters.endDate, + }); + } else if (filters.startDate) { + queryBuilder.andWhere('audit.timestamp >= :startDate', { startDate: filters.startDate }); + } else if (filters.endDate) { + queryBuilder.andWhere('audit.timestamp <= :endDate', { endDate: filters.endDate }); } - // Order by timestamp desc queryBuilder.orderBy('audit.timestamp', 'DESC'); - // Get total count const total = await queryBuilder.getCount(); - - // Apply pagination const skip = (page - 1) * limit; queryBuilder.skip(skip).take(limit); - const logs = await queryBuilder.getMany(); return { diff --git a/src/audit-log/interceptors/audit-log.interceptor.ts b/src/audit-log/interceptors/audit-log.interceptor.ts deleted file mode 100644 index 47ebfd80..00000000 --- a/src/audit-log/interceptors/audit-log.interceptor.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common'; -import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { AuditLogService } from '../audit-log.service'; -import { AuditAction, AuditCategory, AuditSeverity } from '../enums/audit-action.enum'; -import { Request } from 'express'; - -interface IRequestWithUser extends Request { - user?: { - id: string; - email: string; - role?: string; - }; - requestId?: string; -} - -/** - * Intercepts audit Log request handling. - */ -@Injectable() -export class AuditLogInterceptor implements NestInterceptor { - private readonly logger = new Logger(AuditLogInterceptor.name); - - constructor(private readonly auditLogService: AuditLogService) {} - - /** - * Executes intercept. - * @param context The context. - * @param next The next. - * @returns The resulting observable. - */ - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context.switchToHttp().getRequest(); - const response = context.switchToHttp().getResponse(); - const startTime = Date.now(); - - const { method, path, ip, headers, user, requestId } = request; - - const userAgent = headers['user-agent'] || 'Unknown'; - const userId = user?.id || null; - const userEmail = user?.email || null; - - return next.handle().pipe( - tap({ - next: () => { - this.logRequest( - userId, - userEmail, - path, - method, - response.statusCode, - Date.now() - startTime, - ip, - userAgent, - requestId, - ); - }, - error: (error) => { - const statusCode = error.status || 500; - this.logRequest( - userId, - userEmail, - path, - method, - statusCode, - Date.now() - startTime, - ip, - userAgent, - requestId, - error.message, - ); - }, - }), - ); - } - - private async logRequest( - userId: string | null, - userEmail: string | null, - apiEndpoint: string, - httpMethod: string, - statusCode: number, - responseTimeMs: number, - ipAddress: string, - userAgent: string, - requestId?: string, - errorMessage?: string, - ): Promise { - try { - // Skip logging for health checks and static assets - if (this.shouldSkipLogging(apiEndpoint)) { - return; - } - - const severity = - statusCode >= 500 - ? AuditSeverity.ERROR - : statusCode >= 400 - ? AuditSeverity.WARNING - : AuditSeverity.INFO; - - await this.auditLogService.log({ - userId: userId || undefined, - userEmail: userEmail || undefined, - action: AuditAction.API_CALLED, - category: AuditCategory.DATA_ACCESS, - severity, - apiEndpoint, - httpMethod, - statusCode, - responseTimeMs, - ipAddress, - userAgent, - requestId, - description: errorMessage || `${httpMethod} ${apiEndpoint} - ${statusCode}`, - }); - } catch (error) { - this.logger.error('Failed to log audit entry:', error); - } -} diff --git a/src/audit-log/interceptors/sensitive-operation.interceptor.ts b/src/audit-log/interceptors/sensitive-operation.interceptor.ts deleted file mode 100644 index 60436a4d..00000000 --- a/src/audit-log/interceptors/sensitive-operation.interceptor.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - Injectable, - NestInterceptor, - ExecutionContext, - CallHandler, - Logger, -} from '@nestjs/common'; -import { Observable } from 'rxjs'; -import { tap, catchError } from 'rxjs/operators'; -import { Reflector } from '@nestjs/core'; -import { Request } from 'express'; -import { SensitiveOperationsService } from '../services/sensitive-operations.service'; -import { SENSITIVE_OPERATION_KEY, ISensitiveOperationOptions } from '../decorators/sensitive-operation.decorator'; -import { AuditSeverity } from '../enums/audit-action.enum'; - -interface IRequestWithUser extends Request { - user?: { - id: string; - email: string; - role?: string; - }; - requestId?: string; -} - -@Injectable() -export class SensitiveOperationInterceptor implements NestInterceptor { - private readonly logger = new Logger(SensitiveOperationInterceptor.name); - - constructor( - private readonly reflector: Reflector, - private readonly sensitiveOpsService: SensitiveOperationsService, - ) {} - - intercept(context: ExecutionContext, next: CallHandler): Observable { - const options = this.reflector.get( - SENSITIVE_OPERATION_KEY, - context.getHandler(), - ); - - if (!options) { - return next.handle(); - } - - const request = context.switchToHttp().getRequest(); - const response = context.switchToHttp().getResponse(); - const startTime = Date.now(); - - const { method, path, ip, headers, user, requestId, body, params } = request; - - const userAgent = headers['user-agent'] || 'Unknown'; - const userId = user?.id || null; - const userEmail = user?.email || null; - - // Extract entity ID from params if specified - let entityId = options.entityIdParam ? params[options.entityIdParam] : 'unknown'; - - return next.handle().pipe( - tap({ - next: (data) => { - this.logSensitiveOperation( - userId, - userEmail, - options, - entityId, - ip, - userAgent, - requestId, - body, - data, - response.statusCode, - Date.now() - startTime, - ); - }, - error: (error) => { - this.logSensitiveOperation( - userId, - userEmail, - options, - entityId, - ip, - userAgent, - requestId, - body, - null, - error.status || 500, - Date.now() - startTime, - error.message, - ); - }, - }), - catchError((error) => { - throw error; - }), - ); - } - - private async logSensitiveOperation( - userId: string | null, - userEmail: string | null, - options: ISensitiveOperationOptions, - entityId: string, - ipAddress: string, - userAgent: string, - requestId: string | undefined, - requestBody: unknown, - responseData: unknown, - statusCode: number, - responseTimeMs: number, - errorMessage?: string, - ): Promise { - try { - if (!userId || !userEmail) { - this.logger.warn('Sensitive operation attempted without authentication'); - return; - } - - const oldValues = options.logOldValues ? this.extractOldValues(requestBody) : undefined; - const newValues = options.logNewValues ? this.extractNewValues(requestBody, responseData) : undefined; - - await this.sensitiveOpsService.logSensitiveOperation({ - userId, - userEmail, - action: options.action, - entityType: options.entityType, - entityId, - description: options.description || `${options.action} on ${options.entityType}`, - oldValues, - newValues, - ipAddress, - userAgent, - requestId, - metadata: { - statusCode, - responseTimeMs, - errorMessage, - method: requestBody ? 'POST/PUT/PATCH' : 'GET', - }, - }); - } catch (error) { - this.logger.error('Failed to log sensitive operation:', error); - } - } - - private extractOldValues(body: unknown): Record | undefined { - if (!body || typeof body !== 'object') { - return undefined; - } - - const bodyObj = body as Record; - const oldValues = bodyObj.oldValues || bodyObj.previous; - return oldValues as Record | undefined; - } - - private extractNewValues(body: unknown, response: unknown): Record | undefined { - if (response && typeof response === 'object') { - const responseObj = response as Record; - return (responseObj.data || responseObj) as Record; - } - - if (body && typeof body === 'object') { - const bodyObj = body as Record; - return (bodyObj.newValues || bodyObj) as Record; - } - - return undefined; - } -} diff --git a/src/audit-log/tests/audit-log.test.ts b/src/audit-log/tests/audit-log.test.ts deleted file mode 100644 index fab3fc62..00000000 --- a/src/audit-log/tests/audit-log.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AuditLogService } from '../audit-log.service'; -import { Repository } from 'typeorm'; -import { AuditLog } from '../audit-log.entity'; -import { ConfigService } from '@nestjs/config'; -describe('AuditLogService', () => { - let service: AuditLogService; - let repo: Repository; - let configService: ConfigService; - beforeEach(() => { - // Mock repository and config service - repo = {} as Repository; - configService = { - get: jest.fn().mockReturnValue(365), - } as unknown; - service = new AuditLogService(repo as unknown, configService); - }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts deleted file mode 100644 index ca05455b..00000000 --- a/src/auth/auth.controller.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { Throttle } from '@nestjs/throttler'; -import { THROTTLE } from '../common/constants/throttle.constants'; -import { Request } from 'express'; -import { AuthService } from './auth.service'; -import { RegisterDto, LoginDto, RefreshTokenDto, ForgotPasswordDto, ResetPasswordDto, ChangePasswordDto, VerifyEmailDto, } from './dto/auth.dto'; -import { JwtAuthGuard } from './guards/jwt-auth.guard'; -import { CurrentUser } from './decorators/current-user.decorator'; - -/** - * Exposes auth endpoints. - */ -@ApiTags('auth') -@Controller('auth') -export class AuthController { - constructor(private readonly authService: AuthService) {} - - /** - * Registers register. - * @param registerDto The request payload. - * @param req The req. - * @returns The operation result. - */ - @Post('register') - @Throttle({ default: THROTTLE.STRICT }) - @ApiOperation({ summary: 'Register a new user' }) - async register(@Body() registerDto: RegisterDto, @Req() req: Request): Promise { - const ipAddress = req.ip || req.socket.remoteAddress || 'unknown'; - const userAgent = req.headers['user-agent'] || 'unknown'; - return this.authService.register(registerDto, ipAddress, userAgent); - } - - /** - * Executes login. - * @param loginDto The request payload. - * @param req The req. - * @returns The operation result. - */ - @Post('login') - @Throttle({ default: THROTTLE.AUTH_LOGIN }) - @ApiOperation({ summary: 'Login user and get tokens' }) - async login(@Body() loginDto: LoginDto, @Req() req: Request): Promise { - const ipAddress = req.ip || req.socket.remoteAddress || 'unknown'; - const userAgent = req.headers['user-agent'] || 'unknown'; - return this.authService.login(loginDto, ipAddress, userAgent); - } - - /** - * Refreshes refresh. - * @param refreshTokenDto The request payload. - * @returns The operation result. - */ - @Post('refresh') - @Throttle({ default: THROTTLE.REFRESH }) - @ApiOperation({ summary: 'Refresh access token using refresh token' }) - async refresh(@Body() refreshTokenDto: RefreshTokenDto): Promise { - return this.authService.refreshToken(refreshTokenDto.refreshToken); - } - - /** - * Executes logout. - * @param user The user. - * @param req The req. - * @returns The operation result. - */ - @Post('logout') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Logout user (invalidate refresh token)' }) - async logout(@CurrentUser() user: any, @Req() req: Request): Promise { - const ipAddress = req.ip || req.socket.remoteAddress || 'unknown'; - const userAgent = req.headers['user-agent'] || 'unknown'; - return this.authService.logout(user.userId, user.sessionId, ipAddress, userAgent); - } - - /** - * Executes forgot Password. - * @param forgotPasswordDto The request payload. - * @returns The operation result. - */ - @Post('forgot-password') - @Throttle({ default: THROTTLE.AUTH_DEFAULT }) - @ApiOperation({ summary: 'Request a password reset link' }) - async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto): Promise { - return this.authService.forgotPassword(forgotPasswordDto.email); - } - - /** - * Resets password. - * @param resetPasswordDto The request payload. - * @returns The operation result. - */ - @Post('reset-password') - @Throttle({ default: THROTTLE.AUTH_DEFAULT }) - @ApiOperation({ summary: 'Reset password using token' }) - async resetPassword(@Body() resetPasswordDto: ResetPasswordDto): Promise { - return this.authService.resetPassword(resetPasswordDto); - } - - /** - * Executes change Password. - * @param user The user. - * @param changePasswordDto The request payload. - * @param req The req. - * @returns The operation result. - */ - @Post('change-password') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Change password for authenticated user' }) - async changePassword( - @CurrentUser() user: any, - @Body() changePasswordDto: ChangePasswordDto, - @Req() req: Request, - ): Promise { - const ipAddress = req.ip || req.socket.remoteAddress || 'unknown'; - const userAgent = req.headers['user-agent'] || 'unknown'; - return this.authService.changePassword(user.userId, changePasswordDto, ipAddress, userAgent); - } - - /** - * Validates email. - * @param verifyEmailDto The request payload. - * @returns The operation result. - */ - @Post('verify-email') - @Throttle({ default: THROTTLE.MODERATE }) - @ApiOperation({ summary: 'Verify email using token' }) - async verifyEmail(@Body() verifyEmailDto: VerifyEmailDto): Promise { - return this.authService.verifyEmail(verifyEmailDto.token); - } -} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts deleted file mode 100644 index a6a1da39..00000000 --- a/src/auth/auth.module.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Module } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { AuthService } from './auth.service'; -import { AuthController } from './auth.controller'; -import { UsersModule } from '../users/users.module'; -import { JwtStrategy } from './strategies/jwt.strategy'; -import { SessionModule } from '../session/session.module'; -import { TransactionService } from '../common/database/transaction.service'; -import { NotificationsModule } from '../notifications/notifications.module'; -import { AuditLogModule } from '../audit-log/audit-log.module'; -import { PasswordPolicyService } from './services/password-policy.service'; - -function parseJwtSecrets(raw: string): Record { - try { - const parsed = JSON.parse(raw) as unknown; - if (parsed && typeof parsed === 'object') { - return parsed as Record; - } - } - catch { - // ignore - } - return raw - .split(',') - .map((pair) => pair.trim()) - .filter(Boolean) - .reduce>((acc, pair) => { - const idx = pair.indexOf(':'); - if (idx <= 0) - return acc; - const version = pair.slice(0, idx).trim(); - const secret = pair.slice(idx + 1).trim(); - if (!version || !secret) - return acc; - acc[version] = secret; - return acc; - }, {}); -} -function getCurrentJwtAccessSecret(configService: ConfigService): string { - const jwtSecretsRaw = configService.get('JWT_SECRETS'); - if (!jwtSecretsRaw) - return configService.get('JWT_SECRET') ?? 'your-secret-key'; - const currentVersion = configService.get('JWT_SECRET_CURRENT_VERSION'); - const secrets = parseJwtSecrets(jwtSecretsRaw); - const current = (currentVersion && secrets[currentVersion]) || configService.get('JWT_SECRET'); - return current || Object.values(secrets)[0] || 'your-secret-key'; -} -@Module({ - imports: [ - ConfigModule, - UsersModule, - SessionModule, - NotificationsModule, - AuditLogModule, - PassportModule, - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: async ( - configService: ConfigService, - ): Promise<{ secret: string; signOptions: { expiresIn: number } }> => ({ - secret: getCurrentJwtAccessSecret(configService), - signOptions: { - expiresIn: parseInt(configService.get('JWT_EXPIRES_IN') ?? '900', 10), // Convert to seconds (number) - }, - }), - }), - ], - controllers: [AuthController], - providers: [AuthService, JwtStrategy, TransactionService, PasswordPolicyService], - exports: [AuthService], -}) -export class AuthModule { -} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts deleted file mode 100644 index 8c934255..00000000 --- a/src/auth/auth.service.spec.ts +++ /dev/null @@ -1,696 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UnauthorizedException, BadRequestException } from '@nestjs/common'; -import * as bcrypt from 'bcryptjs'; -import { AuthService } from './auth.service'; -import { UsersService } from '../users/users.service'; -import { JwtService } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; -import { SessionService } from '../session/session.service'; -import { TransactionService } from '../common/database/transaction.service'; -import { NotificationsService } from '../notifications/notifications.service'; -import { AuditLogService } from '../audit-log/audit-log.service'; -import { UserRole } from '../users/entities/user.entity'; -import { AuditAction, AuditSeverity } from '../audit-log/enums/audit-action.enum'; -import { - createMockRepository, - createMockConfigService, - createMockEventEmitter, -} from '../../test/utils/mock-factories'; -import { Repository } from 'typeorm'; -describe('AuthService', () => { - // ───────────────────────────────────────────────────────────────────────── - // DECLARATIONS - // ───────────────────────────────────────────────────────────────────────── - - let service: AuthService; - let mockUsersService: jest.Mocked; - let mockJwtService: jest.Mocked; - let mockConfigService: jest.Mocked; - let mockSessionService: jest.Mocked; - let mockTransactionService: jest.Mocked; - let mockNotificationsService: jest.Mocked; - let mockAuditLogService: jest.Mocked; - - // ───────────────────────────────────────────────────────────────────────── - // SETUP & TEARDOWN - // ───────────────────────────────────────────────────────────────────────── - - beforeEach(async () => { - // Initialize all dependency mocks - mockUsersService = { - create: jest.fn(), - findByEmail: jest.fn(), - findOne: jest.fn(), - updateEmailVerificationToken: jest.fn(), - updateRefreshToken: jest.fn(), - updateLastLogin: jest.fn(), - updatePasswordResetToken: jest.fn(), - } as unknown as jest.Mocked; - - mockJwtService = { - sign: jest.fn(), - verify: jest.fn(), - } as unknown as jest.Mocked; - - mockConfigService = createMockConfigService({ - JWT_ACCESS_SECRET: 'access-secret', - JWT_REFRESH_SECRET: 'refresh-secret', - JWT_ACCESS_EXPIRES_IN: '15m', - JWT_REFRESH_EXPIRES_IN: '7d', - }); - - mockSessionService = { - createSession: jest.fn(), - getSession: jest.fn(), - removeSession: jest.fn(), - touchSession: jest.fn(), - withLock: jest.fn(), - } as unknown as jest.Mocked; - - mockTransactionService = { - runInTransaction: jest.fn(), - } as jest.Mocked; - - mockNotificationsService = { - sendVerificationEmail: jest.fn(), - } as unknown as jest.Mocked; - - mockAuditLogService = { - logAuth: jest.fn(), - } as unknown as jest.Mocked; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AuthService, - { - provide: UsersService, - useValue: mockUsersService, - }, - { - provide: JwtService, - useValue: mockJwtService, - }, - { - provide: ConfigService, - useValue: mockConfigService, - }, - { - provide: SessionService, - useValue: mockSessionService, - }, - { - provide: TransactionService, - useValue: mockTransactionService, - }, - { - provide: NotificationsService, - useValue: mockNotificationsService, - }, - { - provide: AuditLogService, - useValue: mockAuditLogService, - }, - ], - }).compile(); - - service = module.get(AuthService); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - // ───────────────────────────────────────────────────────────────────────── - // TEST SUITES - // ───────────────────────────────────────────────────────────────────────── - - describe('register', () => { - const registerDto = { - email: 'test@example.com', - password: 'Password123!', - firstName: 'John', - lastName: 'Doe', - }; - - const mockUser = { - id: 'user-1', - email: registerDto.email, - firstName: registerDto.firstName, - lastName: registerDto.lastName, - role: UserRole.STUDENT, - isEmailVerified: false, - }; - - beforeEach(() => { - mockTransactionService.runInTransaction.mockImplementation(async (fn) => fn()); - mockUsersService.create.mockResolvedValue(mockUser as any); - mockUsersService.updateEmailVerificationToken.mockResolvedValue(undefined); - mockUsersService.updateRefreshToken.mockResolvedValue(undefined); - mockSessionService.createSession.mockResolvedValue('session-1'); - mockJwtService.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token'); - mockNotificationsService.sendVerificationEmail.mockResolvedValue(undefined); - mockAuditLogService.logAuth.mockResolvedValue(undefined); - }); - // ───────────────────────────────────────────────────────────────────────── - // TEST SUITES - // ───────────────────────────────────────────────────────────────────────── - describe('register', () => { - const registerDto = { - email: 'test@example.com', - password: 'Password123!', - firstName: 'John', - lastName: 'Doe', - }; - const mockUser = { - id: 'user-1', - email: registerDto.email, - firstName: registerDto.firstName, - lastName: registerDto.lastName, - role: UserRole.STUDENT, - isEmailVerified: false, - }; - beforeEach(() => { - mockTransactionService.runInTransaction.mockImplementation(async (fn) => fn()); - mockUsersService.create.mockResolvedValue(mockUser); - mockUsersService.updateEmailVerificationToken.mockResolvedValue(undefined); - mockUsersService.updateRefreshToken.mockResolvedValue(undefined); - mockSessionService.createSession.mockResolvedValue('session-1'); - mockJwtService.sign - .mockReturnValueOnce('access-token') - .mockReturnValueOnce('refresh-token'); - mockNotificationsService.sendVerificationEmail.mockResolvedValue(undefined); - mockAuditLogService.logAuth.mockResolvedValue(undefined); - }); - it('should register a new user successfully', async () => { - const result = await service.register(registerDto, '127.0.0.1', 'TestAgent'); - expect(result).toEqual({ - user: { - id: mockUser.id, - email: mockUser.email, - firstName: mockUser.firstName, - lastName: mockUser.lastName, - role: mockUser.role, - isEmailVerified: mockUser.isEmailVerified, - }, - accessToken: 'access-token', - refreshToken: 'refresh-token', - message: 'Registration successful. Please check your email to verify your account.', - }); - expect(mockUsersService.create).toHaveBeenCalledWith(registerDto); - expect(mockNotificationsService.sendVerificationEmail).toHaveBeenCalled(); - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith(AuditAction.REGISTER, mockUser.id, mockUser.email, '127.0.0.1', 'TestAgent', { sessionId: 'session-1' }); - }); - it('should handle registration without IP and user agent', async () => { - await service.register(registerDto); - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith(AuditAction.REGISTER, mockUser.id, mockUser.email, 'unknown', 'unknown', { sessionId: 'session-1' }); - }); - it('should run registration in a transaction', async () => { - await service.register(registerDto); - expect(mockTransactionService.runInTransaction).toHaveBeenCalled(); - }); - }); - describe('login', () => { - const loginDto = { - email: 'test@example.com', - password: 'Password123!', - }; - const mockUser = { - id: 'user-1', - email: loginDto.email, - password: '$2a$10$hashedpassword', - firstName: 'John', - lastName: 'Doe', - role: UserRole.STUDENT, - isEmailVerified: true, - status: 'ACTIVE', - }; - beforeEach(() => { - mockUsersService.findByEmail.mockResolvedValue(mockUser); - mockUsersService.updateLastLogin.mockResolvedValue(undefined); - mockUsersService.updateRefreshToken.mockResolvedValue(undefined); - mockSessionService.createSession.mockResolvedValue('session-1'); - mockJwtService.sign - .mockReturnValueOnce('access-token') - .mockReturnValueOnce('refresh-token'); - mockAuditLogService.logAuth.mockResolvedValue(undefined); - }); - it('should login user successfully with valid credentials', async () => { - // Mock bcrypt.compare to return true - const bcrypt = require('bcryptjs'); - jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); - const result = await service.login(loginDto, '127.0.0.1', 'TestAgent'); - expect(result).toEqual({ - user: { - id: mockUser.id, - email: mockUser.email, - firstName: mockUser.firstName, - lastName: mockUser.lastName, - role: mockUser.role, - isEmailVerified: mockUser.isEmailVerified, - }, - accessToken: 'access-token', - refreshToken: 'refresh-token', - }); - expect(mockUsersService.findByEmail).toHaveBeenCalledWith(loginDto.email); - expect(mockUsersService.updateLastLogin).toHaveBeenCalledWith(mockUser.id); - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith(AuditAction.LOGIN, mockUser.id, mockUser.email, '127.0.0.1', 'TestAgent', { sessionId: 'session-1' }); - }); - it('should throw UnauthorizedException when user not found', async () => { - mockUsersService.findByEmail.mockResolvedValue(null); - await expect(service.login(loginDto)).rejects.toThrow(UnauthorizedException); - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith(AuditAction.LOGIN_FAILED, null, loginDto.email, 'unknown', 'unknown', { reason: 'User not found' }, AuditSeverity.WARNING); - }); - it('should throw UnauthorizedException when password is invalid', async () => { - const bcrypt = require('bcryptjs'); - jest.spyOn(bcrypt, 'compare').mockResolvedValue(false); - await expect(service.login(loginDto)).rejects.toThrow(UnauthorizedException); - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith(AuditAction.LOGIN_FAILED, mockUser.id, mockUser.email, 'unknown', 'unknown', { reason: 'Invalid password' }, AuditSeverity.WARNING); - }); - it('should handle login without IP and user agent', async () => { - const bcrypt = require('bcryptjs'); - jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); - await service.login(loginDto); - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith(AuditAction.LOGIN, mockUser.id, mockUser.email, 'unknown', 'unknown', { sessionId: 'session-1' }); - }); - }); - describe('refreshToken', () => { - const refreshToken = 'valid-refresh-token'; - const mockUser = { - id: 'user-1', - email: 'test@example.com', - refreshToken: '$2a$10$hashedrefreshtoken', - }; - beforeEach(() => { - mockJwtService.verify.mockReturnValue({ - sub: mockUser.id, - email: mockUser.email, - sid: 'session-1', - }); - mockSessionService.withLock.mockImplementation(async (key, fn) => fn()); - mockUsersService.findOne.mockResolvedValue(mockUser); - mockSessionService.getSession.mockResolvedValue({ id: 'session-1' }); - mockSessionService.touchSession.mockResolvedValue(undefined); - mockJwtService.sign - .mockReturnValueOnce('new-access-token') - .mockReturnValueOnce('new-refresh-token'); - mockUsersService.updateRefreshToken.mockResolvedValue(undefined); - }); - it('should refresh tokens successfully', async () => { - const bcrypt = require('bcryptjs'); - jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); - const result = await service.refreshToken(refreshToken); - expect(result).toEqual({ - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }); - expect(mockJwtService.verify).toHaveBeenCalledWith(refreshToken, { - secret: 'refresh-secret', - }); - expect(mockSessionService.touchSession).toHaveBeenCalledWith('session-1', { - lastRefreshAt: expect.any(Number), - }); - }); - it('should create new session if current session not found', async () => { - mockSessionService.getSession.mockResolvedValue(null); - mockSessionService.createSession.mockResolvedValue('new-session-1'); - const bcrypt = require('bcryptjs'); - jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); - await service.refreshToken(refreshToken); - expect(mockSessionService.createSession).toHaveBeenCalledWith(mockUser.id, { - type: 'auth-refresh', - }); - }); - it('should throw UnauthorizedException when refresh token is invalid', async () => { - mockJwtService.verify.mockImplementation(() => { - throw new Error('Invalid token'); - }); - await expect(service.refreshToken('invalid-token')).rejects.toThrow(UnauthorizedException); - }); - it('should throw UnauthorizedException when user not found', async () => { - mockUsersService.findOne.mockResolvedValue(null); - await expect(service.refreshToken(refreshToken)).rejects.toThrow(UnauthorizedException); - }); - it('should throw UnauthorizedException when stored refresh token is invalid', async () => { - const bcrypt = require('bcryptjs'); - jest.spyOn(bcrypt, 'compare').mockResolvedValue(false); - await expect(service.refreshToken(refreshToken)).rejects.toThrow(UnauthorizedException); - }); - }); - }); - - describe('login', () => { - const loginDto = { - email: 'test@example.com', - password: 'Password123!', - }; - - const mockUser = { - id: 'user-1', - email: loginDto.email, - password: '$2a$10$hashedpassword', - firstName: 'John', - lastName: 'Doe', - role: UserRole.STUDENT, - isEmailVerified: true, - status: 'ACTIVE', - }; - - beforeEach(() => { - mockUsersService.findByEmail.mockResolvedValue(mockUser as any); - mockUsersService.updateLastLogin.mockResolvedValue(undefined); - mockUsersService.updateRefreshToken.mockResolvedValue(undefined); - mockSessionService.createSession.mockResolvedValue('session-1'); - mockJwtService.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token'); - mockAuditLogService.logAuth.mockResolvedValue(undefined); - }); - - it('should login user successfully with valid credentials', async () => { - // Mock bcrypt.compare to return true - ( - jest.spyOn(bcrypt, 'compare') as unknown as jest.SpyInstance< - Promise, - [string, string] - > - ).mockResolvedValue(true); - - const result = await service.login(loginDto, '127.0.0.1', 'TestAgent'); - - expect(result).toEqual({ - user: { - id: mockUser.id, - email: mockUser.email, - firstName: mockUser.firstName, - lastName: mockUser.lastName, - role: mockUser.role, - isEmailVerified: mockUser.isEmailVerified, - }, - accessToken: 'access-token', - refreshToken: 'refresh-token', - }); - - expect(mockUsersService.findByEmail).toHaveBeenCalledWith(loginDto.email); - expect(mockUsersService.updateLastLogin).toHaveBeenCalledWith(mockUser.id); - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith( - AuditAction.LOGIN, - mockUser.id, - mockUser.email, - '127.0.0.1', - 'TestAgent', - { sessionId: 'session-1' }, - ); - }); - describe('generateTokens', () => { - const mockUser = { - id: 'user-1', - email: 'test@example.com', - role: UserRole.STUDENT, - }; - const sessionId = 'session-1'; - beforeEach(() => { - mockJwtService.sign - .mockReturnValueOnce('access-token') - .mockReturnValueOnce('refresh-token'); - }); - it('should generate access and refresh tokens', async () => { - const result = await (service as unknown).generateTokens(mockUser, sessionId); - expect(result).toEqual({ - accessToken: 'access-token', - refreshToken: 'refresh-token', - }); - expect(mockJwtService.sign).toHaveBeenCalledTimes(2); - expect(mockJwtService.sign).toHaveBeenNthCalledWith(1, { - sub: mockUser.id, - email: mockUser.email, - role: mockUser.role, - sid: sessionId, - }, { - secret: 'access-secret', - expiresIn: '15m', - }); - }); - }); - - it('should throw UnauthorizedException when password is invalid', async () => { - ( - jest.spyOn(bcrypt, 'compare') as unknown as jest.SpyInstance< - Promise, - [string, string] - > - ).mockResolvedValue(false); - - await expect(service.login(loginDto)).rejects.toThrow(UnauthorizedException); - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith( - AuditAction.LOGIN_FAILED, - mockUser.id, - mockUser.email, - 'unknown', - 'unknown', - { reason: 'Invalid password' }, - AuditSeverity.WARNING, - ); - }); - - it('should handle login without IP and user agent', async () => { - ( - jest.spyOn(bcrypt, 'compare') as unknown as jest.SpyInstance< - Promise, - [string, string] - > - ).mockResolvedValue(true); - - await service.login(loginDto); - - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith( - AuditAction.LOGIN, - mockUser.id, - mockUser.email, - 'unknown', - 'unknown', - { sessionId: 'session-1' }, - ); - }); - }); - - describe('refreshToken', () => { - const refreshToken = 'valid-refresh-token'; - const mockUser = { - id: 'user-1', - email: 'test@example.com', - refreshToken: '$2a$10$hashedrefreshtoken', - }; - - beforeEach(() => { - mockJwtService.verify.mockReturnValue({ - sub: mockUser.id, - email: mockUser.email, - sid: 'session-1', - }); - mockSessionService.withLock.mockImplementation(async (key, fn) => fn()); - mockUsersService.findOne.mockResolvedValue(mockUser as any); - mockSessionService.getSession.mockResolvedValue({ id: 'session-1' } as any); - mockSessionService.touchSession.mockResolvedValue(undefined); - mockJwtService.sign - .mockReturnValueOnce('new-access-token') - .mockReturnValueOnce('new-refresh-token'); - mockUsersService.updateRefreshToken.mockResolvedValue(undefined); - }); - - it('should refresh tokens successfully', async () => { - ( - jest.spyOn(bcrypt, 'compare') as unknown as jest.SpyInstance< - Promise, - [string, string] - > - ).mockResolvedValue(true); - - const result = await service.refreshToken(refreshToken); - - expect(result).toEqual({ - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }); - - expect(mockJwtService.verify).toHaveBeenCalledWith(refreshToken, { - secret: 'refresh-secret', - }); - expect(mockSessionService.touchSession).toHaveBeenCalledWith('session-1', { - lastRefreshAt: expect.any(Number), - }); - }); - - it('should create new session if current session not found', async () => { - mockSessionService.getSession.mockResolvedValue(null); - mockSessionService.createSession.mockResolvedValue('new-session-1'); - - ( - jest.spyOn(bcrypt, 'compare') as unknown as jest.SpyInstance< - Promise, - [string, string] - > - ).mockResolvedValue(true); - - await service.refreshToken(refreshToken); - - expect(mockSessionService.createSession).toHaveBeenCalledWith(mockUser.id, { - type: 'auth-refresh', - }); - }); - - it('should throw UnauthorizedException when refresh token is invalid', async () => { - mockJwtService.verify.mockImplementation(() => { - throw new Error('Invalid token'); - }); - - await expect(service.refreshToken('invalid-token')).rejects.toThrow(UnauthorizedException); - }); - - it('should throw UnauthorizedException when user not found', async () => { - mockUsersService.findOne.mockResolvedValue(mockUser as any); - - await expect(service.refreshToken(refreshToken)).rejects.toThrow(UnauthorizedException); - }); - - it('should throw UnauthorizedException when stored refresh token is invalid', async () => { - ( - jest.spyOn(bcrypt, 'compare') as unknown as jest.SpyInstance< - Promise, - [string, string] - > - ).mockResolvedValue(false); - - await expect(service.refreshToken(refreshToken)).rejects.toThrow(UnauthorizedException); - }); - }); - - describe('logout', () => { - const userId = 'user-1'; - const sessionId = 'session-1'; - const mockUser = { - id: userId, - email: 'test@example.com', - }; - - beforeEach(() => { - mockUsersService.findOne.mockResolvedValue(mockUser as any); - mockSessionService.withLock.mockImplementation(async (key, fn) => fn()); - mockSessionService.removeSession.mockResolvedValue(undefined); - mockUsersService.updateRefreshToken.mockResolvedValue(undefined); - mockAuditLogService.logAuth.mockResolvedValue(undefined); - }); - - it('should logout user successfully', async () => { - const result = await service.logout(userId, sessionId, '127.0.0.1', 'TestAgent'); - - expect(result).toEqual({ message: 'Logout successful' }); - expect(mockSessionService.removeSession).toHaveBeenCalledWith(sessionId); - expect(mockUsersService.updateRefreshToken).toHaveBeenCalledWith(userId, null); - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith( - AuditAction.LOGOUT, - userId, - mockUser.email, - '127.0.0.1', - 'TestAgent', - { sessionId }, - ); - }); - - it('should handle logout without session ID', async () => { - await service.logout(userId); - - expect(mockSessionService.removeSession).not.toHaveBeenCalled(); - expect(mockUsersService.updateRefreshToken).toHaveBeenCalledWith(userId, null); - }); - - it('should handle logout without IP and user agent', async () => { - await service.logout(userId, sessionId); - - expect(mockAuditLogService.logAuth).toHaveBeenCalledWith( - AuditAction.LOGOUT, - userId, - mockUser.email, - 'unknown', - 'unknown', - { sessionId }, - ); - }); - }); - - describe('forgotPassword', () => { - const email = 'test@example.com'; - const mockUser = { id: 'user-1', email }; - - beforeEach(() => { - mockUsersService.findByEmail.mockResolvedValue(mockUser as any); - mockUsersService.updatePasswordResetToken.mockResolvedValue(undefined); - }); - - it('should initiate password reset for existing user', async () => { - const result = await service.forgotPassword(email); - - expect(result).toEqual({ - message: 'If the email exists, a password reset link has been sent.', - }); - expect(mockUsersService.findByEmail).toHaveBeenCalledWith(email); - expect(mockUsersService.updatePasswordResetToken).toHaveBeenCalled(); - }); - - it('should not reveal if user does not exist', async () => { - mockUsersService.findByEmail.mockResolvedValue(null); - - const result = await service.forgotPassword('nonexistent@example.com'); - - expect(result).toEqual({ - message: 'If the email exists, a password reset link has been sent.', - }); - expect(mockUsersService.updatePasswordResetToken).not.toHaveBeenCalled(); - }); - }); - - describe('generateTokens', () => { - const mockUser = { - id: 'user-1', - email: 'test@example.com', - role: UserRole.STUDENT, - }; - const sessionId = 'session-1'; - - beforeEach(() => { - mockJwtService.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token'); - }); - - it('should generate access and refresh tokens', async () => { - const result = await (service as any).generateTokens(mockUser, sessionId); - - expect(result).toEqual({ - accessToken: 'access-token', - refreshToken: 'refresh-token', - }); - - expect(mockJwtService.sign).toHaveBeenCalledTimes(2); - expect(mockJwtService.sign).toHaveBeenNthCalledWith( - 1, - { - sub: mockUser.id, - email: mockUser.email, - role: mockUser.role, - sid: sessionId, - }, - { - secret: 'access-secret', - expiresIn: '15m', - }, - ); - }); - }); - - describe('generateRandomToken', () => { - it('should generate a random token', () => { - const token1 = (service as any).generateRandomToken(); - const token2 = (service as any).generateRandomToken(); - - expect(typeof token1).toBe('string'); - expect(token1.length).toBeGreaterThan(0); - expect(token1).not.toBe(token2); // Should be different each time - }); - }); -}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts deleted file mode 100644 index 01e444b1..00000000 --- a/src/auth/auth.service.ts +++ /dev/null @@ -1,580 +0,0 @@ -import { Injectable, UnauthorizedException, BadRequestException, Logger } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; -import { UsersService } from '../users/users.service'; -import { RegisterDto, LoginDto, ResetPasswordDto, ChangePasswordDto } from './dto/auth.dto'; -import * as bcrypt from 'bcryptjs'; -import { randomBytes } from 'crypto'; -import { SessionService } from '../session/session.service'; -import { TransactionService } from '../common/database/transaction.service'; -import { UserRole } from '../users/entities/user.entity'; -import { ensureValidCredentials, ensureUserIsActive, ensureValidUserToken, } from '../common/utils/user.utils'; -import { NotificationsService } from '../notifications/notifications.service'; -import { AuditLogService } from '../audit-log/audit-log.service'; -import { AuditAction, AuditSeverity } from '../audit-log/enums/audit-action.enum'; -import { PasswordPolicyService } from './services/password-policy.service'; - -interface IJwtTokenPayload { - sub: string; - email: string; - role: UserRole; - sid: string; -} - -interface IAuthTokens { - accessToken: string; - refreshToken: string; -} - -interface IAuthUserResponse { - id: string; - email: string; - firstName: string; - lastName: string; - role: UserRole; - isEmailVerified: boolean; -} - -interface IRegisterResponse { - user: IAuthUserResponse; - accessToken: string; - refreshToken: string; - message: string; -} - -interface ILoginResponse { - user: IAuthUserResponse; - accessToken: string; - refreshToken: string; -} - -interface ITokenUser { - id: string; - email: string; - role: UserRole; -} - -/** - * Provides auth operations. - */ -@Injectable() -export class AuthService { - private readonly logger = new Logger(AuthService.name); - - constructor( - private readonly usersService: UsersService, - private readonly jwtService: JwtService, - private readonly configService: ConfigService, - private readonly sessionService: SessionService, - private readonly transactionService: TransactionService, - private readonly notificationsService: NotificationsService, - private readonly auditLogService: AuditLogService, - private readonly passwordPolicyService: PasswordPolicyService, - ) {} - - /** - * Registers register. - * @param registerDto The request payload. - * @param ipAddress The ip address. - * @param userAgent The user agent. - * @returns The resulting register response. - */ - async register( - registerDto: RegisterDto, - ipAddress?: string, - userAgent?: string, - ): Promise { - await this.passwordPolicyService.enforce(registerDto.password); - - return await this.transactionService.runInTransaction(async (_manager) => { - // Create user - const user = await this.usersService.create(registerDto); - - // Generate email verification token - const verificationToken = this.generateRandomToken(); - const verificationExpires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours - - await this.usersService.updateEmailVerificationToken( - user.id, - verificationToken, - verificationExpires, - ); - - // Send verification email - await this.notificationsService.sendVerificationEmail(user.email, verificationToken); - - const sessionId = await this.sessionService.createSession(user.id, { type: 'auth-register' }); - const { accessToken, refreshToken } = await this.generateTokens(user, sessionId); - - // Save refresh token - const hashedRefreshToken = await bcrypt.hash(refreshToken, 10); - await this.usersService.updateRefreshToken(user.id, hashedRefreshToken); - - // Log registration - await this.auditLogService.logAuth( - AuditAction.REGISTER, - user.id, - user.email, - ipAddress || 'unknown', - userAgent || 'unknown', - { sessionId }, - ); - - return { - user: { - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - role: user.role, - isEmailVerified: user.isEmailVerified, - }, - accessToken, - refreshToken, - message: 'Registration successful. Please check your email to verify your account.', - }; - }); - } - - async login(loginDto: LoginDto, ipAddress?: string, userAgent?: string): Promise { - // Find user - const userOrNull = await this.usersService.findByEmail(loginDto.email); - - // Log failed login attempt if user not found - if (!userOrNull) { - await this.auditLogService.logAuth( - AuditAction.LOGIN_FAILED, - null, - loginDto.email, - ipAddress || 'unknown', - userAgent || 'unknown', - { reason: 'User not found' }, - AuditSeverity.WARNING, - ); - throw new UnauthorizedException('Invalid credentials'); - } - - const user = ensureValidCredentials(userOrNull); - - // Verify password - const isPasswordValid = await bcrypt.compare(loginDto.password, user.password); - if (!isPasswordValid) { - await this.auditLogService.logAuth( - AuditAction.LOGIN_FAILED, - user.id, - user.email, - ipAddress || 'unknown', - userAgent || 'unknown', - { reason: 'Invalid password' }, - AuditSeverity.WARNING, - ); - throw new UnauthorizedException('Invalid credentials'); - } - - // Check if user is active - try { - ensureUserIsActive(user); - } catch (error) { - await this.auditLogService.logAuth( - AuditAction.LOGIN_FAILED, - user.id, - user.email, - ipAddress || 'unknown', - userAgent || 'unknown', - { reason: 'User account inactive' }, - AuditSeverity.WARNING, - ); - throw error; - } - - // Update last login - await this.usersService.updateLastLogin(user.id); - - const sessionId = await this.sessionService.createSession(user.id, { type: 'auth-login' }); - const { accessToken, refreshToken } = await this.generateTokens(user, sessionId); - - // Save refresh token - const hashedRefreshToken = await bcrypt.hash(refreshToken, 10); - await this.usersService.updateRefreshToken(user.id, hashedRefreshToken); - - // Log successful login - await this.auditLogService.logAuth( - AuditAction.LOGIN, - user.id, - user.email, - ipAddress || 'unknown', - userAgent || 'unknown', - { sessionId }, - ); - - return { - user: { - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - role: user.role, - isEmailVerified: user.isEmailVerified, - }, - accessToken, - refreshToken, - }; - } - - async refreshToken(refreshToken: string): Promise { - try { - // Verify refresh token - const payload = this.jwtService.verify(refreshToken, { - secret: this.configService.get('JWT_REFRESH_SECRET') || 'refresh-secret-key', - }); - return this.sessionService.withLock(`refresh:${payload.sub}`, async () => { - // Find user - const userOrNull = await this.usersService.findByEmail(loginDto.email); - // Log failed login attempt if user not found - if (!userOrNull) { - await this.auditLogService.logAuth(AuditAction.LOGIN_FAILED, null, loginDto.email, ipAddress || 'unknown', userAgent || 'unknown', { reason: 'User not found' }, AuditSeverity.WARNING); - throw new UnauthorizedException('Invalid credentials'); - } - const user = ensureValidCredentials(userOrNull); - // Verify password - const isPasswordValid = await bcrypt.compare(loginDto.password, user.password); - if (!isPasswordValid) { - await this.auditLogService.logAuth(AuditAction.LOGIN_FAILED, user.id, user.email, ipAddress || 'unknown', userAgent || 'unknown', { reason: 'Invalid password' }, AuditSeverity.WARNING); - throw new UnauthorizedException('Invalid credentials'); - } - // Check if user is active - try { - ensureUserIsActive(user); - } - catch (error) { - await this.auditLogService.logAuth(AuditAction.LOGIN_FAILED, user.id, user.email, ipAddress || 'unknown', userAgent || 'unknown', { reason: 'User account inactive' }, AuditSeverity.WARNING); - throw error; - } - // Update last login - await this.usersService.updateLastLogin(user.id); - const sessionId = await this.sessionService.createSession(user.id, { type: 'auth-login' }); - const { accessToken, refreshToken } = await this.generateTokens(user, sessionId); - // Save refresh token - const hashedRefreshToken = await bcrypt.hash(refreshToken, 10); - await this.usersService.updateRefreshToken(user.id, hashedRefreshToken); - // Log successful login - await this.auditLogService.logAuth(AuditAction.LOGIN, user.id, user.email, ipAddress || 'unknown', userAgent || 'unknown', { sessionId }); - return { - user: { - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - role: user.role, - isEmailVerified: user.isEmailVerified, - }, - accessToken, - refreshToken, - }; - } - } - - /** - * Executes logout. - * @param userId The user identifier. - * @param sessionId The session identifier. - * @param ipAddress The ip address. - * @param userAgent The user agent. - * @returns The operation result. - */ - async logout( - userId: string, - sessionId?: string, - ipAddress?: string, - userAgent?: string, - ): Promise<{ message: string }> { - const user = await this.usersService.findOne(userId); - - await this.sessionService.withLock(`logout:${userId}`, async () => { - if (sessionId) { - await this.sessionService.removeSession(sessionId); - } - await this.usersService.updateRefreshToken(userId, null); - }); - - // Log logout - await this.auditLogService.logAuth( - AuditAction.LOGOUT, - userId, - user?.email || null, - ipAddress || 'unknown', - userAgent || 'unknown', - { sessionId }, - ); - - return { message: 'Logout successful' }; - } - - /** - * Executes forgot Password. - * @param email The email address. - * @returns The operation result. - */ - async forgotPassword(email: string): Promise<{ message: string }> { - const user = await this.usersService.findByEmail(email); - if (!user) { - // Don't reveal if user exists - return { message: 'If the email exists, a password reset link has been sent.' }; - } - - // Generate reset token - const resetToken = this.generateRandomToken(); - const resetExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour - - await this.usersService.updatePasswordResetToken(user.id, resetToken, resetExpires); - - // Send password reset email - await this.notificationsService.sendPasswordResetEmail(user.email, resetToken); - - return { message: 'If the email exists, a password reset link has been sent.' }; - } - - /** - * Resets password. - * @param resetPasswordDto The request payload. - * @returns The operation result. - */ - async resetPassword(resetPasswordDto: ResetPasswordDto): Promise<{ message: string }> { - await this.passwordPolicyService.enforce(resetPasswordDto.newPassword); - - // Find user by reset token - const userOrNull = await this.usersService.findByPasswordResetToken(resetPasswordDto.token); - const user = ensureValidUserToken( - userOrNull, - 'passwordResetToken', - 'passwordResetExpires', - 'Invalid or expired reset token', - ); - - // Update password - await this.usersService.update(user.id, { password: resetPasswordDto.newPassword }); - - // Clear reset token - await this.usersService.updatePasswordResetToken(user.id, null, null); - - return { message: 'Password has been reset successfully' }; - } - - /** - * Executes change Password. - * @param userId The user identifier. - * @param changePasswordDto The request payload. - * @param ipAddress The ip address. - * @param userAgent The user agent. - * @returns The operation result. - */ - async changePassword( - userId: string, - changePasswordDto: ChangePasswordDto, - ipAddress?: string, - userAgent?: string, - ): Promise<{ message: string }> { - const user = await this.usersService.findOne(userId); - - // Verify current password - const isPasswordValid = await bcrypt.compare(changePasswordDto.currentPassword, user.password); - if (!isPasswordValid) { - await this.auditLogService.logAuth( - AuditAction.PASSWORD_CHANGE, - userId, - user.email, - ipAddress || 'unknown', - userAgent || 'unknown', - { success: false, reason: 'Current password incorrect' }, - AuditSeverity.WARNING, - ); - throw new BadRequestException('Current password is incorrect'); - } - - await this.passwordPolicyService.enforce(changePasswordDto.newPassword); - - // Update password - await this.usersService.update(userId, { password: changePasswordDto.newPassword }); - - // Log password change - await this.auditLogService.logAuth( - AuditAction.PASSWORD_CHANGE, - userId, - user.email, - ipAddress || 'unknown', - userAgent || 'unknown', - { success: true }, - ); - - return { message: 'Password changed successfully' }; - } - - /** - * Validates email. - * @param token The token value. - * @returns The operation result. - */ - async verifyEmail(token: string): Promise<{ message: string }> { - // Find user by verification token - const userOrNull = await this.usersService.findByEmailVerificationToken(token); - const user = ensureValidUserToken( - userOrNull, - 'emailVerificationToken', - 'emailVerificationExpires', - 'Invalid or expired verification token', - ); - - // Update user as verified - await this.usersService.update(user.id, { isEmailVerified: true }); - - // Clear verification token - await this.usersService.updateEmailVerificationToken(user.id, null, null); - - return { message: 'Email verified successfully' }; - } - - private async generateTokens(user: ITokenUser, sessionId: string): Promise { - const payload: IJwtTokenPayload = { - sub: user.id, - email: user.email, - role: user.role, - sid: sessionId, - }; - - const { currentVersion, currentSecret } = this.getCurrentJwtAccessSecret(); - - const [accessToken, refreshToken] = await Promise.all([ - this.jwtService.signAsync(payload, { - secret: currentSecret, - expiresIn: parseInt(this.configService.get('JWT_EXPIRES_IN') || '900', 10), // 900s = 15m - header: currentVersion ? { kid: currentVersion } : undefined, - }), - this.jwtService.signAsync(payload, { - secret: this.configService.get('JWT_REFRESH_SECRET') || 'refresh-secret-key', - expiresIn: parseInt( - this.configService.get('JWT_REFRESH_EXPIRES_IN') || '604800', - 10, - ), // 604800s = 7d - }), - ]); - - return { accessToken, refreshToken }; - } - - private getCurrentJwtAccessSecret(): { currentVersion: string | null; currentSecret: string } { - const jwtSecretsRaw = this.configService.get('JWT_SECRETS'); - const currentVersion = this.configService.get('JWT_SECRET_CURRENT_VERSION') || null; - - if (!jwtSecretsRaw) { - return { - currentVersion, - currentSecret: this.configService.get('JWT_SECRET') || 'your-secret-key', - }; - } - async resetPassword(resetPasswordDto: ResetPasswordDto): Promise<{ - message: string; - }> { - // Find user by reset token - const userOrNull = await this.usersService.findByPasswordResetToken(resetPasswordDto.token); - const user = ensureValidUserToken(userOrNull, 'passwordResetToken', 'passwordResetExpires', 'Invalid or expired reset token'); - // Update password - await this.usersService.update(user.id, { password: resetPasswordDto.newPassword }); - // Clear reset token - await this.usersService.updatePasswordResetToken(user.id, null, null); - return { message: 'Password has been reset successfully' }; - } - async changePassword(userId: string, changePasswordDto: ChangePasswordDto, ipAddress?: string, userAgent?: string): Promise<{ - message: string; - }> { - const user = await this.usersService.findOne(userId); - // Verify current password - const isPasswordValid = await bcrypt.compare(changePasswordDto.currentPassword, user.password); - if (!isPasswordValid) { - await this.auditLogService.logAuth(AuditAction.PASSWORD_CHANGE, userId, user.email, ipAddress || 'unknown', userAgent || 'unknown', { success: false, reason: 'Current password incorrect' }, AuditSeverity.WARNING); - throw new BadRequestException('Current password is incorrect'); - } - // Update password - await this.usersService.update(userId, { password: changePasswordDto.newPassword }); - // Log password change - await this.auditLogService.logAuth(AuditAction.PASSWORD_CHANGE, userId, user.email, ipAddress || 'unknown', userAgent || 'unknown', { success: true }); - return { message: 'Password changed successfully' }; - } - async verifyEmail(token: string): Promise<{ - message: string; - }> { - // Find user by verification token - const userOrNull = await this.usersService.findByEmailVerificationToken(token); - const user = ensureValidUserToken(userOrNull, 'emailVerificationToken', 'emailVerificationExpires', 'Invalid or expired verification token'); - // Update user as verified - await this.usersService.update(user.id, { isEmailVerified: true }); - // Clear verification token - await this.usersService.updateEmailVerificationToken(user.id, null, null); - return { message: 'Email verified successfully' }; - } - private async generateTokens(user: TokenUser, sessionId: string): Promise { - const payload: JwtTokenPayload = { - sub: user.id, - email: user.email, - role: user.role, - sid: sessionId, - }; - const { currentVersion, currentSecret } = this.getCurrentJwtAccessSecret(); - const [accessToken, refreshToken] = await Promise.all([ - this.jwtService.signAsync(payload, { - secret: currentSecret, - expiresIn: parseInt(this.configService.get('JWT_EXPIRES_IN') || '900', 10), // 900s = 15m - header: currentVersion ? { kid: currentVersion } : undefined, - }), - this.jwtService.signAsync(payload, { - secret: this.configService.get('JWT_REFRESH_SECRET') || 'refresh-secret-key', - expiresIn: parseInt(this.configService.get('JWT_REFRESH_EXPIRES_IN') || '604800', 10), // 604800s = 7d - }), - ]); - return { accessToken, refreshToken }; - } - private getCurrentJwtAccessSecret(): { - currentVersion: string | null; - currentSecret: string; - } { - const jwtSecretsRaw = this.configService.get('JWT_SECRETS'); - const currentVersion = this.configService.get('JWT_SECRET_CURRENT_VERSION') || null; - if (!jwtSecretsRaw) { - return { - currentVersion, - currentSecret: this.configService.get('JWT_SECRET') || 'your-secret-key', - }; - } - const secrets = this.parseJwtSecrets(jwtSecretsRaw); - const currentSecret = (currentVersion && secrets[currentVersion]) || this.configService.get('JWT_SECRET'); - return { currentVersion, currentSecret: currentSecret || 'your-secret-key' }; - } - private parseJwtSecrets(raw: string): Record { - try { - const parsed = JSON.parse(raw) as unknown; - if (parsed && typeof parsed === 'object') { - return parsed as Record; - } - } - catch { - // ignore - } - return raw - .split(',') - .map((pair) => pair.trim()) - .filter(Boolean) - .reduce>((acc, pair) => { - const idx = pair.indexOf(':'); - if (idx <= 0) - return acc; - const version = pair.slice(0, idx).trim(); - const secret = pair.slice(idx + 1).trim(); - if (!version || !secret) - return acc; - acc[version] = secret; - return acc; - }, {}); - } - private generateRandomToken(): string { - return randomBytes(32).toString('hex'); - } -} diff --git a/src/auth/dto/auth.dto.ts b/src/auth/dto/auth.dto.ts deleted file mode 100644 index 61fd9fa9..00000000 --- a/src/auth/dto/auth.dto.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { IsEmail, IsString, IsEnum, IsOptional, IsNotEmpty } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { UserRole } from '../../users/entities/user.entity'; -import { IsStrongPassword } from '../../common/validators/password.validator'; - -/** - * Defines the register payload. - */ -export class RegisterDto { - @ApiProperty({ example: 'john.doe@example.com' }) - @IsEmail({}, { message: 'Must be a valid email address' }) - @IsNotEmpty({ message: 'Email is required' }) - email: string; - @ApiProperty({ example: 'StrongPass123!' }) - @IsString({ message: 'Password must be a string' }) - @IsStrongPassword({ message: 'Password must be stronger' }) - password: string; - @ApiProperty({ example: 'John' }) - @IsString({ message: 'First name must be a string' }) - @IsNotEmpty({ message: 'First name is required' }) - firstName: string; - @ApiProperty({ example: 'Doe' }) - @IsString({ message: 'Last name must be a string' }) - @IsNotEmpty({ message: 'Last name is required' }) - lastName: string; - @ApiProperty({ enum: UserRole, required: false, default: UserRole.STUDENT }) - @IsOptional() - @IsEnum(UserRole, { message: 'Role must be a valid enum value' }) - role?: UserRole; -} - -/** - * Defines the login payload. - */ -export class LoginDto { - @ApiProperty({ example: 'john.doe@example.com' }) - @IsEmail({}, { message: 'Must be a valid email address' }) - @IsNotEmpty({ message: 'Email is required' }) - email: string; - @ApiProperty({ example: 'StrongPass123!' }) - @IsString() - @IsNotEmpty({ message: 'Password is required' }) - password: string; -} - -/** - * Defines the refresh Token payload. - */ -export class RefreshTokenDto { - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'Refresh token is required' }) - refreshToken: string; -} - -/** - * Defines the forgot Password payload. - */ -export class ForgotPasswordDto { - @ApiProperty({ example: 'john.doe@example.com' }) - @IsEmail({}, { message: 'Must be a valid email address' }) - @IsNotEmpty({ message: 'Email is required' }) - email: string; -} - -/** - * Defines the reset Password payload. - */ -export class ResetPasswordDto { - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'Token is required' }) - token: string; - @ApiProperty({ example: 'NewStrongPass123!' }) - @IsString() - @IsStrongPassword() - newPassword: string; -} - -/** - * Defines the change Password payload. - */ -export class ChangePasswordDto { - @ApiProperty({ example: 'OldPass123!' }) - @IsString() - @IsNotEmpty({ message: 'Current password is required' }) - currentPassword: string; - @ApiProperty({ example: 'NewPass123!' }) - @IsString() - @IsStrongPassword() - newPassword: string; -} - -/** - * Defines the verify Email payload. - */ -export class VerifyEmailDto { - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'Token is required' }) - token: string; -} diff --git a/src/auth/guards/roles.guard.ts b/src/auth/guards/roles.guard.ts index 758b965b..6359b583 100644 --- a/src/auth/guards/roles.guard.ts +++ b/src/auth/guards/roles.guard.ts @@ -1,4 +1,4 @@ -import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ROLES_KEY } from '../decorators/roles.decorator'; import { UserRole } from '../../users/entities/user.entity'; @@ -24,4 +24,12 @@ export class RolesGuard implements CanActivate { if (!requiredRoles) { return true; } + + const { user } = context.switchToHttp().getRequest(); + if (!user) { + throw new UnauthorizedException(); + } + + return requiredRoles.includes(user.role); + } } diff --git a/src/auth/guards/ws-jwt-auth.guard.ts b/src/auth/guards/ws-jwt-auth.guard.ts deleted file mode 100644 index 12278142..00000000 --- a/src/auth/guards/ws-jwt-auth.guard.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; -import { Socket } from 'socket.io'; -import * as jwt from 'jsonwebtoken'; - -/** - * Protects ws Jwt Auth execution paths. - */ -@Injectable() -export class WsJwtAuthGuard implements CanActivate { - constructor( - private readonly jwtService: JwtService, - private readonly configService: ConfigService, - ) {} - - /** - * Executes can Activate. - * @param context The context. - * @returns Whether the operation succeeded. - */ - async canActivate(context: ExecutionContext): Promise { - const client: Socket = context.switchToWs().getClient(); - const token = - client.handshake.auth?.token || client.handshake.headers?.authorization?.split(' ')[1]; - - if (!token) { - client.disconnect(true); - return false; - } - private async verifyAccessToken(token: string): Promise { - const { secrets } = this.getJwtAccessSecrets(); - const kid = this.getTokenKid(token); - if (kid && secrets[kid]) { - return this.jwtService.verifyAsync(token, { secret: secrets[kid] }); - } - const secretList = Object.values(secrets); - if (secretList.length === 0) { - return this.jwtService.verifyAsync(token, { - secret: this.configService.get('JWT_SECRET') || 'your-secret-key', - }); - } - let lastError: unknown; - for (const secret of secretList) { - try { - return await this.jwtService.verifyAsync(token, { secret }); - } - catch (err) { - lastError = err; - } - } - throw lastError; - } - private getTokenKid(token: string): string | null { - const decoded = jwt.decode(token, { complete: true }) as jwt.Jwt | null; - const kid = decoded && typeof decoded === 'object' ? (decoded.header as unknown)?.kid : undefined; - return typeof kid === 'string' ? kid : null; - } - private getJwtAccessSecrets(): { - currentVersion: string | null; - secrets: Record; - } { - const jwtSecretsRaw = this.configService.get('JWT_SECRETS'); - const currentVersion = this.configService.get('JWT_SECRET_CURRENT_VERSION') || null; - if (!jwtSecretsRaw) { - const secret = this.configService.get('JWT_SECRET') || 'your-secret-key'; - return { - currentVersion, - secrets: currentVersion ? { [currentVersion]: secret } : { default: secret }, - }; - } - return { currentVersion, secrets: this.parseJwtSecrets(jwtSecretsRaw) }; - } - private parseJwtSecrets(raw: string): Record { - try { - const parsed = JSON.parse(raw) as unknown; - if (parsed && typeof parsed === 'object') { - return parsed as Record; - } - } - catch { - // ignore - } - return raw - .split(',') - .map((pair) => pair.trim()) - .filter(Boolean) - .reduce>((acc, pair) => { - const idx = pair.indexOf(':'); - if (idx <= 0) - return acc; - const version = pair.slice(0, idx).trim(); - const secret = pair.slice(idx + 1).trim(); - if (!version || !secret) - return acc; - acc[version] = secret; - return acc; - }, {}); - } -} diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts deleted file mode 100644 index a7e5af7d..00000000 --- a/src/auth/jwt.strategy.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; -import { ConfigService } from '@nestjs/config'; -import { JwtService } from '@nestjs/jwt'; -import type { Request } from 'express'; -import * as jwt from 'jsonwebtoken'; -import { UserRole } from '../users/entities/user.entity'; - -interface IJwtPayload { - sub: string; - email: string; - role: UserRole; - sid: string; -} - -interface IValidatedUser { - sub: string; - email: string; - role: UserRole; - sid: string; -} - -/** - * Implements the jwt strategy. - */ -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { - constructor( - private readonly configService: ConfigService, - private readonly jwtService: JwtService, - ) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - passReqToCallback: true, - secretOrKeyProvider: ( - req: Request, - rawJwtToken: string, - done: (err: any, secret?: string) => void, - ) => { - const { secrets } = this.getJwtAccessSecrets(); - const decoded = jwt.decode(rawJwtToken, { complete: true }) as jwt.Jwt | null; - const kid = - decoded && typeof decoded === 'object' ? (decoded.header as any)?.kid : undefined; - - if (kid && secrets[kid]) { - (req as any).jwtAccessSecretVersionUsed = kid; - return done(null, secrets[kid]); - } - - const entries = Object.entries(secrets); - if (entries.length === 0) { - (req as any).jwtAccessSecretVersionUsed = null; - return done(null, this.configService.get('JWT_SECRET') || 'your-secret-key'); - } - - for (const [version, secret] of entries) { - try { - jwt.verify(rawJwtToken, secret); - (req as any).jwtAccessSecretVersionUsed = version; - return done(null, secret); - } catch { - // try next - } - } - - (req as any).jwtAccessSecretVersionUsed = null; - return done(null, entries[0][1]); - }, - }); - } - - async validate(req: Request, payload: IJwtPayload): Promise { - const { currentVersion, currentSecret } = this.getCurrentJwtAccessSecret(); - const token = this.extractBearerToken(req); - const tokenKid = token ? this.getTokenKid(token) : null; - const usedVersion = (req as any).jwtAccessSecretVersionUsed ?? tokenKid; - const shouldReissue = !!currentVersion && !!usedVersion && usedVersion !== currentVersion; - - if (shouldReissue && (req as any)?.res) { - const newAccessToken = await this.jwtService.signAsync( - { - sub: payload.sub, - email: payload.email, - role: payload.role, - sid: payload.sid, - }, - { - secret: currentSecret, - expiresIn: parseInt(this.configService.get('JWT_EXPIRES_IN') || '900', 10), - header: { kid: currentVersion }, - }, - ); - (req as any).res.setHeader('x-access-token', newAccessToken); - } - async validate(req: Request, payload: JwtPayload): Promise { - const { currentVersion, currentSecret } = this.getCurrentJwtAccessSecret(); - const token = this.extractBearerToken(req); - const tokenKid = token ? this.getTokenKid(token) : null; - const usedVersion = (req as unknown).jwtAccessSecretVersionUsed ?? tokenKid; - const shouldReissue = !!currentVersion && !!usedVersion && usedVersion !== currentVersion; - if (shouldReissue && (req as unknown)?.res) { - const newAccessToken = await this.jwtService.signAsync({ - sub: payload.sub, - email: payload.email, - role: payload.role, - sid: payload.sid, - }, { - secret: currentSecret, - expiresIn: parseInt(this.configService.get('JWT_EXPIRES_IN') || '900', 10), - header: { kid: currentVersion }, - }); - (req as unknown).res.setHeader('x-access-token', newAccessToken); - } - return { sub: payload.sub, email: payload.email, role: payload.role, sid: payload.sid }; - } - private extractBearerToken(req: Request): string | null { - const header = req.headers.authorization; - if (!header) - return null; - const [type, token] = header.split(' '); - if (!token || type.toLowerCase() !== 'bearer') - return null; - return token; - } - private getTokenKid(token: string): string | null { - const decoded = jwt.decode(token, { complete: true }) as jwt.Jwt | null; - const kid = decoded && typeof decoded === 'object' ? (decoded.header as unknown)?.kid : undefined; - return typeof kid === 'string' ? kid : null; - } - private getJwtAccessSecrets(): { - currentVersion: string | null; - secrets: Record; - } { - const jwtSecretsRaw = this.configService.get('JWT_SECRETS'); - const currentVersion = this.configService.get('JWT_SECRET_CURRENT_VERSION') || null; - if (!jwtSecretsRaw) { - const secret = this.configService.get('JWT_SECRET') || 'your-secret-key'; - return { - currentVersion, - secrets: currentVersion ? { [currentVersion]: secret } : { default: secret }, - }; - } - return { currentVersion, secrets: this.parseJwtSecrets(jwtSecretsRaw) }; - } - private getCurrentJwtAccessSecret(): { - currentVersion: string | null; - currentSecret: string; - } { - const { currentVersion, secrets } = this.getJwtAccessSecrets(); - const currentSecret = (currentVersion && secrets[currentVersion]) || this.configService.get('JWT_SECRET'); - return { - currentVersion, - currentSecret: currentSecret || Object.values(secrets)[0] || 'your-secret-key', - }; - } - private parseJwtSecrets(raw: string): Record { - try { - const parsed = JSON.parse(raw) as unknown; - if (parsed && typeof parsed === 'object') { - return parsed as Record; - } - } - catch { - // ignore - } - return raw - .split(',') - .map((pair) => pair.trim()) - .filter(Boolean) - .reduce>((acc, pair) => { - const idx = pair.indexOf(':'); - if (idx <= 0) - return acc; - const version = pair.slice(0, idx).trim(); - const secret = pair.slice(idx + 1).trim(); - if (!version || !secret) - return acc; - acc[version] = secret; - return acc; - }, {}); - } -} diff --git a/src/auth/services/password-policy.service.ts b/src/auth/services/password-policy.service.ts deleted file mode 100644 index 39c4d69b..00000000 --- a/src/auth/services/password-policy.service.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { createHash } from 'crypto'; -import axios from 'axios'; -import { - calculatePasswordStrength, - IPasswordStrengthResult, -} from '../../common/validators/password.validator'; - -const DEFAULT_MIN_LENGTH = 12; -const HIBP_RANGE_ENDPOINT = 'https://api.pwnedpasswords.com/range'; - -interface IBreachCheckResult { - checked: boolean; - breached: boolean; - breachCount: number; -} - -@Injectable() -export class PasswordPolicyService { - private readonly logger = new Logger(PasswordPolicyService.name); - - constructor(private readonly configService: ConfigService) {} - - async enforce(password: string): Promise { - const complexity = this.validateComplexity(password); - if (!complexity.isValid) { - throw new BadRequestException(complexity.errors.join('; ')); - } - - const breachResult = await this.checkBreach(password); - if (breachResult.breached) { - throw new BadRequestException( - 'Password has appeared in known data breaches. Choose a different password.', - ); - } - } - - validateComplexity(password: string): IPasswordStrengthResult { - const result = calculatePasswordStrength(password); - const minLength = this.configService.get( - 'PASSWORD_POLICY_MIN_LENGTH', - DEFAULT_MIN_LENGTH, - ); - - if (password.length < minLength) { - result.errors.push(`Password must be at least ${minLength} characters long`); - } - - if (/\s/.test(password)) { - result.errors.push('Password must not contain whitespace'); - } - - if (/(password|qwerty|123456|letmein|admin)/i.test(password)) { - result.errors.push('Password must not include common weak patterns'); - } - - return { - ...result, - isValid: result.errors.length === 0, - }; - } - - async checkBreach(password: string): Promise { - const breachCheckEnabled = - this.configService.get('PASSWORD_BREACH_CHECK_ENABLED', 'true') !== 'false'; - - if (!breachCheckEnabled || process.env.NODE_ENV === 'test') { - return { checked: false, breached: false, breachCount: 0 }; - } - - const hash = createHash('sha1').update(password).digest('hex').toUpperCase(); - const prefix = hash.slice(0, 5); - const suffix = hash.slice(5); - - try { - const response = await axios.get(`${HIBP_RANGE_ENDPOINT}/${prefix}`, { - timeout: 5000, - headers: { - 'Add-Padding': 'true', - }, - responseType: 'text', - }); - - const lines = response.data.split('\n'); - const breachThreshold = this.configService.get('PASSWORD_BREACH_MIN_COUNT', 1); - - for (const line of lines) { - const [hashSuffix, countRaw] = line.trim().split(':'); - if (!hashSuffix || !countRaw) { - continue; - } - - if (hashSuffix === suffix) { - const breachCount = Number.parseInt(countRaw, 10) || 0; - return { - checked: true, - breached: breachCount >= breachThreshold, - breachCount, - }; - } - } - - return { checked: true, breached: false, breachCount: 0 }; - } catch (error) { - const failClosed = - this.configService.get('PASSWORD_BREACH_CHECK_FAIL_CLOSED', 'false') === 'true'; - this.logger.warn( - 'Password breach check failed', - error instanceof Error ? error.message : String(error), - ); - - if (failClosed) { - throw new BadRequestException( - 'Password breach verification unavailable. Please try again.', - ); - } - - return { checked: false, breached: false, breachCount: 0 }; - } - } -} diff --git a/src/auth/strategies/jwt.strategy.ts b/src/auth/strategies/jwt.strategy.ts deleted file mode 100644 index d2673562..00000000 --- a/src/auth/strategies/jwt.strategy.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; -import { ConfigService } from '@nestjs/config'; -import { JwtService } from '@nestjs/jwt'; -import type { Request } from 'express'; -import * as jwt from 'jsonwebtoken'; - -export interface IJwtPayload { - sub: string; - email: string; - role?: string; - roles?: string[]; - sid?: string; -} - -/** - * Implements the jwt strategy. - */ -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { - constructor( - private readonly configService: ConfigService, - private readonly jwtService: JwtService, - ) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - passReqToCallback: true, - secretOrKeyProvider: ( - req: Request, - rawJwtToken: string, - done: (err: any, secret?: string) => void, - ) => { - const { secrets } = this.getJwtAccessSecrets(); - const decoded = jwt.decode(rawJwtToken, { complete: true }) as jwt.Jwt | null; - const kid = - decoded && typeof decoded === 'object' ? (decoded.header as any)?.kid : undefined; - - if (kid && secrets[kid]) { - (req as any).jwtAccessSecretVersionUsed = kid; - return done(null, secrets[kid]); - } - - // No/unknown kid: probe each secret until one verifies the token signature. - const entries = Object.entries(secrets); - if (entries.length === 0) { - (req as any).jwtAccessSecretVersionUsed = null; - return done(null, this.configService.get('JWT_SECRET') || 'your-secret-key'); - } - - for (const [version, secret] of entries) { - try { - jwt.verify(rawJwtToken, secret); - (req as any).jwtAccessSecretVersionUsed = version; - return done(null, secret); - } catch { - // try next - } - } - - // Fall back to first secret so passport-jwt returns a consistent 401 on bad signature. - (req as any).jwtAccessSecretVersionUsed = null; - return done(null, entries[0][1]); - }, - }); - } - - async validate(req: Request, payload: IJwtPayload) { - const roles = payload.roles || (payload.role ? [payload.role] : []); - - const { currentVersion, currentSecret } = this.getCurrentJwtAccessSecret(); - const token = this.extractBearerToken(req); - const tokenKid = token ? this.getTokenKid(token) : null; - const usedVersion = (req as any).jwtAccessSecretVersionUsed ?? tokenKid; - const shouldReissue = !!currentVersion && !!usedVersion && usedVersion !== currentVersion; - - if (shouldReissue && (req as any)?.res) { - const newAccessToken = await this.jwtService.signAsync( - { - sub: payload.sub, - email: payload.email, - role: roles[0], - roles, - sid: payload.sid, - }, - { - secret: currentSecret, - expiresIn: parseInt(this.configService.get('JWT_EXPIRES_IN') || '900', 10), - header: { kid: currentVersion }, - }, - ); - (req as any).res.setHeader('x-access-token', newAccessToken); - } - async validate(req: Request, payload: JwtPayload) { - const roles = payload.roles || (payload.role ? [payload.role] : []); - const { currentVersion, currentSecret } = this.getCurrentJwtAccessSecret(); - const token = this.extractBearerToken(req); - const tokenKid = token ? this.getTokenKid(token) : null; - const usedVersion = (req as unknown).jwtAccessSecretVersionUsed ?? tokenKid; - const shouldReissue = !!currentVersion && !!usedVersion && usedVersion !== currentVersion; - if (shouldReissue && (req as unknown)?.res) { - const newAccessToken = await this.jwtService.signAsync({ - sub: payload.sub, - email: payload.email, - role: roles[0], - roles, - sid: payload.sid, - }, { - secret: currentSecret, - expiresIn: parseInt(this.configService.get('JWT_EXPIRES_IN') || '900', 10), - header: { kid: currentVersion }, - }); - (req as unknown).res.setHeader('x-access-token', newAccessToken); - } - return { - userId: payload.sub, - email: payload.email, - roles, - sessionId: payload.sid, - }; - } - private extractBearerToken(req: Request): string | null { - const header = req.headers.authorization; - if (!header) - return null; - const [type, token] = header.split(' '); - if (!token || type.toLowerCase() !== 'bearer') - return null; - return token; - } - private getTokenKid(token: string): string | null { - const decoded = jwt.decode(token, { complete: true }) as jwt.Jwt | null; - const kid = decoded && typeof decoded === 'object' ? (decoded.header as unknown)?.kid : undefined; - return typeof kid === 'string' ? kid : null; - } - private getJwtAccessSecrets(): { - currentVersion: string | null; - secrets: Record; - } { - const jwtSecretsRaw = this.configService.get('JWT_SECRETS'); - const currentVersion = this.configService.get('JWT_SECRET_CURRENT_VERSION') || null; - if (!jwtSecretsRaw) { - const secret = this.configService.get('JWT_SECRET') || 'your-secret-key'; - return { - currentVersion, - secrets: currentVersion ? { [currentVersion]: secret } : { default: secret }, - }; - } - return { currentVersion, secrets: this.parseJwtSecrets(jwtSecretsRaw) }; - } - private getCurrentJwtAccessSecret(): { - currentVersion: string | null; - currentSecret: string; - } { - const { currentVersion, secrets } = this.getJwtAccessSecrets(); - const currentSecret = (currentVersion && secrets[currentVersion]) || this.configService.get('JWT_SECRET'); - return { - currentVersion, - currentSecret: currentSecret || Object.values(secrets)[0] || 'your-secret-key', - }; - } - private parseJwtSecrets(raw: string): Record { - try { - const parsed = JSON.parse(raw) as unknown; - if (parsed && typeof parsed === 'object') { - return parsed as Record; - } - } - catch { - // ignore - } - return raw - .split(',') - .map((pair) => pair.trim()) - .filter(Boolean) - .reduce>((acc, pair) => { - const idx = pair.indexOf(':'); - if (idx <= 0) - return acc; - const version = pair.slice(0, idx).trim(); - const secret = pair.slice(idx + 1).trim(); - if (!version || !secret) - return acc; - acc[version] = secret; - return acc; - }, {}); - } -} diff --git a/src/backup/backup.controller.ts b/src/backup/backup.controller.ts deleted file mode 100644 index 93d9e91c..00000000 --- a/src/backup/backup.controller.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Controller, Post, Get, Param, Body, ParseUUIDPipe, HttpCode, HttpStatus, UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { RecoveryTestingService } from './testing/recovery-testing.service'; -import { DisasterRecoveryService } from './disaster-recovery/disaster-recovery.service'; -import { BackupMonitoringService } from './monitoring/backup-monitoring.service'; -import { RestoreBackupDto } from './dto/restore-backup.dto'; -import { TriggerRecoveryTestDto } from './dto/trigger-recovery-test.dto'; -import { RecoveryTestResponseDto } from './dto/recovery-test-response.dto'; - -/** - * Exposes backup endpoints. - */ -@ApiTags('backup') -@ApiBearerAuth() -@UseGuards(JwtAuthGuard) -@Controller('backup') -export class BackupController { - constructor( - private readonly recoveryTestingService: RecoveryTestingService, - private readonly disasterRecoveryService: DisasterRecoveryService, - private readonly backupMonitoringService: BackupMonitoringService, - ) {} - - /** - * Executes restore Backup. - * @param dto The dto. - * @returns The operation result. - */ - @Post('restore') - @ApiOperation({ summary: 'Restore from backup' }) - @ApiResponse({ status: HttpStatus.ACCEPTED, description: 'Restore initiated' }) - @HttpCode(HttpStatus.ACCEPTED) - async restoreBackup(@Body() dto: RestoreBackupDto): Promise<{ message: string }> { - await this.disasterRecoveryService.executeRestore(dto.backupRecordId); - return { message: 'Restore initiated' }; - } - - /** - * Executes trigger Recovery Test. - * @param dto The dto. - * @returns The resulting recovery test response dto. - */ - @Post('test') - @ApiOperation({ summary: 'Trigger recovery test' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Recovery test triggered' }) - async triggerRecoveryTest(@Body() dto: TriggerRecoveryTestDto): Promise { - return this.recoveryTestingService.createRecoveryTest(dto.backupRecordId); - } - - /** - * Returns recovery Test. - * @param testId The test identifier. - * @returns The resulting recovery test response dto. - */ - @Get('test/:testId') - @ApiOperation({ summary: 'Get recovery test results' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Recovery test results', - type: RecoveryTestResponseDto, - }) - async getRecoveryTest( - @Param('testId', ParseUUIDPipe) testId: string, - ): Promise { - return this.recoveryTestingService.getTestResults(testId); - } - - /** - * Returns backup Health. - * @returns The operation result. - */ - @Get('health') - @ApiOperation({ summary: 'Get backup system health' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Backup health status' }) - async getBackupHealth(): Promise<{ healthy: boolean; issues: string[] }> { - return this.backupMonitoringService.checkBackupHealth(); - } -} diff --git a/src/backup/backup.module.ts b/src/backup/backup.module.ts deleted file mode 100644 index 06d16673..00000000 --- a/src/backup/backup.module.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { BullModule } from '@nestjs/bull'; -import { ConfigModule } from '@nestjs/config'; -import { ScheduleModule } from '@nestjs/schedule'; -import { QUEUE_NAMES } from '../common/constants/queue.constants'; -// Entities -import { BackupRecord } from './entities/backup-record.entity'; -import { RecoveryTest } from './entities/recovery-test.entity'; -// Services -import { BackupService } from './backup.service'; -import { DisasterRecoveryService } from './disaster-recovery/disaster-recovery.service'; -import { DataIntegrityService } from './integrity/data-integrity.service'; -import { RecoveryTestingService } from './testing/recovery-testing.service'; -import { BackupMonitoringService } from './monitoring/backup-monitoring.service'; -// Controller -import { BackupController } from './backup.controller'; -// Processor -import { BackupQueueProcessor } from './processing/backup-queue.processor'; -// External modules -import { MediaModule } from '../media/media.module'; -import { MonitoringModule } from '../monitoring/monitoring.module'; - -/** - * Registers the backup module. - */ -@Module({ - imports: [ - ConfigModule, - ScheduleModule.forRoot(), - TypeOrmModule.forFeature([BackupRecord, RecoveryTest]), - BullModule.registerQueue({ - name: QUEUE_NAMES.BACKUP_PROCESSING, - }), - MediaModule, // For FileStorageService - MonitoringModule, // For AlertingService and MetricsCollectionService - ], - controllers: [BackupController], - providers: [ - BackupService, - DisasterRecoveryService, - DataIntegrityService, - RecoveryTestingService, - BackupMonitoringService, - BackupQueueProcessor, - ], - exports: [BackupService, DisasterRecoveryService], -}) -export class BackupModule { -} diff --git a/src/backup/backup.service.spec.ts b/src/backup/backup.service.spec.ts deleted file mode 100644 index 7a7c4dbf..00000000 --- a/src/backup/backup.service.spec.ts +++ /dev/null @@ -1,564 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { getQueueToken } from '@nestjs/bull'; -import { BackupService } from './backup.service'; -import { BackupRecord } from './entities/backup-record.entity'; -import { BackupStatus, BackupType, Region } from './enums/backup-status.enum'; -import { QUEUE_NAMES, JOB_NAMES } from '../common/constants/queue.constants'; -import { createMockRepository, createMockConfigService, createMockQueue, } from 'test/utils/mock-factories'; -import { Repository } from 'typeorm'; -describe('BackupService', () => { - // ───────────────────────────────────────────────────────────────────────── - // DECLARATIONS - // ───────────────────────────────────────────────────────────────────────── - let service: BackupService; - let mockBackupRepo: jest.Mocked>; - let mockBackupQueue: jest.Mocked; - let mockConfigService: jest.Mocked; - let mockAlertingService: jest.Mocked; - let mockMetricsService: jest.Mocked; - let mockScheduledTaskMonitoringService: jest.Mocked; - // ───────────────────────────────────────────────────────────────────────── - // SETUP & TEARDOWN - // ───────────────────────────────────────────────────────────────────────── - beforeEach(async () => { - // Initialize all dependency mocks - mockBackupRepo = createMockRepository(); - mockBackupQueue = createMockQueue(); - mockConfigService = createMockConfigService({ - BACKUP_RETENTION_DAYS: 30, - BACKUP_SCHEDULED_TASK_RETRY_LIMIT: 2, - BACKUP_SCHEDULED_TASK_RETRY_DELAY_MS: 10000, - BACKUP_SCHEDULED_TASK_TIMEOUT_MS: 30 * 60 * 1000, - BACKUP_PRIMARY_REGION: Region.US_EAST_1, - DB_DATABASE: 'teachlink', - }); - mockAlertingService = { - sendAlert: jest.fn(), - }; - mockMetricsService = { - recordMetric: jest.fn(), - }; - mockScheduledTaskMonitoringService = { - registerTask: jest.fn(), - startExecution: jest.fn().mockReturnValue('execution-1'), - markSuccess: jest.fn(), - markFailure: jest.fn(), - recordRetry: jest.fn(), - }; - const module: TestingModule = await Test.createTestingModule({ - providers: [ - BackupService, - { - provide: getRepositoryToken(BackupRecord), - useValue: mockBackupRepo, - }, - { - provide: getQueueToken(QUEUE_NAMES.BACKUP_PROCESSING), - useValue: mockBackupQueue, - }, - { - provide: 'ConfigService', - useValue: mockConfigService, - }, - { - provide: 'AlertingService', - useValue: mockAlertingService, - }, - { - provide: 'MetricsCollectionService', - useValue: mockMetricsService, - }, - { - provide: 'ScheduledTaskMonitoringService', - useValue: mockScheduledTaskMonitoringService, - }, - ], - }).compile(); - service = module.get(BackupService); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - // ───────────────────────────────────────────────────────────────────────── - // TEST SUITES - // ───────────────────────────────────────────────────────────────────────── - describe('constructor', () => { - it('should initialize with default configuration values', () => { - const defaultConfigService = createMockConfigService({}); - const defaultService = new (BackupService as unknown)(mockBackupRepo, mockBackupQueue, defaultConfigService, mockAlertingService, mockMetricsService, mockScheduledTaskMonitoringService); - expect((defaultService as unknown).retentionDays).toBe(30); - expect((defaultService as unknown).scheduledTaskRetryLimit).toBe(2); - expect((defaultService as unknown).scheduledTaskRetryDelayMs).toBe(10000); - expect((defaultService as unknown).scheduledTaskTimeoutMs).toBe(30 * 60 * 1000); - }); - it('should register scheduled tasks', () => { - expect(mockScheduledTaskMonitoringService.registerTask).toHaveBeenCalledWith('weekly-database-backup', { - expectedIntervalMs: 7 * 24 * 60 * 60 * 1000, - timeoutMs: 30 * 60 * 1000, - maxRetries: 2, - }); - expect(mockScheduledTaskMonitoringService.registerTask).toHaveBeenCalledWith('cleanup-expired-backups', { - expectedIntervalMs: 24 * 60 * 60 * 1000, - timeoutMs: 30 * 60 * 1000, - maxRetries: 2, - }); - }); - }); - describe('handleScheduledBackup', () => { - const mockBackupRecord = { - id: 'backup-1', - backupType: BackupType.FULL, - status: BackupStatus.PENDING, - region: Region.US_EAST_1, - databaseName: 'teachlink', - storageKey: '', - expiresAt: new Date(), - metadata: { startTime: new Date() }, - }; - beforeEach(() => { - mockScheduledTaskMonitoringService.startExecution.mockReturnValue('execution-1'); - mockBackupRepo.create.mockReturnValue(mockBackupRecord as BackupRecord); - mockBackupRepo.save.mockResolvedValue(mockBackupRecord as BackupRecord); - mockBackupQueue.add.mockResolvedValue({} as unknown); - mockScheduledTaskMonitoringService.markSuccess.mockResolvedValue(); - }); - it('should create and queue a scheduled backup', async () => { - await service.handleScheduledBackup(); - expect(mockScheduledTaskMonitoringService.startExecution).toHaveBeenCalledWith('weekly-database-backup', { - expectedIntervalMs: 7 * 24 * 60 * 60 * 1000, - timeoutMs: 30 * 60 * 1000, - maxRetries: 2, - }, { source: 'BackupService' }); - expect(mockBackupRepo.create).toHaveBeenCalledWith(expect.objectContaining({ - backupType: BackupType.FULL, - status: BackupStatus.PENDING, - region: Region.US_EAST_1, - databaseName: 'teachlink', - storageKey: '', - metadata: expect.objectContaining({ - startTime: expect.any(Date), - }), - })); - expect(mockBackupQueue.add).toHaveBeenCalledWith(JOB_NAMES.CREATE_BACKUP, { - backupRecordId: 'backup-1', - backupType: BackupType.FULL, - region: Region.US_EAST_1, - databaseName: 'teachlink', - }, { - attempts: 3, - backoff: { - type: 'exponential', - delay: 10000, - }, - timeout: 3600000, - }); - expect(mockScheduledTaskMonitoringService.markSuccess).toHaveBeenCalledWith('execution-1', { - attempt: 1, - maxAttempts: 3, - retriesUsed: 0, - }); - }); - it('should handle backup creation failure and retry', async () => { - mockBackupRepo.save.mockRejectedValueOnce(new Error('Database error')); - mockBackupRepo.save.mockResolvedValueOnce(mockBackupRecord as BackupRecord); - await service.handleScheduledBackup(); - expect(mockScheduledTaskMonitoringService.recordRetry).toHaveBeenCalledWith('weekly-database-backup', 1, 2, 'Database error'); - expect(mockScheduledTaskMonitoringService.markSuccess).toHaveBeenCalledWith('execution-1', { - attempt: 2, - maxAttempts: 3, - retriesUsed: 1, - }); - }); - it('should handle complete failure after retries', async () => { - const error = new Error('Persistent error'); - mockBackupRepo.save.mockRejectedValue(error); - await service.handleScheduledBackup(); - expect(mockScheduledTaskMonitoringService.markFailure).toHaveBeenCalledWith('execution-1', 'Persistent error', { - attempt: 3, - maxAttempts: 3, - retriesUsed: 2, - }); - expect(mockAlertingService.sendAlert).toHaveBeenCalledWith('BACKUP_SCHEDULED_FAILED', 'Scheduled task weekly-database-backup failed after 3 attempt(s): Persistent error', 'CRITICAL'); - }); - }); - - it('should create and queue a scheduled backup', async () => { - await service.handleScheduledBackup(); - - expect(mockScheduledTaskMonitoringService.startExecution).toHaveBeenCalledWith( - 'weekly-database-backup', - { - expectedIntervalMs: 7 * 24 * 60 * 60 * 1000, - timeoutMs: 30 * 60 * 1000, - maxRetries: 2, - }, - { source: 'BackupService' }, - ); - - expect(mockBackupRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - backupType: BackupType.FULL, - status: BackupStatus.PENDING, - region: Region.US_EAST_1, - databaseName: 'teachlink', - storageKey: '', - metadata: expect.objectContaining({ - startTime: expect.any(Date), - }), - }), - ); - - expect(mockBackupQueue.add).toHaveBeenCalledWith( - JOB_NAMES.CREATE_BACKUP, - { - backupRecordId: 'backup-1', - backupType: BackupType.FULL, - region: Region.US_EAST_1, - databaseName: 'teachlink', - }, - { - attempts: 3, - backoff: { - type: 'exponential', - delay: 10000, - }, - timeout: 3600000, - }, - ); - - expect(mockScheduledTaskMonitoringService.markSuccess).toHaveBeenCalledWith('execution-1', { - attempt: 1, - maxAttempts: 3, - retriesUsed: 0, - }); - }); - - it('should handle backup creation failure and retry', async () => { - mockBackupRepo.save.mockRejectedValueOnce(new Error('Database error')); - mockBackupRepo.save.mockResolvedValueOnce(mockBackupRecord as BackupRecord); - - await service.handleScheduledBackup(); - - expect(mockScheduledTaskMonitoringService.recordRetry).toHaveBeenCalledWith( - 'weekly-database-backup', - 1, - 2, - 'Database error', - ); - expect(mockScheduledTaskMonitoringService.markSuccess).toHaveBeenCalledWith('execution-1', { - attempt: 2, - maxAttempts: 3, - retriesUsed: 1, - }); - }); - describe('updateBackupStatus', () => { - const backupId = 'backup-1'; - const updates = { errorMessage: 'Test error' }; - beforeEach(() => { - mockBackupRepo.update.mockResolvedValue({ affected: 1, raw: {}, generatedMaps: [] }); - }); - it('should update backup status to completed and send success alert', async () => { - await service.updateBackupStatus(backupId, BackupStatus.COMPLETED, updates); - expect(mockBackupRepo.update).toHaveBeenCalledWith(backupId, { - status: BackupStatus.COMPLETED, - ...updates, - updatedAt: expect.any(Date), - }); - expect(mockAlertingService.sendAlert).toHaveBeenCalledWith('BACKUP_COMPLETED', `Backup ${backupId} completed successfully`, 'INFO'); - }); - it('should update backup status to failed and send critical alert', async () => { - await service.updateBackupStatus(backupId, BackupStatus.FAILED, updates); - expect(mockAlertingService.sendAlert).toHaveBeenCalledWith('BACKUP_FAILED', `Backup ${backupId} failed: Test error`, 'CRITICAL'); - }); - it('should update status without sending alert for other statuses', async () => { - await service.updateBackupStatus(backupId, BackupStatus.IN_PROGRESS); - expect(mockAlertingService.sendAlert).not.toHaveBeenCalled(); - }); - }); - describe('toResponseDto', () => { - const mockBackup = { - id: 'backup-1', - backupType: BackupType.FULL, - status: BackupStatus.COMPLETED, - region: Region.US_EAST_1, - databaseName: 'teachlink', - backupSizeBytes: 1024000, - integrityVerified: true, - completedAt: new Date('2024-01-15T10:00:00Z'), - expiresAt: new Date('2024-02-15T10:00:00Z'), - createdAt: new Date('2024-01-15T09:00:00Z'), - metadata: { version: '1.0' }, - storageKey: 'backup-key', - updatedAt: new Date(), - }; - it('should convert backup record to response DTO', () => { - const result = service.toResponseDto(mockBackup as BackupRecord); - expect(result).toEqual({ - id: 'backup-1', - backupType: BackupType.FULL, - status: BackupStatus.COMPLETED, - region: Region.US_EAST_1, - databaseName: 'teachlink', - backupSizeBytes: 1024000, - integrityVerified: true, - completedAt: new Date('2024-01-15T10:00:00Z'), - expiresAt: new Date('2024-02-15T10:00:00Z'), - createdAt: new Date('2024-01-15T09:00:00Z'), - metadata: { version: '1.0' }, - }); - }); - }); - - it('should find and queue deletion of expired backups', async () => { - await service.handleBackupCleanup(); - - expect(mockBackupRepo.find).toHaveBeenCalledWith({ - where: { - createdAt: expect.any(Object), // LessThan operator - status: BackupStatus.COMPLETED, - }, - }); - - expect(mockBackupQueue.add).toHaveBeenCalledTimes(2); - expect(mockBackupQueue.add).toHaveBeenCalledWith( - JOB_NAMES.DELETE_BACKUP, - { backupRecordId: 'backup-1' }, - { - attempts: 3, - backoff: { type: 'exponential', delay: 5000 }, - }, - ); - expect(mockBackupQueue.add).toHaveBeenCalledWith( - JOB_NAMES.DELETE_BACKUP, - { backupRecordId: 'backup-2' }, - { - attempts: 3, - backoff: { type: 'exponential', delay: 5000 }, - }, - ); - - expect(mockScheduledTaskMonitoringService.markSuccess).toHaveBeenCalledWith('execution-1', { - attempt: 1, - maxAttempts: 3, - retriesUsed: 0, - }); - }); - - it('should handle cleanup failure and retry', async () => { - mockBackupRepo.find.mockRejectedValueOnce(new Error('Query error')); - mockBackupRepo.find.mockResolvedValueOnce([]); - - await service.handleBackupCleanup(); - - expect(mockScheduledTaskMonitoringService.recordRetry).toHaveBeenCalledWith( - 'cleanup-expired-backups', - 1, - 2, - 'Query error', - ); - }); - }); - - describe('getLatestBackup', () => { - const mockBackup = { - id: 'backup-1', - status: BackupStatus.COMPLETED, - integrityVerified: true, - completedAt: new Date('2024-01-15'), - }; - - beforeEach(() => { - mockBackupRepo.findOne.mockResolvedValue(mockBackup as BackupRecord); - }); - - it('should return latest completed and verified backup', async () => { - const result = await service.getLatestBackup(); - - expect(result).toEqual(mockBackup); - expect(mockBackupRepo.findOne).toHaveBeenCalledWith({ - where: { - status: BackupStatus.COMPLETED, - integrityVerified: true, - }, - order: { completedAt: 'DESC' }, - }); - }); - - it('should filter by region when specified', async () => { - await service.getLatestBackup(Region.EU_WEST_1); - - expect(mockBackupRepo.findOne).toHaveBeenCalledWith({ - where: { - status: BackupStatus.COMPLETED, - integrityVerified: true, - region: Region.EU_WEST_1, - }, - order: { completedAt: 'DESC' }, - }); - }); - - it('should return null when no backup found', async () => { - mockBackupRepo.findOne.mockResolvedValue(null); - - const result = await service.getLatestBackup(); - - expect(result).toBeNull(); - }); - }); - - describe('updateBackupStatus', () => { - const backupId = 'backup-1'; - const updates = { errorMessage: 'Test error' }; - - beforeEach(() => { - mockBackupRepo.update.mockResolvedValue({ affected: 1, raw: {}, generatedMaps: [] }); - }); - - it('should update backup status to completed and send success alert', async () => { - await service.updateBackupStatus(backupId, BackupStatus.COMPLETED, updates); - - expect(mockBackupRepo.update).toHaveBeenCalledWith(backupId, { - status: BackupStatus.COMPLETED, - ...updates, - updatedAt: expect.any(Date), - }); - - expect(mockAlertingService.sendAlert).toHaveBeenCalledWith( - 'BACKUP_COMPLETED', - `Backup ${backupId} completed successfully`, - 'INFO', - ); - }); - - it('should update backup status to failed and send critical alert', async () => { - await service.updateBackupStatus(backupId, BackupStatus.FAILED, updates); - - expect(mockAlertingService.sendAlert).toHaveBeenCalledWith( - 'BACKUP_FAILED', - `Backup ${backupId} failed: Test error`, - 'CRITICAL', - ); - }); - - it('should update status without sending alert for other statuses', async () => { - await service.updateBackupStatus(backupId, BackupStatus.IN_PROGRESS); - - expect(mockAlertingService.sendAlert).not.toHaveBeenCalled(); - }); - }); - - describe('toResponseDto', () => { - const mockBackup = { - id: 'backup-1', - backupType: BackupType.FULL, - status: BackupStatus.COMPLETED, - region: Region.US_EAST_1, - databaseName: 'teachlink', - backupSizeBytes: 1024000, - integrityVerified: true, - completedAt: new Date('2024-01-15T10:00:00Z'), - expiresAt: new Date('2024-02-15T10:00:00Z'), - createdAt: new Date('2024-01-15T09:00:00Z'), - metadata: { version: '1.0' }, - storageKey: 'backup-key', - updatedAt: new Date(), - }; - - it('should convert backup record to response DTO', () => { - const result = service.toResponseDto(mockBackup as BackupRecord); - - expect(result).toEqual({ - id: 'backup-1', - backupType: BackupType.FULL, - status: BackupStatus.COMPLETED, - region: Region.US_EAST_1, - databaseName: 'teachlink', - backupSizeBytes: 1024000, - integrityVerified: true, - completedAt: new Date('2024-01-15T10:00:00Z'), - expiresAt: new Date('2024-02-15T10:00:00Z'), - createdAt: new Date('2024-01-15T09:00:00Z'), - metadata: { version: '1.0' }, - }); - }); - }); - - describe('private methods', () => { - describe('executeMonitoredScheduledTask', () => { - const taskConfig = { - expectedIntervalMs: 24 * 60 * 60 * 1000, - timeoutMs: 30 * 60 * 1000, - maxRetries: 2, - }; - - it('should execute task successfully on first attempt', async () => { - const taskRunner = jest.fn().mockResolvedValue(undefined); - - await (service as any).executeMonitoredScheduledTask('test-task', taskConfig, taskRunner); - - expect(taskRunner).toHaveBeenCalledTimes(1); - expect(mockScheduledTaskMonitoringService.markSuccess).toHaveBeenCalledWith('execution-1', { - attempt: 1, - maxAttempts: 3, - retriesUsed: 0, - }); - }); - - it('should retry on failure and succeed', async () => { - const taskRunner = jest - .fn() - .mockRejectedValueOnce(new Error('First attempt failed')) - .mockResolvedValueOnce(undefined); - - await (service as any).executeMonitoredScheduledTask('test-task', taskConfig, taskRunner); - - expect(taskRunner).toHaveBeenCalledTimes(2); - expect(mockScheduledTaskMonitoringService.recordRetry).toHaveBeenCalledWith( - 'test-task', - 1, - 2, - 'First attempt failed', - ); - expect(mockScheduledTaskMonitoringService.markSuccess).toHaveBeenCalledWith('execution-1', { - attempt: 2, - maxAttempts: 3, - retriesUsed: 1, - }); - }); - - it('should fail after all retries exhausted', async () => { - const error = new Error('Persistent failure'); - const taskRunner = jest.fn().mockRejectedValue(error); - - await (service as any).executeMonitoredScheduledTask('test-task', taskConfig, taskRunner); - - expect(taskRunner).toHaveBeenCalledTimes(3); - expect(mockScheduledTaskMonitoringService.markFailure).toHaveBeenCalledWith( - 'execution-1', - 'Persistent failure', - { - attempt: 3, - maxAttempts: 3, - retriesUsed: 2, - }, - ); - expect(mockAlertingService.sendAlert).toHaveBeenCalledWith( - 'BACKUP_SCHEDULED_FAILED', - 'Scheduled task test-task failed after 3 attempt(s): Persistent failure', - 'CRITICAL', - ); - }); - }); - - describe('delay', () => { - it('should delay execution for specified milliseconds', async () => { - const startTime = Date.now(); - await (service as any).delay(100); - const endTime = Date.now(); - - expect(endTime - startTime).toBeGreaterThanOrEqual(95); // Allow some tolerance - }); - }); - }); -}); diff --git a/src/backup/backup.service.ts b/src/backup/backup.service.ts deleted file mode 100644 index 7d2ef038..00000000 --- a/src/backup/backup.service.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, LessThan } from 'typeorm'; -import { InjectQueue } from '@nestjs/bull'; -import { Queue } from 'bull'; -import { QUEUE_NAMES, JOB_NAMES } from '../common/constants/queue.constants'; -import { ConfigService } from '@nestjs/config'; -import { Cron } from '@nestjs/schedule'; -import { BackupRecord } from './entities/backup-record.entity'; -import { BackupStatus } from './enums/backup-status.enum'; -import { BackupType } from './enums/backup-type.enum'; -import { Region } from './enums/region.enum'; -import { BackupResponseDto } from './dto/backup-response.dto'; -import { IBackupJobData } from './interfaces/backup.interfaces'; -import { AlertingService } from '../monitoring/alerting/alerting.service'; -import { MetricsCollectionService } from '../monitoring/metrics/metrics-collection.service'; -import { - IScheduledTaskConfig, - ScheduledTaskMonitoringService, -} from '../monitoring/scheduled-task-monitoring.service'; -import { TIME } from '../common/constants/time.constants'; - -/** - * Provides backup operations. - */ -@Injectable() -export class BackupService { - private readonly logger = new Logger(BackupService.name); - private readonly retentionDays: number; - private readonly scheduledTaskRetryLimit: number; - private readonly scheduledTaskRetryDelayMs: number; - private readonly scheduledTaskTimeoutMs: number; - constructor( - @InjectRepository(BackupRecord) - private readonly backupRepository: Repository, - @InjectQueue(QUEUE_NAMES.BACKUP_PROCESSING) - private readonly backupQueue: Queue, - private readonly configService: ConfigService, - private readonly alertingService: AlertingService, - private readonly metricsService: MetricsCollectionService, - private readonly scheduledTaskMonitoringService: ScheduledTaskMonitoringService, - ) { - this.retentionDays = this.configService.get('BACKUP_RETENTION_DAYS', 30); - this.scheduledTaskRetryLimit = this.configService.get( - 'BACKUP_SCHEDULED_TASK_RETRY_LIMIT', - 2, - ); - this.scheduledTaskRetryDelayMs = this.configService.get( - 'BACKUP_SCHEDULED_TASK_RETRY_DELAY_MS', - 10000, - ); - this.scheduledTaskTimeoutMs = this.configService.get( - 'BACKUP_SCHEDULED_TASK_TIMEOUT_MS', - 30 * TIME.ONE_MINUTE_MS, - ); - - this.scheduledTaskMonitoringService.registerTask('weekly-database-backup', { - expectedIntervalMs: 7 * TIME.ONE_DAY_SECONDS * 1000, - timeoutMs: this.scheduledTaskTimeoutMs, - maxRetries: this.scheduledTaskRetryLimit, - }); - this.scheduledTaskMonitoringService.registerTask('cleanup-expired-backups', { - expectedIntervalMs: TIME.ONE_DAY_SECONDS * 1000, - timeoutMs: this.scheduledTaskTimeoutMs, - maxRetries: this.scheduledTaskRetryLimit, - }); - } - - /** - * Scheduled weekly backup (every Sunday at 2 AM UTC) - */ - @Cron('0 2 * * 0', { - name: 'weekly-database-backup', - timeZone: 'UTC', - }) - async handleScheduledBackup(): Promise { - await this.executeMonitoredScheduledTask( - 'weekly-database-backup', - { - expectedIntervalMs: 7 * 24 * 60 * 60 * 1000, - timeoutMs: this.scheduledTaskTimeoutMs, - maxRetries: this.scheduledTaskRetryLimit, - }, - async () => { - this.logger.log('Starting scheduled weekly backup'); - - const region = - (this.configService.get('BACKUP_PRIMARY_REGION') as Region) || Region.US_EAST_1; - const databaseName = this.configService.get('DB_DATABASE', 'teachlink'); - - const expiresAt = new Date(); - expiresAt.setDate(expiresAt.getDate() + this.retentionDays); - - const backupRecord = this.backupRepository.create({ - backupType: BackupType.FULL, - status: BackupStatus.PENDING, - region, - databaseName, - storageKey: '', - expiresAt, - metadata: { - startTime: new Date(), - }, - }); - - await this.backupRepository.save(backupRecord); - - // Queue backup job - await this.backupQueue.add( - JOB_NAMES.CREATE_BACKUP, - { - backupRecordId: backupRecord.id, - backupType: BackupType.FULL, - region, - databaseName, - } as IBackupJobData, - { - attempts: 3, - backoff: { - type: 'exponential', - delay: TIME.TEN_SECONDS_MS, - }, - timeout: TIME.ONE_HOUR_MS, // 1 hour timeout - }, - ); - - this.logger.log(`Scheduled backup ${backupRecord.id} queued`); - }, - ); - } - - /** - * Cleanup expired backups (daily at 3 AM UTC) - */ - @Cron('0 3 * * *', { - name: 'cleanup-expired-backups', - timeZone: 'UTC', - }) - async handleBackupCleanup(): Promise { - await this.executeMonitoredScheduledTask( - 'cleanup-expired-backups', - { - expectedIntervalMs: 24 * 60 * 60 * 1000, - timeoutMs: this.scheduledTaskTimeoutMs, - maxRetries: this.scheduledTaskRetryLimit, - }, - async () => { - this.logger.log('Starting backup cleanup job'); - - const expirationDate = new Date(); - expirationDate.setDate(expirationDate.getDate() - this.retentionDays); - - const expiredBackups = await this.backupRepository.find({ - where: { - createdAt: LessThan(expirationDate), - status: BackupStatus.COMPLETED, - }, - }); - - this.logger.log(`Found ${expiredBackups.length} expired backups to cleanup`); - - for (const backup of expiredBackups) { - await this.backupQueue.add( - JOB_NAMES.DELETE_BACKUP, - { backupRecordId: backup.id }, - { - attempts: 3, - backoff: { type: 'exponential', delay: TIME.FIVE_SECONDS_MS }, - }, - ); - } - }, - ); - } - - private async executeMonitoredScheduledTask( - taskName: string, - config: IScheduledTaskConfig, - taskRunner: () => Promise, - ): Promise { - const executionId = this.scheduledTaskMonitoringService.startExecution(taskName, config, { - source: BackupService.name, - }); - - const maxAttempts = (config.maxRetries || 0) + 1; - - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - try { - await taskRunner(); - this.scheduledTaskMonitoringService.markSuccess(executionId, { - attempt, - maxAttempts, - retriesUsed: attempt - 1, - }); - } - async updateBackupStatus(backupId: string, status: BackupStatus, updates: Partial = {}): Promise { - await this.backupRepository.update(backupId, { - status, - ...updates, - updatedAt: new Date(), - }); - if (status === BackupStatus.COMPLETED) { - this.alertingService.sendAlert('BACKUP_COMPLETED', `Backup ${backupId} completed successfully`, 'INFO'); - } - else if (status === BackupStatus.FAILED) { - this.alertingService.sendAlert('BACKUP_FAILED', `Backup ${backupId} failed: ${updates.errorMessage}`, 'CRITICAL'); - } - } - } - - private async delay(ms: number): Promise { - await new Promise((resolve) => setTimeout(resolve, ms)); - } - - /** - * Retrieves latest Backup. - * @param region The region. - * @returns The operation result. - */ - async getLatestBackup(region?: Region): Promise { - const where: any = { - status: BackupStatus.COMPLETED, - integrityVerified: true, - }; - if (region) { - where.region = region; - } - - return this.backupRepository.findOne({ - where, - order: { completedAt: 'DESC' }, - }); - } - - /** - * Updates backup Status. - * @param backupId The backup identifier. - * @param status The status value. - * @param updates The updates. - */ - async updateBackupStatus( - backupId: string, - status: BackupStatus, - updates: Partial = {}, - ): Promise { - await this.backupRepository.update(backupId, { - status, - ...updates, - updatedAt: new Date(), - }); - - if (status === BackupStatus.COMPLETED) { - this.alertingService.sendAlert( - 'BACKUP_COMPLETED', - `Backup ${backupId} completed successfully`, - 'INFO', - ); - } else if (status === BackupStatus.FAILED) { - this.alertingService.sendAlert( - 'BACKUP_FAILED', - `Backup ${backupId} failed: ${updates.errorMessage}`, - 'CRITICAL', - ); - } - } - - /** - * Executes to Response Dto. - * @param backup The backup. - * @returns The resulting backup response dto. - */ - toResponseDto(backup: BackupRecord): BackupResponseDto { - return { - id: backup.id, - backupType: backup.backupType, - status: backup.status, - region: backup.region, - databaseName: backup.databaseName, - backupSizeBytes: backup.backupSizeBytes, - integrityVerified: backup.integrityVerified, - completedAt: backup.completedAt, - expiresAt: backup.expiresAt, - createdAt: backup.createdAt, - metadata: backup.metadata, - }; - } -} diff --git a/src/backup/disaster-recovery/disaster-recovery.service.ts b/src/backup/disaster-recovery/disaster-recovery.service.ts deleted file mode 100644 index 54150043..00000000 --- a/src/backup/disaster-recovery/disaster-recovery.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { BackupService } from '../backup.service'; -import { AlertingService } from '../../monitoring/alerting/alerting.service'; -import { FileStorageService } from '../../media/storage/file-storage.service'; -import { KMSClient, DecryptCommand } from '@aws-sdk/client-kms'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import * as fs from 'fs'; -const execAsync = promisify(exec); -const RTO_THRESHOLD_SECONDS = 900; // 15 minutes - -/** - * Provides disaster Recovery operations. - */ -@Injectable() -export class DisasterRecoveryService { - private readonly logger = new Logger(DisasterRecoveryService.name); - private readonly kmsClient: KMSClient; - - constructor( - private readonly backupService: BackupService, - private readonly alertingService: AlertingService, - private readonly fileStorageService: FileStorageService, - private readonly configService: ConfigService, - ) { - const awsRegion = this.configService.get('AWS_REGION', 'us-east-1'); - this.kmsClient = new KMSClient({ region: awsRegion }); - } - - /** - * Executes execute Restore. - * @param backupId The backup identifier. - */ - async executeRestore(backupId: string): Promise { - this.logger.log(`Starting disaster recovery restore for backup: ${backupId}`); - - const rtoStartTime = Date.now(); - - try { - // Step 1: Get latest verified backup - const backup = await this.backupService.getLatestBackup(); - if (!backup) { - throw new Error('No verified backup available for restore'); - } - - // Step 2: Download from secondary region - this.logger.log('Downloading backup from secondary region'); - const backupData = await this.fileStorageService.downloadFile( - backup.replicatedStorageKey || backup.encryptedStorageKey, - ); - - // Step 3: Decrypt with AWS KMS - this.logger.log('Decrypting backup'); - const decryptCommand = new DecryptCommand({ - CiphertextBlob: backupData, - }); - const decryptResponse = await this.kmsClient.send(decryptCommand); - const decryptedData = Buffer.from(decryptResponse.Plaintext); - - // Save to temp file - const tempFile = `/tmp/disaster-recovery-${backupId}.sql`; - await fs.promises.writeFile(tempFile, decryptedData); - - // Step 4: Execute pg_restore to primary database - this.logger.log('Restoring to primary database'); - const databaseName = this.configService.get('DB_DATABASE', 'teachlink'); - await this.restoreDatabase(databaseName, tempFile); - - // Cleanup - await fs.promises.unlink(tempFile); - - // Step 5: Check RTO - const rtoActual = Math.floor((Date.now() - rtoStartTime) / 1000); - this.logger.log(`Disaster recovery completed. RTO: ${rtoActual} seconds`); - - if (rtoActual > RTO_THRESHOLD_SECONDS) { - this.alertingService.sendAlert( - 'DISASTER_RECOVERY_RTO_EXCEEDED', - `Disaster recovery completed but RTO exceeded: ${rtoActual}s > ${RTO_THRESHOLD_SECONDS}s`, - 'CRITICAL', - ); - } else { - this.alertingService.sendAlert( - 'DISASTER_RECOVERY_SUCCESS', - `Disaster recovery completed successfully in ${rtoActual} seconds`, - 'INFO', - ); - } - } catch (error) { - this.logger.error('Disaster recovery failed:', error); - this.alertingService.sendAlert( - 'DISASTER_RECOVERY_FAILED', - `Disaster recovery failed: ${error.message}`, - 'CRITICAL', - ); - throw error; - } -} diff --git a/src/backup/integrity/data-integrity.service.ts b/src/backup/integrity/data-integrity.service.ts deleted file mode 100644 index 111e1e72..00000000 --- a/src/backup/integrity/data-integrity.service.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { BackupRecord } from '../entities/backup-record.entity'; -import { FileStorageService } from '../../media/storage/file-storage.service'; -import * as crypto from 'crypto'; -import * as fs from 'fs'; - -/** - * Provides data Integrity operations. - */ -@Injectable() -export class DataIntegrityService { - private readonly logger = new Logger(DataIntegrityService.name); - constructor( - @InjectRepository(BackupRecord) - private readonly backupRepository: Repository, - private readonly fileStorageService: FileStorageService, - ) {} - - /** - * Validates backup Integrity. - * @param backupId The backup identifier. - * @returns Whether the operation succeeded. - */ - async verifyBackupIntegrity(backupId: string): Promise { - this.logger.log(`Verifying backup integrity for: ${backupId}`); - - const backup = await this.backupRepository.findOne({ - where: { id: backupId }, - }); - - if (!backup) { - throw new NotFoundException(`Backup ${backupId} not found`); - } - async calculateChecksums(filePath: string): Promise<{ - md5: string; - sha256: string; - }> { - const fileBuffer = await fs.promises.readFile(filePath); - const md5 = crypto.createHash('md5').update(fileBuffer).digest('hex'); - const sha256 = crypto.createHash('sha256').update(fileBuffer).digest('hex'); - return { md5, sha256 }; - } - - try { - // Download backup from S3 - const backupData = await this.fileStorageService.downloadFile(backup.encryptedStorageKey); - - // Save to temp file - const tempFile = `/tmp/verify-${backupId}.backup`; - await fs.promises.writeFile(tempFile, backupData); - - // Calculate checksums - const checksums = await this.calculateChecksums(tempFile); - - // Clean up temp file - await fs.promises.unlink(tempFile); - - // Compare checksums - const md5Match = checksums.md5 === backup.checksumMd5; - const sha256Match = checksums.sha256 === backup.checksumSha256; - - if (md5Match && sha256Match) { - this.logger.log(`Backup ${backupId} integrity verified successfully`); - return true; - } else { - this.logger.error( - `Backup ${backupId} integrity verification failed. MD5: ${md5Match}, SHA256: ${sha256Match}`, - ); - return false; - } - } catch (error) { - this.logger.error(`Error verifying backup ${backupId} integrity:`, error); - return false; - } - } - - /** - * Calculates checksums. - * @param filePath The file to process. - * @returns The operation result. - */ - async calculateChecksums(filePath: string): Promise<{ md5: string; sha256: string }> { - const fileBuffer = await fs.promises.readFile(filePath); - - const md5 = crypto.createHash('md5').update(fileBuffer).digest('hex'); - const sha256 = crypto.createHash('sha256').update(fileBuffer).digest('hex'); - - return { md5, sha256 }; - } -} diff --git a/src/backup/monitoring/backup-monitoring.service.ts b/src/backup/monitoring/backup-monitoring.service.ts deleted file mode 100644 index 53fe6973..00000000 --- a/src/backup/monitoring/backup-monitoring.service.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { BackupRecord } from '../entities/backup-record.entity'; -import { RecoveryTest } from '../entities/recovery-test.entity'; -import { BackupStatus } from '../enums/backup-status.enum'; -import { RecoveryTestStatus } from '../enums/recovery-test-status.enum'; -import { MetricsCollectionService } from '../../monitoring/metrics/metrics-collection.service'; -import { AlertingService } from '../../monitoring/alerting/alerting.service'; -import { Histogram, Counter } from 'prom-client'; - -/** - * Provides backup Monitoring operations. - */ -@Injectable() -export class BackupMonitoringService { - private readonly logger = new Logger(BackupMonitoringService.name); - private backupDuration: Histogram; - private backupTotal: Counter; - constructor( - @InjectRepository(BackupRecord) - private readonly backupRepository: Repository, - @InjectRepository(RecoveryTest) - private readonly recoveryTestRepository: Repository, - private readonly metricsService: MetricsCollectionService, - private readonly alertingService: AlertingService, - ) { - this.initializeMetrics(); - } - - private initializeMetrics(): void { - const registry = this.metricsService.getRegistry(); - - this.backupDuration = new Histogram({ - name: 'backup_duration_seconds', - help: 'Duration of backup operations in seconds', - labelNames: ['status'], - buckets: [60, 300, 600, 900, 1200], - registers: [registry], - }); - - this.backupTotal = new Counter({ - name: 'backup_total', - help: 'Total number of backups', - labelNames: ['status'], - registers: [registry], - }); - } - - /** - * Validates backup Health. - * @returns The operation result. - */ - async checkBackupHealth(): Promise<{ healthy: boolean; issues: string[] }> { - const issues: string[] = []; - - // Check last backup was successful - const lastBackup = await this.backupRepository.findOne({ - where: {}, - order: { createdAt: 'DESC' }, - }); - - if (!lastBackup) { - issues.push('No backups found'); - } else { - if (lastBackup.status === BackupStatus.FAILED) { - issues.push(`Last backup failed: ${lastBackup.errorMessage}`); - } - - // Check last backup was within 7 days - const sevenDaysAgo = new Date(); - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - - if (lastBackup.createdAt < sevenDaysAgo) { - issues.push('Last backup is older than 7 days'); - } - - // Check replication - if (lastBackup.status === BackupStatus.COMPLETED && !lastBackup.replicatedStorageKey) { - issues.push('Last backup was not replicated to secondary region'); - } - } - private initializeMetrics(): void { - const registry = this.metricsService.getRegistry(); - this.backupDuration = new Histogram({ - name: 'backup_duration_seconds', - help: 'Duration of backup operations in seconds', - labelNames: ['status'], - buckets: [60, 300, 600, 900, 1200], - registers: [registry], - }); - this.backupTotal = new Counter({ - name: 'backup_total', - help: 'Total number of backups', - labelNames: ['status'], - registers: [registry], - }); - } - - return { - healthy: issues.length === 0, - issues, - }; - } - - /** - * Records backup Metrics. - * @param backupId The backup identifier. - * @param duration The duration. - */ - async recordBackupMetrics(backupId: string, duration: number): Promise { - const backup = await this.backupRepository.findOne({ - where: { id: backupId }, - }); - - if (!backup) { - return; - } -} diff --git a/src/backup/processing/backup-queue.processor.ts b/src/backup/processing/backup-queue.processor.ts deleted file mode 100644 index f70c29b4..00000000 --- a/src/backup/processing/backup-queue.processor.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { Process, Processor } from '@nestjs/bull'; -import { Logger } from '@nestjs/common'; -import { Job } from 'bull'; -import { QUEUE_NAMES, JOB_NAMES } from '../../common/constants/queue.constants'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { ConfigService } from '@nestjs/config'; -import { BackupRecord } from '../entities/backup-record.entity'; -import { BackupStatus } from '../enums/backup-status.enum'; -import { BackupService } from '../backup.service'; -import { FileStorageService } from '../../media/storage/file-storage.service'; -import { DataIntegrityService } from '../integrity/data-integrity.service'; -import { - IBackupJobData, - IVerificationJobData, - IRecoveryTestJobData, -} from '../interfaces/backup.interfaces'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import * as fs from 'fs'; -import * as path from 'path'; -import { KMSClient, EncryptCommand } from '@aws-sdk/client-kms'; -import { S3Client, CopyObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; -import { TIME } from '../../common/constants/time.constants'; - -const execAsync = promisify(exec); -const BACKUP_MAX_RETRIES = 3; - -@Processor(QUEUE_NAMES.BACKUP_PROCESSING) -export class BackupQueueProcessor { - private readonly logger = new Logger(BackupQueueProcessor.name); - private readonly kmsClient: KMSClient; - private readonly s3Client: S3Client; - constructor( - @InjectRepository(BackupRecord) - private readonly backupRepository: Repository, - private readonly backupService: BackupService, - private readonly fileStorageService: FileStorageService, - private readonly dataIntegrityService: DataIntegrityService, - private readonly configService: ConfigService, - ) { - const awsRegion = this.configService.get('AWS_REGION', 'us-east-1'); - - this.kmsClient = new KMSClient({ - region: awsRegion, - credentials: { - accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID', ''), - secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY', ''), - }, - }); - - this.s3Client = new S3Client({ - region: awsRegion, - credentials: { - accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID', ''), - secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY', ''), - }, - }); - } - - @Process(JOB_NAMES.CREATE_BACKUP) - async handleCreateBackup(job: Job) { - const { backupRecordId, databaseName } = job.data; - this.logger.log(`Processing backup creation for: ${backupRecordId}`); - - const backup = await this.backupRepository.findOne({ - where: { id: backupRecordId }, - }); - if (!backup) { - this.logger.warn(`Backup ${backupRecordId} not found, skipping`); - return; - } - - try { - // Step 1: Create pg_dump - await this.backupService.updateBackupStatus(backupRecordId, BackupStatus.IN_PROGRESS); - const dumpStartTime = Date.now(); - - const tempFile = path.join('/tmp', `backup-${backupRecordId}.sql`); - const pgDumpCommand = this.buildPgDumpCommand(databaseName, tempFile); - - await execAsync(pgDumpCommand); - const dumpDuration = Date.now() - dumpStartTime; - - const stats = await fs.promises.stat(tempFile); - backup.backupSizeBytes = stats.size; - - // Step 2: Upload to S3 - const uploadStartTime = Date.now(); - const storageKey = `backups/${backup.region}/${databaseName}/${backupRecordId}.sql`; - const fileBuffer = await fs.promises.readFile(tempFile); - - await this.fileStorageService.uploadProcessedFile(fileBuffer, storageKey, 'application/sql'); - - const uploadDuration = Date.now() - uploadStartTime; - backup.storageKey = storageKey; - - // Step 3: Encrypt with AWS KMS - const encryptionStartTime = Date.now(); - const kmsKeyId = this.configService.get('AWS_KMS_KEY_ID'); - const encryptedKey = await this.encryptBackup(storageKey, kmsKeyId); - backup.encryptedStorageKey = encryptedKey; - backup.kmsKeyId = kmsKeyId; - - const encryptionDuration = Date.now() - encryptionStartTime; - - // Step 4: Replicate to secondary region - const replicationStartTime = Date.now(); - const secondaryRegion = this.configService.get( - 'BACKUP_SECONDARY_REGION', - 'us-west-2', - ); - const replicatedKey = await this.replicateToRegion(encryptedKey, secondaryRegion); - backup.replicatedStorageKey = replicatedKey; - - const replicationDuration = Date.now() - replicationStartTime; - - // Step 5: Calculate checksums - const checksums = await this.dataIntegrityService.calculateChecksums(tempFile); - backup.checksumMd5 = checksums.md5; - backup.checksumSha256 = checksums.sha256; - - // Cleanup temp file - await fs.promises.unlink(tempFile); - - // Update metadata - backup.metadata = { - ...backup.metadata, - endTime: new Date(), - dumpDuration, - uploadDuration, - encryptionDuration, - replicationDuration, - }; - - await this.backupRepository.save(backup); - - // Queue verification job - await (job.queue as any).add( - JOB_NAMES.VERIFY_BACKUP, - { backupRecordId, storageKey: encryptedKey }, - { - attempts: 3, - backoff: { type: 'exponential', delay: TIME.FIVE_SECONDS_MS }, - }, - ); - - this.logger.log(`Backup ${backupRecordId} completed successfully`); - } catch (error) { - await this.handleProcessingError(backup, error, job.attemptsMade); - throw error; // Re-throw to trigger retry - } - } - - @Process(JOB_NAMES.VERIFY_BACKUP) - async handleVerifyBackup(job: Job) { - const { backupRecordId } = job.data; - this.logger.log(`Verifying backup integrity: ${backupRecordId}`); - - try { - const isValid = await this.dataIntegrityService.verifyBackupIntegrity(backupRecordId); - - if (isValid) { - await this.backupService.updateBackupStatus(backupRecordId, BackupStatus.COMPLETED, { - integrityVerified: true, - verifiedAt: new Date(), - completedAt: new Date(), - }); - this.logger.log(`Backup ${backupRecordId} verified successfully`); - } else { - throw new Error('Backup integrity verification failed'); - } - } catch (error) { - await this.backupService.updateBackupStatus(backupRecordId, BackupStatus.FAILED, { - errorMessage: `Verification failed: ${error.message}`, - }); - throw error; - } - } - - @Process(JOB_NAMES.RECOVERY_TEST) - async handleRecoveryTest(_job: Job) { - this.logger.log('Recovery test processing handled by RecoveryTestingService'); - // Delegated to RecoveryTestingService.executeRecoveryTest() - } - - @Process(JOB_NAMES.DELETE_BACKUP) - async handleDeleteBackup(job: Job<{ backupRecordId: string }>) { - const { backupRecordId } = job.data; - this.logger.log(`Deleting expired backup: ${backupRecordId}`); - - const backup = await this.backupRepository.findOne({ - where: { id: backupRecordId }, - }); - - if (!backup) { - this.logger.warn(`Backup ${backupRecordId} not found, skipping deletion`); - return; - } - - try { - const bucketName = this.configService.get('AWS_S3_BUCKET', ''); - - // Delete from primary region - if (backup.encryptedStorageKey) { - await this.s3Client.send( - new DeleteObjectCommand({ - Bucket: bucketName, - Key: backup.encryptedStorageKey, - }), - ); - } - - // Delete from secondary region (if replicated) - if (backup.replicatedStorageKey) { - const secondaryBucket = this.configService.get( - 'AWS_S3_BUCKET_SECONDARY', - bucketName, - ); - const secondaryS3Client = new S3Client({ - region: this.configService.get('BACKUP_SECONDARY_REGION', 'us-west-2'), - }); - } - } - - private buildPgDumpCommand(databaseName: string, outputFile: string): string { - const host = this.configService.get('DB_HOST', 'localhost'); - const port = this.configService.get('DB_PORT', '5432'); - const username = this.configService.get('DB_USERNAME', 'postgres'); - const password = this.configService.get('DB_PASSWORD', ''); - - return `PGPASSWORD="${password}" pg_dump -h ${host} -p ${port} -U ${username} -F c -b -v -f ${outputFile} ${databaseName}`; - } - - private async encryptBackup(storageKey: string, kmsKeyId: string): Promise { - const encryptedKey = `${storageKey}.encrypted`; - - // Download from S3, encrypt with KMS, re-upload - const fileBuffer = await this.fileStorageService.downloadFile(storageKey); - - const command = new EncryptCommand({ - KeyId: kmsKeyId, - Plaintext: fileBuffer, - }); - - const response = await this.kmsClient.send(command); - - await this.fileStorageService.uploadProcessedFile( - Buffer.from(response.CiphertextBlob), - encryptedKey, - 'application/octet-stream', - ); - - return encryptedKey; - } - - private async replicateToRegion(storageKey: string, targetRegion: string): Promise { - this.logger.log(`Replicating ${storageKey} to ${targetRegion}`); - - const sourceBucket = this.configService.get('AWS_S3_BUCKET', ''); - const targetBucket = this.configService.get('AWS_S3_BUCKET_SECONDARY', sourceBucket); - - const targetKey = storageKey.replace('backups/', `backups-${targetRegion}/`); - - const copyCommand = new CopyObjectCommand({ - CopySource: `${sourceBucket}/${storageKey}`, - Bucket: targetBucket, - Key: targetKey, - }); - - await this.s3Client.send(copyCommand); - - return targetKey; - } - - private async handleProcessingError( - backup: BackupRecord, - error: Error, - attemptsMade: number, - ): Promise { - this.logger.error(`Backup processing failed for ${backup.id}:`, error); - - backup.retryCount = attemptsMade; - backup.errorMessage = error.message; - - if (attemptsMade >= BACKUP_MAX_RETRIES) { - backup.status = BackupStatus.FAILED; - this.logger.error(`Max retries exceeded for backup ${backup.id}`); - } -} diff --git a/src/backup/testing/recovery-testing.service.ts b/src/backup/testing/recovery-testing.service.ts deleted file mode 100644 index 2acb9d58..00000000 --- a/src/backup/testing/recovery-testing.service.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { InjectQueue } from '@nestjs/bull'; -import { Queue } from 'bull'; -import { QUEUE_NAMES, JOB_NAMES } from '../../common/constants/queue.constants'; -import { ConfigService } from '@nestjs/config'; -import { RecoveryTest } from '../entities/recovery-test.entity'; -import { RecoveryTestStatus } from '../enums/recovery-test-status.enum'; -import { BackupService } from '../backup.service'; -import { RecoveryTestResponseDto } from '../dto/recovery-test-response.dto'; -import { IRecoveryTestJobData } from '../interfaces/backup.interfaces'; -import { FileStorageService } from '../../media/storage/file-storage.service'; -import { KMSClient, DecryptCommand } from '@aws-sdk/client-kms'; -import { AlertingService } from '../../monitoring/alerting/alerting.service'; -import { Client } from 'pg'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import * as fs from 'fs'; -const execAsync = promisify(exec); - -/** - * Provides recovery Testing operations. - */ -@Injectable() -export class RecoveryTestingService { - private readonly logger = new Logger(RecoveryTestingService.name); - private readonly kmsClient: KMSClient; - constructor( - @InjectRepository(RecoveryTest) - private readonly recoveryTestRepository: Repository, - @InjectQueue(QUEUE_NAMES.BACKUP_PROCESSING) - private readonly backupQueue: Queue, - private readonly backupService: BackupService, - private readonly fileStorageService: FileStorageService, - private readonly configService: ConfigService, - private readonly alertingService: AlertingService, - ) { - const awsRegion = this.configService.get('AWS_REGION', 'us-east-1'); - this.kmsClient = new KMSClient({ region: awsRegion }); - } - - /** - * Creates recovery Test. - * @param backupId The backup identifier. - * @returns The resulting recovery test response dto. - */ - async createRecoveryTest(backupId: string): Promise { - const backup = await this.backupService.getLatestBackup(); - if (!backup) { - throw new NotFoundException('No verified backup found'); - } - - const testDatabaseName = this.configService.get( - 'BACKUP_TEST_DATABASE', - 'teachlink_backup_test', - ); - - const recoveryTest = this.recoveryTestRepository.create({ - backupRecordId: backupId, - status: RecoveryTestStatus.PENDING, - testDatabaseName, - }); - - await this.recoveryTestRepository.save(recoveryTest); - - // Queue recovery test job - await this.backupQueue.add( - JOB_NAMES.RECOVERY_TEST, - { - recoveryTestId: recoveryTest.id, - backupRecordId: backupId, - testDatabaseName, - } as IRecoveryTestJobData, - { - attempts: 3, - backoff: { type: 'exponential', delay: 10000 }, - }, - ); - - return this.toResponseDto(recoveryTest); - } - - /** - * Executes execute Recovery Test. - * @param testId The test identifier. - */ - async executeRecoveryTest(testId: string): Promise { - const test = await this.recoveryTestRepository.findOne({ - where: { id: testId }, - relations: ['backupRecord'], - }); - - if (!test) { - throw new NotFoundException(`Recovery test ${testId} not found`); - } - async executeRecoveryTest(testId: string): Promise { - const test = await this.recoveryTestRepository.findOne({ - where: { id: testId }, - relations: ['backupRecord'], - }); - if (!test) { - throw new NotFoundException(`Recovery test ${testId} not found`); - } - const totalStartTime = Date.now(); - try { - test.status = RecoveryTestStatus.RUNNING; - await this.recoveryTestRepository.save(test); - // Step 1: Download encrypted backup - const downloadStartTime = Date.now(); - const backupData = await this.fileStorageService.downloadFile(test.backupRecord.encryptedStorageKey); - const downloadDuration = Date.now() - downloadStartTime; - // Step 2: Decrypt - const decryptStartTime = Date.now(); - const decryptCommand = new DecryptCommand({ - CiphertextBlob: backupData, - }); - const decryptResponse = await this.kmsClient.send(decryptCommand); - const decryptedData = Buffer.from(decryptResponse.Plaintext); - const decryptionDuration = Date.now() - decryptStartTime; - // Save to temp file - const tempFile = `/tmp/recovery-test-${testId}.sql`; - await fs.promises.writeFile(tempFile, decryptedData); - // Step 3: Create test database - await this.createTestDatabase(test.testDatabaseName); - // Step 4: Restore - const restoreStartTime = Date.now(); - await this.restoreDatabase(test.testDatabaseName, tempFile); - const restoreDuration = Date.now() - restoreStartTime; - // Step 5: Validate - const validationStartTime = Date.now(); - const validationResults = await this.validateRestoredDatabase(test.testDatabaseName); - const validationDuration = Date.now() - validationStartTime; - // Step 6: Cleanup - await this.dropTestDatabase(test.testDatabaseName); - await fs.promises.unlink(tempFile); - // Update test results - test.status = validationResults.connectionSuccessful - ? RecoveryTestStatus.PASSED - : RecoveryTestStatus.FAILED; - test.validationResults = validationResults; - test.performanceMetrics = { - downloadDuration, - decryptionDuration, - restoreDuration, - validationDuration, - totalDuration: Date.now() - totalStartTime, - }; - test.testCompletedAt = new Date(); - await this.recoveryTestRepository.save(test); - // Send alert - this.alertingService.sendAlert('RECOVERY_TEST_COMPLETED', `Recovery test ${testId} ${test.status}`, test.status === RecoveryTestStatus.PASSED ? 'INFO' : 'CRITICAL'); - } - catch (error) { - this.logger.error(`Recovery test ${testId} failed:`, error); - test.status = RecoveryTestStatus.FAILED; - test.errorMessage = error.message; - await this.recoveryTestRepository.save(test); - this.alertingService.sendAlert('RECOVERY_TEST_FAILED', `Recovery test ${testId} failed: ${error.message}`, 'CRITICAL'); - } - } - } - - /** - * Retrieves test Results. - * @param testId The test identifier. - * @returns The resulting recovery test response dto. - */ - async getTestResults(testId: string): Promise { - const test = await this.recoveryTestRepository.findOne({ - where: { id: testId }, - relations: ['backupRecord'], - }); - - if (!test) { - throw new NotFoundException(`Recovery test ${testId} not found`); - } - private async createTestDatabase(dbName: string): Promise { - const client = new Client({ - host: this.configService.get('DB_HOST', 'localhost'), - port: parseInt(this.configService.get('DB_PORT', '5432')), - user: this.configService.get('DB_USERNAME', 'postgres'), - password: this.configService.get('DB_PASSWORD', ''), - database: 'postgres', - }); - await client.connect(); - await client.query(`DROP DATABASE IF EXISTS ${dbName}`); - await client.query(`CREATE DATABASE ${dbName}`); - await client.end(); - } - private async restoreDatabase(dbName: string, backupFile: string): Promise { - const host = this.configService.get('DB_HOST', 'localhost'); - const port = this.configService.get('DB_PORT', '5432'); - const username = this.configService.get('DB_USERNAME', 'postgres'); - const password = this.configService.get('DB_PASSWORD', ''); - const restoreCommand = `PGPASSWORD="${password}" pg_restore -h ${host} -p ${port} -U ${username} -d ${dbName} ${backupFile}`; - await execAsync(restoreCommand); - } - private async validateRestoredDatabase(dbName: string): Promise { - const client = new Client({ - host: this.configService.get('DB_HOST', 'localhost'), - port: parseInt(this.configService.get('DB_PORT', '5432')), - user: this.configService.get('DB_USERNAME', 'postgres'), - password: this.configService.get('DB_PASSWORD', ''), - database: dbName, - }); - try { - await client.connect(); - // Run validation queries - const tableCountResult = await client.query("SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'"); - const tableCount = parseInt(tableCountResult.rows[0].count); - await client.end(); - return { - tableCountMatch: tableCount > 0, - connectionSuccessful: true, - queriesExecuted: 1, - }; - } - catch (error) { - return { - connectionSuccessful: false, - errors: [error.message], - }; - } - } - private async dropTestDatabase(dbName: string): Promise { - const client = new Client({ - host: this.configService.get('DB_HOST', 'localhost'), - port: parseInt(this.configService.get('DB_PORT', '5432')), - user: this.configService.get('DB_USERNAME', 'postgres'), - password: this.configService.get('DB_PASSWORD', ''), - database: 'postgres', - }); - await client.connect(); - await client.query(`DROP DATABASE IF EXISTS ${dbName}`); - await client.end(); - } - private toResponseDto(test: RecoveryTest): RecoveryTestResponseDto { - return { - id: test.id, - backupRecordId: test.backupRecordId, - status: test.status, - testDatabaseName: test.testDatabaseName, - validationResults: test.validationResults, - performanceMetrics: test.performanceMetrics - ? { totalDuration: test.performanceMetrics.totalDuration } - : undefined, - createdAt: test.createdAt, - testCompletedAt: test.testCompletedAt, - }; - } -} diff --git a/src/caching/analytics/cache-analytics.service.ts b/src/caching/analytics/cache-analytics.service.ts deleted file mode 100644 index 10ac2eba..00000000 --- a/src/caching/analytics/cache-analytics.service.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; -import { CachingService } from '../caching.service'; - -export interface ICacheMetric { - key: string; - pattern: string; - hits: number; - misses: number; - avgResponseTime: number; - totalRequests: number; - lastAccess: Date; -} - -export interface ICacheAnalyticsSummary { - totalHits: number; - totalMisses: number; - hitRate: number; - missRate: number; - totalKeys: number; - memoryUsage: string; - topKeys: ICacheMetric[]; - patternStats: Map; -} - -/** - * Provides cache Analytics operations. - */ -@Injectable() -export class CacheAnalyticsService implements OnModuleInit, OnModuleDestroy { - private readonly logger = new Logger(CacheAnalyticsService.name); - private metrics: Map = new Map(); - private flushInterval?: NodeJS.Timeout; - private readonly flushIntervalMs = 60000; // Flush every minute - - constructor(private readonly cachingService: CachingService) {} - - /** - * Executes on Module Init. - */ - onModuleInit(): void { - // Start periodic metrics aggregation - this.startMetricsFlush(); - this.logger.log('Cache analytics service initialized'); - } - - /** - * Executes on Module Destroy. - */ - onModuleDestroy(): void { - if (this.flushInterval) { - clearInterval(this.flushInterval); - } - onModuleDestroy(): void { - if (this.flushInterval) { - clearInterval(this.flushInterval); - } - this.metrics.clear(); - } - /** - * Record a cache hit - */ - recordHit(key: string, responseTime?: number): void { - const pattern = this.extractPattern(key); - const metric = this.getOrCreateMetric(key, pattern); - metric.hits++; - metric.totalRequests++; - metric.lastAccess = new Date(); - if (responseTime !== undefined) { - this.updateAvgResponseTime(metric, responseTime); - } - } - } - - /** - * Get metrics for a specific key - */ - getMetric(key: string): ICacheMetric | undefined { - return this.metrics.get(key); - } - - /** - * Get all metrics - */ - getAllMetrics(): ICacheMetric[] { - return Array.from(this.metrics.values()); - } - - /** - * Get metrics by pattern - */ - getMetricsByPattern(pattern: string): ICacheMetric[] { - return this.getAllMetrics().filter((m) => m.pattern === pattern); - } - - /** - * Get analytics summary - */ - async getSummary(): Promise { - const allMetrics = this.getAllMetrics(); - const stats = await this.cachingService.getStats(); - - let totalHits = 0; - let totalMisses = 0; - const patternStats = new Map(); - - for (const metric of allMetrics) { - totalHits += metric.hits; - totalMisses += metric.misses; - - const patternStat = patternStats.get(metric.pattern) || { hits: 0, misses: 0 }; - patternStat.hits += metric.hits; - patternStat.misses += metric.misses; - patternStats.set(metric.pattern, patternStat); - } - /** - * Get metrics for a specific key - */ - getMetric(key: string): CacheMetric | undefined { - return this.metrics.get(key); - } - /** - * Get all metrics - */ - getAllMetrics(): CacheMetric[] { - return Array.from(this.metrics.values()); - } - /** - * Get metrics by pattern - */ - getMetricsByPattern(pattern: string): CacheMetric[] { - return this.getAllMetrics().filter((m) => m.pattern === pattern); - } - - return summary; - } - - /** - * Get or create a metric entry - */ - private getOrCreateMetric(key: string, pattern: string): ICacheMetric { - let metric = this.metrics.get(key); - - if (!metric) { - metric = { - key, - pattern, - hits: 0, - misses: 0, - avgResponseTime: 0, - totalRequests: 0, - lastAccess: new Date(), - }; - this.metrics.set(key, metric); - } - /** - * Get hit rate for a specific pattern - */ - getPatternHitRate(pattern: string): number { - const metrics = this.getMetricsByPattern(pattern); - if (metrics.length === 0) - return 0; - let hits = 0; - let total = 0; - for (const metric of metrics) { - hits += metric.hits; - total += metric.totalRequests; - } - return total > 0 ? (hits / total) * 100 : 0; - } - - // Keep prefix and first segment, replace rest with * - return `${parts[0]}:${parts[1]}:*`; - } - - /** - * Update average response time using exponential moving average - */ - private updateAvgResponseTime(metric: ICacheMetric, responseTime: number): void { - const alpha = 0.2; // Smoothing factor - metric.avgResponseTime = alpha * responseTime + (1 - alpha) * metric.avgResponseTime; - } - - /** - * Start periodic metrics flush to prevent memory bloat - */ - private startMetricsFlush(): void { - this.flushInterval = setInterval(() => { - this.flushOldMetrics(); - }, this.flushIntervalMs); - } - - /** - * Remove old metrics that haven't been accessed recently - */ - private flushOldMetrics(): void { - const maxAge = 24 * 60 * 60 * 1000; // 24 hours - const now = Date.now(); - let flushed = 0; - - for (const [key, metric] of this.metrics.entries()) { - const age = now - metric.lastAccess.getTime(); - if (age > maxAge) { - this.metrics.delete(key); - flushed++; - } - } - /** - * Reset metrics for a specific pattern - */ - resetPatternMetrics(pattern: string): void { - for (const [key, metric] of this.metrics.entries()) { - if (metric.pattern === pattern) { - this.metrics.delete(key); - } - } - this.logger.log(`Reset metrics for pattern: ${pattern}`); - } - /** - * Export metrics for external monitoring - */ - exportMetrics(): Record { - const summary: Record = { - timestamp: new Date().toISOString(), - metrics: {}, - }; - for (const [key, metric] of this.metrics.entries()) { - summary.metrics[key] = { - hits: metric.hits, - misses: metric.misses, - hitRate: metric.totalRequests > 0 ? (metric.hits / metric.totalRequests) * 100 : 0, - missRate: metric.totalRequests > 0 ? (metric.misses / metric.totalRequests) * 100 : 0, - avgResponseTime: metric.avgResponseTime, - }; - } - return summary; - } - /** - * Get or create a metric entry - */ - private getOrCreateMetric(key: string, pattern: string): CacheMetric { - let metric = this.metrics.get(key); - if (!metric) { - metric = { - key, - pattern, - hits: 0, - misses: 0, - avgResponseTime: 0, - totalRequests: 0, - lastAccess: new Date(), - }; - this.metrics.set(key, metric); - } - return metric; - } - /** - * Extract pattern from key - * e.g., 'cache:course:123' -> 'cache:course:*' - */ - private extractPattern(key: string): string { - const parts = key.split(':'); - if (parts.length <= 2) { - return key; - } - // Keep prefix and first segment, replace rest with * - return `${parts[0]}:${parts[1]}:*`; - } - /** - * Update average response time using exponential moving average - */ - private updateAvgResponseTime(metric: CacheMetric, responseTime: number): void { - const alpha = 0.2; // Smoothing factor - metric.avgResponseTime = alpha * responseTime + (1 - alpha) * metric.avgResponseTime; - } - /** - * Start periodic metrics flush to prevent memory bloat - */ - private startMetricsFlush(): void { - this.flushInterval = setInterval(() => { - this.flushOldMetrics(); - }, this.flushIntervalMs); - } - /** - * Remove old metrics that haven't been accessed recently - */ - private flushOldMetrics(): void { - const maxAge = 24 * 60 * 60 * 1000; // 24 hours - const now = Date.now(); - let flushed = 0; - for (const [key, metric] of this.metrics.entries()) { - const age = now - metric.lastAccess.getTime(); - if (age > maxAge) { - this.metrics.delete(key); - flushed++; - } - } - if (flushed > 0) { - this.logger.debug(`Flushed ${flushed} old metrics entries`); - } - } - /** - * Get Prometheus-style metrics - */ - getPrometheusMetrics(): string { - const lines: string[] = []; - lines.push('# HELP cache_hits_total Total number of cache hits'); - lines.push('# TYPE cache_hits_total counter'); - for (const metric of this.metrics.values()) { - lines.push(`cache_hits_total{pattern="${metric.pattern}"} ${metric.hits}`); - } - lines.push(''); - lines.push('# HELP cache_misses_total Total number of cache misses'); - lines.push('# TYPE cache_misses_total counter'); - for (const metric of this.metrics.values()) { - lines.push(`cache_misses_total{pattern="${metric.pattern}"} ${metric.misses}`); - } - lines.push(''); - lines.push('# HELP cache_avg_response_time_ms Average response time in ms'); - lines.push('# TYPE cache_avg_response_time_ms gauge'); - for (const metric of this.metrics.values()) { - lines.push(`cache_avg_response_time_ms{pattern="${metric.pattern}"} ${Math.round(metric.avgResponseTime)}`); - } - return lines.join('\n'); - } -} diff --git a/src/caching/cache-management.controller.spec.ts b/src/caching/cache-management.controller.spec.ts deleted file mode 100644 index 1663628b..00000000 --- a/src/caching/cache-management.controller.spec.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CacheManagementController } from './cache-management.controller'; -import { CachingService } from './caching.service'; -import { CacheAnalyticsService } from './analytics/cache-analytics.service'; -import { CacheInvalidationService } from './invalidation/invalidation.service'; -import { CacheWarmingService } from './warming/cache-warming.service'; -import { CacheStrategiesService } from './strategies/cache-strategies.service'; -describe('CacheManagementController', () => { - let controller: CacheManagementController; - let cachingService: jest.Mocked; - let analyticsService: jest.Mocked; - let invalidationService: jest.Mocked; - let warmingService: jest.Mocked; - let strategiesService: jest.Mocked; - beforeEach(async () => { - const mockCachingService = { - getStats: jest.fn(), - get: jest.fn(), - getTtl: jest.fn(), - delPattern: jest.fn(), - clearAll: jest.fn(), - getTTLConstants: jest.fn(), - }; - const mockAnalyticsService = { - getSummary: jest.fn(), - getAllMetrics: jest.fn(), - getPrometheusMetrics: jest.fn(), - resetMetrics: jest.fn(), - resetPatternMetrics: jest.fn(), - }; - const mockInvalidationService = { - invalidateCourse: jest.fn(), - invalidateUser: jest.fn(), - invalidateSearch: jest.fn(), - getStats: jest.fn(), - }; - const mockWarmingService = { - getStats: jest.fn(), - getWarmedKeys: jest.fn(), - refreshAll: jest.fn(), - }; - const mockStrategiesService = { - getAllStrategies: jest.fn(), - }; - const module: TestingModule = await Test.createTestingModule({ - controllers: [CacheManagementController], - providers: [ - { - provide: CachingService, - useValue: mockCachingService, - }, - { - provide: CacheAnalyticsService, - useValue: mockAnalyticsService, - }, - { - provide: CacheInvalidationService, - useValue: mockInvalidationService, - }, - { - provide: CacheWarmingService, - useValue: mockWarmingService, - }, - { - provide: CacheStrategiesService, - useValue: mockStrategiesService, - }, - ], - }).compile(); - controller = module.get(CacheManagementController); - cachingService = module.get(CachingService); - analyticsService = module.get(CacheAnalyticsService); - invalidationService = module.get(CacheInvalidationService); - warmingService = module.get(CacheWarmingService); - strategiesService = module.get(CacheStrategiesService); - }); - describe('getStats', () => { - it('should return combined stats', async () => { - cachingService.getStats.mockResolvedValue({ - keys: 100, - memory: '1.5M', - hits: 1000, - misses: 100, - }); - analyticsService.getSummary.mockResolvedValue({ - totalHits: 1000, - totalMisses: 100, - hitRate: 90.91, - missRate: 9.09, - totalKeys: 100, - memoryUsage: '1.5M', - topKeys: [], - patternStats: new Map(), - }); - warmingService.getStats.mockReturnValue({ - totalKeys: 10, - byType: { popular: 5 }, - lastWarmup: new Date(), - }); - invalidationService.getStats.mockReturnValue({ - registeredTags: 5, - totalTrackedKeys: 20, - }); - const result = await controller.getStats(); - expect(result).toHaveProperty('redis'); - expect(result).toHaveProperty('analytics'); - expect(result).toHaveProperty('warming'); - expect(result).toHaveProperty('invalidation'); - }); - }); - describe('getAnalytics', () => { - it('should return analytics', async () => { - analyticsService.getSummary.mockResolvedValue({ - totalHits: 100, - totalMisses: 10, - hitRate: 90.91, - missRate: 9.09, - totalKeys: 50, - memoryUsage: '1M', - topKeys: [], - patternStats: new Map([['cache:course:*', { hits: 50, misses: 5 }]]), - }); - analyticsService.getAllMetrics.mockReturnValue([]); - const result = await controller.getAnalytics(); - expect(result).toHaveProperty('summary'); - expect(result).toHaveProperty('metrics'); - expect(result).toHaveProperty('patternStats'); - }); - }); - describe('getPrometheusMetrics', () => { - it('should return prometheus metrics', () => { - analyticsService.getPrometheusMetrics.mockReturnValue('# HELP cache_hits_total'); - const result = controller.getPrometheusMetrics(); - expect(result).toContain('cache_hits_total'); - }); - }); - describe('getStrategies', () => { - it('should return strategies', () => { - strategiesService.getAllStrategies.mockReturnValue([]); - cachingService.getTTLConstants.mockReturnValue({} as unknown); - const result = controller.getStrategies(); - expect(result).toHaveProperty('strategies'); - expect(result).toHaveProperty('ttlConstants'); - }); - }); - describe('getWarmedKeys', () => { - it('should return warmed keys', () => { - warmingService.getStats.mockReturnValue({ - totalKeys: 5, - byType: {}, - lastWarmup: new Date(), - }); - warmingService.getWarmedKeys.mockReturnValue([]); - const result = controller.getWarmedKeys(); - expect(result).toHaveProperty('stats'); - expect(result).toHaveProperty('keys'); - }); - }); - describe('getKey', () => { - it('should return key value', async () => { - cachingService.get.mockResolvedValue({ data: 'value' }); - cachingService.getTtl.mockResolvedValue(120); - const result = await controller.getKey('test-key'); - expect(result.key).toBe('test-key'); - expect(result.value).toEqual({ data: 'value' }); - expect(result.exists).toBe(true); - }); - }); - describe('clearAll', () => { - it('should clear all cache', async () => { - cachingService.clearAll.mockResolvedValue(100); - await controller.clearAll(); - expect(cachingService.clearAll).toHaveBeenCalled(); - }); - }); - describe('clearByPattern', () => { - it('should clear by pattern', async () => { - cachingService.delPattern.mockResolvedValue(5); - await controller.clearByPattern('course:*'); - expect(cachingService.delPattern).toHaveBeenCalledWith('cache:course:*'); - }); - }); - describe('invalidateCourse', () => { - it('should invalidate course cache', async () => { - invalidationService.invalidateCourse.mockResolvedValue([]); - await controller.invalidateCourse('course-123'); - expect(invalidationService.invalidateCourse).toHaveBeenCalledWith('course-123'); - }); - }); - describe('invalidateUser', () => { - it('should invalidate user cache', async () => { - invalidationService.invalidateUser.mockResolvedValue([]); - await controller.invalidateUser('user-123'); - expect(invalidationService.invalidateUser).toHaveBeenCalledWith('user-123'); - }); - }); - describe('invalidateSearch', () => { - it('should invalidate search cache', async () => { - invalidationService.invalidateSearch.mockResolvedValue([]); - await controller.invalidateSearch(); - expect(invalidationService.invalidateSearch).toHaveBeenCalled(); - }); - }); - describe('warmCache', () => { - it('should trigger cache warming', async () => { - warmingService.refreshAll.mockResolvedValue(undefined); - warmingService.getStats.mockReturnValue({ - totalKeys: 10, - byType: {}, - lastWarmup: new Date(), - }); - const result = await controller.warmCache(); - expect(result).toHaveProperty('message'); - expect(result).toHaveProperty('stats'); - }); - }); - describe('resetAnalytics', () => { - it('should reset all analytics', async () => { - const result = await controller.resetAnalytics(); - expect(analyticsService.resetMetrics).toHaveBeenCalled(); - expect(result.message).toContain('All analytics reset'); - }); - it('should reset pattern analytics', async () => { - const result = await controller.resetAnalytics('course:*'); - expect(analyticsService.resetPatternMetrics).toHaveBeenCalledWith('course:*'); - expect(result.message).toContain('course:*'); - }); - }); -}); diff --git a/src/caching/cache-management.controller.ts b/src/caching/cache-management.controller.ts deleted file mode 100644 index b184479e..00000000 --- a/src/caching/cache-management.controller.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { Controller, Get, Post, Delete, Param, Query, HttpCode, HttpStatus, UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam } from '@nestjs/swagger'; -import { CachingService } from './caching.service'; -import { CacheAnalyticsService } from './analytics/cache-analytics.service'; -import { CacheInvalidationService } from './invalidation/invalidation.service'; -import { CacheWarmingService } from './warming/cache-warming.service'; -import { CacheStrategiesService } from './strategies/cache-strategies.service'; -import { RolesGuard } from '../common/guards/roles.guard'; -import { Roles, Role } from '../common/decorators/roles.decorator'; - -/** - * Exposes cache Management endpoints. - */ -@ApiTags('Cache Management') -@ApiBearerAuth() -@Controller('cache') -@UseGuards(RolesGuard) -@Roles(Role.ADMIN) -export class CacheManagementController { - constructor( - private readonly cachingService: CachingService, - private readonly analyticsService: CacheAnalyticsService, - private readonly invalidationService: CacheInvalidationService, - private readonly warmingService: CacheWarmingService, - private readonly strategiesService: CacheStrategiesService, - ) {} - - /** - * Returns stats. - * @returns The operation result. - */ - @Get('stats') - @ApiOperation({ summary: 'Get cache statistics' }) - @ApiResponse({ - status: 200, - description: 'Returns cache statistics including hit/miss rates and memory usage', - }) - async getStats() { - const [redisStats, analyticsSummary, warmingStats, invalidationStats] = await Promise.all([ - this.cachingService.getStats(), - this.analyticsService.getSummary(), - this.warmingService.getStats(), - this.invalidationService.getStats(), - ]); - - return { - redis: redisStats, - analytics: { - totalHits: analyticsSummary.totalHits, - totalMisses: analyticsSummary.totalMisses, - hitRate: `${analyticsSummary.hitRate}%`, - missRate: `${analyticsSummary.missRate}%`, - totalKeys: analyticsSummary.totalKeys, - memoryUsage: analyticsSummary.memoryUsage, - topKeys: analyticsSummary.topKeys, - }, - warming: warmingStats, - invalidation: invalidationStats, - }; - } - - /** - * Returns analytics. - * @returns The operation result. - */ - @Get('analytics') - @ApiOperation({ summary: 'Get detailed cache analytics' }) - @ApiResponse({ - status: 200, - description: 'Returns detailed cache analytics including metrics per key', - }) - async getAnalytics() { - const summary = await this.analyticsService.getSummary(); - const allMetrics = this.analyticsService.getAllMetrics(); - - return { - summary: { - totalHits: summary.totalHits, - totalMisses: summary.totalMisses, - hitRate: summary.hitRate, - missRate: summary.missRate, - }, - metrics: allMetrics, - patternStats: Object.fromEntries(summary.patternStats), - }; - } - - /** - * Returns prometheus Metrics. - * @returns The operation result. - */ - @Get('metrics/prometheus') - @ApiOperation({ summary: 'Get Prometheus-compatible metrics' }) - @ApiResponse({ - status: 200, - description: 'Returns metrics in Prometheus format', - }) - getPrometheusMetrics() { - return this.analyticsService.getPrometheusMetrics(); - } - - /** - * Returns strategies. - * @returns The operation result. - */ - @Get('strategies') - @ApiOperation({ summary: 'Get all cache strategies' }) - @ApiResponse({ - status: 200, - description: 'Returns all registered cache strategies', - }) - getStrategies() { - return { - strategies: this.strategiesService.getAllStrategies(), - ttlConstants: this.cachingService.getTTLConstants(), - }; - } - - /** - * Returns warmed Keys. - * @returns The operation result. - */ - @Get('warmed') - @ApiOperation({ summary: 'Get warmed cache keys' }) - @ApiResponse({ - status: 200, - description: 'Returns all keys that have been warmed', - }) - getWarmedKeys() { - return { - stats: this.warmingService.getStats(), - keys: this.warmingService.getWarmedKeys(), - }; - } - - /** - * Returns key. - * @param key The key. - * @returns The operation result. - */ - @Get('key/:key') - @ApiOperation({ summary: 'Get a cached value by key' }) - @ApiParam({ name: 'key', description: 'Cache key to retrieve' }) - @ApiResponse({ - status: 200, - description: 'Returns the cached value', - }) - @ApiResponse({ - status: 404, - description: 'Key not found in cache', - }) - async getKey(@Param('key') key: string) { - const value = await this.cachingService.get(key); - const ttl = await this.cachingService.getTtl(key); - - return { - key, - value, - ttl, - exists: value !== null, - }; - } - - /** - * Clears all. - * @returns The operation result. - */ - @Delete('clear') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: 'Clear all cache' }) - @ApiResponse({ - status: 204, - description: 'All cache cleared successfully', - }) - async clearAll() { - await this.cachingService.clearAll(); - } - - /** - * Clears by Pattern. - * @param pattern The pattern. - * @returns The operation result. - */ - @Delete('clear/:pattern') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: 'Clear cache by pattern' }) - @ApiParam({ name: 'pattern', description: 'Pattern to match (use * as wildcard)' }) - @ApiResponse({ - status: 204, - description: 'Cache entries matching pattern cleared successfully', - }) - async clearByPattern(@Param('pattern') pattern: string) { - // Decode URL-encoded pattern - const decodedPattern = decodeURIComponent(pattern); - await this.cachingService.delPattern(`cache:${decodedPattern}`); - } - - /** - * Invalidates course. - * @param courseId The course identifier. - * @returns The operation result. - */ - @Delete('invalidate/course/:courseId') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: 'Invalidate all cache for a specific course' }) - @ApiParam({ name: 'courseId', description: 'Course ID to invalidate cache for' }) - @ApiResponse({ - status: 204, - description: 'Course cache invalidated successfully', - }) - async invalidateCourse(@Param('courseId') courseId: string) { - await this.invalidationService.invalidateCourse(courseId); - } - - /** - * Invalidates user. - * @param userId The user identifier. - * @returns The operation result. - */ - @Delete('invalidate/user/:userId') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: 'Invalidate all cache for a specific user' }) - @ApiParam({ name: 'userId', description: 'User ID to invalidate cache for' }) - @ApiResponse({ - status: 204, - description: 'User cache invalidated successfully', - }) - async invalidateUser(@Param('userId') userId: string) { - await this.invalidationService.invalidateUser(userId); - } - - /** - * Invalidates search. - * @returns The operation result. - */ - @Delete('invalidate/search') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: 'Invalidate all search cache' }) - @ApiResponse({ - status: 204, - description: 'Search cache invalidated successfully', - }) - async invalidateSearch() { - await this.invalidationService.invalidateSearch(); - } - - /** - * Warms cache. - * @returns The operation result. - */ - @Post('warm') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Manually trigger cache warming' }) - @ApiResponse({ - status: 200, - description: 'Cache warming triggered successfully', - }) - async warmCache() { - await this.warmingService.refreshAll(); - return { - message: 'Cache warming completed', - stats: this.warmingService.getStats(), - }; - } - - /** - * Resets analytics. - * @param pattern The pattern. - * @returns The operation result. - */ - @Post('analytics/reset') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Reset analytics metrics' }) - @ApiResponse({ - status: 200, - description: 'Analytics metrics reset successfully', - }) - async resetAnalytics(@Query('pattern') pattern?: string) { - if (pattern) { - this.analyticsService.resetPatternMetrics(pattern); - } else { - this.analyticsService.resetMetrics(); - } -} diff --git a/src/caching/caching.module.ts b/src/caching/caching.module.ts deleted file mode 100644 index 84ce200e..00000000 --- a/src/caching/caching.module.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Global, Module, OnModuleDestroy } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { EventEmitterModule } from '@nestjs/event-emitter'; -import { CACHE_REDIS_CLIENT } from './caching.constants'; -import { CachingService } from './caching.service'; -import { CacheStrategiesService } from './strategies/cache-strategies.service'; -import { CacheInvalidationService } from './invalidation/invalidation.service'; -import { CacheWarmingService } from './warming/cache-warming.service'; -import { CacheAnalyticsService } from './analytics/cache-analytics.service'; -import { CacheManagementController } from './cache-management.controller'; -import { getSharedRedisClient } from '../config/cache.config'; - -/** - * Registers the caching module. - */ -@Global() -@Module({ - imports: [ConfigModule, EventEmitterModule], - controllers: [CacheManagementController], - providers: [ - // Redis client provider - { - provide: CACHE_REDIS_CLIENT, - inject: [ConfigService], - useFactory: (configService: ConfigService): ReturnType => - getSharedRedisClient(configService), - }, - // Cache services - CachingService, - CacheStrategiesService, - CacheInvalidationService, - CacheWarmingService, - CacheAnalyticsService, - ], - exports: [ - CACHE_REDIS_CLIENT, - CachingService, - CacheStrategiesService, - CacheInvalidationService, - CacheWarmingService, - CacheAnalyticsService, - ], -}) -export class CachingModule implements OnModuleDestroy { - constructor(private readonly cachingService: CachingService) {} - - onModuleDestroy(): void { - // Cleanup is handled by CachingService - } -} diff --git a/src/caching/caching.service.spec.ts b/src/caching/caching.service.spec.ts deleted file mode 100644 index 37b15d22..00000000 --- a/src/caching/caching.service.spec.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigService } from '@nestjs/config'; -import { CachingService } from './caching.service'; -import { CACHE_REDIS_CLIENT } from './caching.constants'; -import { createMockRedisClient, createMockConfigService } from 'test/utils/mock-factories'; -import Redis from 'ioredis'; -describe('CachingService', () => { - let service: CachingService; - let mockRedis: jest.Mocked; - beforeEach(async () => { - // ─── Initialize Mocks ────────────────────────────────────────────────── - mockRedis = createMockRedisClient(); - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CachingService, - { - provide: ConfigService, - useValue: createMockConfigService({ CACHE_TTL: 300 }), - }, - { - provide: CACHE_REDIS_CLIENT, - useValue: mockRedis, - }, - ], - }).compile(); - service = module.get(CachingService); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - describe('get', () => { - it('should return cached value', async () => { - mockRedis.get.mockResolvedValue(JSON.stringify({ id: '1', name: 'Test' })); - const result = await service.get('test:key'); - expect(result).toEqual({ id: '1', name: 'Test' }); - expect(mockRedis.get).toHaveBeenCalledWith('test:key'); - }); - it('should return null when key not found', async () => { - mockRedis.get.mockResolvedValue(null); - const result = await service.get('test:key'); - expect(result).toBeNull(); - }); - it('should return raw string for non-JSON values', async () => { - mockRedis.get.mockResolvedValue('raw-string-value'); - const result = await service.get('test:key'); - expect(result).toBe('raw-string-value'); - }); - }); - describe('set', () => { - it('should set value with TTL', async () => { - await service.set('test:key', { data: 'value' }, 60); - expect(mockRedis.set).toHaveBeenCalledWith('test:key', JSON.stringify({ data: 'value' }), 'EX', 60); - }); - it('should set value without TTL when ttl is 0', async () => { - await service.set('test:key', { data: 'value' }, 0); - expect(mockRedis.set).toHaveBeenCalledWith('test:key', JSON.stringify({ data: 'value' })); - }); - it('should set string value directly', async () => { - await service.set('test:key', 'string-value'); - expect(mockRedis.set).toHaveBeenCalledWith('test:key', 'string-value', 'EX', 300); - }); - }); - describe('del', () => { - it('should delete key', async () => { - await service.del('test:key'); - expect(mockRedis.del).toHaveBeenCalledWith('test:key'); - }); - }); - describe('delPattern', () => { - it('should delete keys matching pattern', async () => { - mockRedis.scan.mockResolvedValueOnce(['0', ['key1', 'key2']]); - const result = await service.delPattern('test:*'); - expect(result).toBe(2); - expect(mockRedis.del).toHaveBeenCalledWith('key1', 'key2'); - }); - it('should return 0 when no keys match', async () => { - mockRedis.scan.mockResolvedValueOnce(['0', []]); - const result = await service.delPattern('test:*'); - expect(result).toBe(0); - }); - }); - describe('getOrSet', () => { - it('should return cached value when available', async () => { - mockRedis.get.mockResolvedValue(JSON.stringify({ cached: true })); - const factory = jest.fn(); - const result = await service.getOrSet('test:key', factory, 60); - expect(result).toEqual({ cached: true }); - expect(factory).not.toHaveBeenCalled(); - }); - it('should call factory and cache result when not available', async () => { - mockRedis.get.mockResolvedValue(null); - const factory = jest.fn().mockResolvedValue({ new: 'data' }); - const result = await service.getOrSet('test:key', factory, 60); - expect(result).toEqual({ new: 'data' }); - expect(factory).toHaveBeenCalled(); - expect(mockRedis.set).toHaveBeenCalled(); - }); - }); - describe('exists', () => { - it('should return true when key exists', async () => { - mockRedis.exists.mockResolvedValue(1); - const result = await service.exists('test:key'); - expect(result).toBe(true); - }); - it('should return false when key does not exist', async () => { - mockRedis.exists.mockResolvedValue(0); - const result = await service.exists('test:key'); - expect(result).toBe(false); - }); - }); - describe('getTtl', () => { - it('should return TTL of key', async () => { - mockRedis.ttl.mockResolvedValue(120); - const result = await service.getTtl('test:key'); - expect(result).toBe(120); - }); - }); - describe('incr', () => { - it('should increment by 1 by default', async () => { - mockRedis.incr.mockResolvedValue(1); - const result = await service.incr('counter'); - expect(result).toBe(1); - expect(mockRedis.incr).toHaveBeenCalledWith('counter'); - }); - it('should increment by specified amount', async () => { - mockRedis.incrby.mockResolvedValue(5); - const result = await service.incr('counter', 5); - expect(result).toBe(5); - expect(mockRedis.incrby).toHaveBeenCalledWith('counter', 5); - }); - }); - describe('mget', () => { - it('should get multiple values', async () => { - mockRedis.mget.mockResolvedValue([ - JSON.stringify({ id: 1 }), - null, - JSON.stringify({ id: 3 }), - ]); - const result = await service.mget(['key1', 'key2', 'key3']); - expect(result).toEqual([{ id: 1 }, null, { id: 3 }]); - }); - it('should return empty array for empty input', async () => { - const result = await service.mget([]); - expect(result).toEqual([]); - expect(mockRedis.mget).not.toHaveBeenCalled(); - }); - }); - describe('mset', () => { - it('should set multiple values', async () => { - const mockPipeline = { - set: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue([]), - }; - mockRedis.pipeline.mockReturnValue(mockPipeline as unknown); - await service.mset([ - { key: 'key1', value: { id: 1 } }, - { key: 'key2', value: { id: 2 } }, - ], 60); - expect(mockRedis.pipeline).toHaveBeenCalled(); - expect(mockPipeline.exec).toHaveBeenCalled(); - }); - }); - describe('getStats', () => { - it('should return cache statistics', async () => { - mockRedis.info - .mockResolvedValueOnce('used_memory_human:1.5M\n') - .mockResolvedValueOnce('db0:keys=100\n') - .mockResolvedValueOnce('keyspace_hits:1000\nkeyspace_misses:100\n'); - const result = await service.getStats(); - expect(result).toEqual({ - keys: 100, - memory: '1.5M', - hits: 1000, - misses: 100, - }); - }); - }); - describe('generateKey', () => { - it('should generate key with prefix and parts', () => { - const result = service.generateKey('prefix', 'part1', 'part2'); - expect(result).toBe('prefix:part1:part2'); - }); - }); - describe('getTTLConstants', () => { - it('should return TTL constants', () => { - const result = service.getTTLConstants(); - expect(result).toHaveProperty('USER_SESSION'); - expect(result).toHaveProperty('COURSE_DETAILS'); - expect(result).toHaveProperty('SEARCH_RESULTS'); - }); - }); -}); diff --git a/src/caching/caching.service.ts b/src/caching/caching.service.ts deleted file mode 100644 index 079379c0..00000000 --- a/src/caching/caching.service.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import Redis from 'ioredis'; -import { CACHE_REDIS_CLIENT, CACHE_TTL } from './caching.constants'; - -export interface ICacheOptions { - ttl?: number; - prefix?: string; -} - -/** - * Provides caching operations. - */ -@Injectable() -export class CachingService implements OnModuleDestroy { - private readonly logger = new Logger(CachingService.name); - private readonly defaultTtl: number; - - constructor( - @Inject(CACHE_REDIS_CLIENT) private readonly redis: Redis, - private readonly configService: ConfigService, - ) { - this.defaultTtl = parseInt(this.configService.get('REDIS_TTL') || '300', 10); - } - - /** - * Executes on Module Destroy. - */ - async onModuleDestroy(): Promise { - if (this.redis.status !== 'end') { - await this.redis.quit(); - } - async onModuleDestroy(): Promise { - if (this.redis.status !== 'end') { - await this.redis.quit(); - } - } - /** - * Get a value from cache - * @param key - Cache key - * @returns The cached value or null if not found - */ - async get(key: string): Promise { - try { - const data = await this.redis.get(key); - if (!data) { - return null; - } - try { - return JSON.parse(data) as T; - } - catch { - // Return raw string if not valid JSON - return data as unknown as T; - } - } - catch (error) { - this.logger.error(`Failed to get cache key: ${key}`, error); - return null; - } - } - /** - * Set a value in cache - * @param key - Cache key - * @param value - Value to cache - * @param ttl - Time to live in seconds (optional) - */ - async set(key: string, value: T, ttl?: number): Promise { - try { - const serializedValue = typeof value === 'string' ? value : JSON.stringify(value); - const effectiveTtl = ttl ?? this.defaultTtl; - if (effectiveTtl > 0) { - await this.redis.set(key, serializedValue, 'EX', effectiveTtl); - } - else { - await this.redis.set(key, serializedValue); - } - } - catch (error) { - this.logger.error(`Failed to set cache key: ${key}`, error); - } - } - /** - * Delete a key from cache - * @param key - Cache key to delete - */ - async del(key: string): Promise { - try { - await this.redis.del(key); - } - catch (error) { - this.logger.error(`Failed to delete cache key: ${key}`, error); - } - } - /** - * Delete all keys matching a pattern - * @param pattern - Pattern to match (e.g., 'cache:course:*') - */ - async delPattern(pattern: string): Promise { - try { - const keys = await this.scanKeys(pattern); - if (keys.length === 0) { - return 0; - } - await this.redis.del(...keys); - this.logger.debug(`Deleted ${keys.length} keys matching pattern: ${pattern}`); - return keys.length; - } - catch (error) { - this.logger.error(`Failed to delete cache pattern: ${pattern}`, error); - return 0; - } - } - /** - * Get a value from cache, or execute factory function and cache the result - * @param key - Cache key - * @param factory - Function to execute if value not in cache - * @param ttl - Time to live in seconds (optional) - * @returns The cached or freshly computed value - */ - async getOrSet(key: string, factory: () => Promise, ttl?: number): Promise { - const cached = await this.get(key); - if (cached !== null) { - return cached; - } - const value = await factory(); - await this.set(key, value, ttl); - return value; - } - /** - * Check if a key exists in cache - * @param key - Cache key - * @returns True if key exists - */ - async exists(key: string): Promise { - try { - const result = await this.redis.exists(key); - return result === 1; - } - catch (error) { - this.logger.error(`Failed to check cache key existence: ${key}`, error); - return false; - } - } - /** - * Get the TTL of a key - * @param key - Cache key - * @returns TTL in seconds, -1 if no expiry, -2 if key doesn't exist - */ - async getTtl(key: string): Promise { - try { - return await this.redis.ttl(key); - } - catch (error) { - this.logger.error(`Failed to get TTL for key: ${key}`, error); - return -2; - } - } - /** - * Set the TTL of an existing key - * @param key - Cache key - * @param ttl - New TTL in seconds - */ - async expire(key: string, ttl: number): Promise { - try { - await this.redis.expire(key, ttl); - } - catch (error) { - this.logger.error(`Failed to set expiry for key: ${key}`, error); - } - } - /** - * Increment a counter - * @param key - Cache key - * @param increment - Amount to increment (default: 1) - * @returns New value after increment - */ - async incr(key: string, increment = 1): Promise { - try { - if (increment === 1) { - return await this.redis.incr(key); - } - return await this.redis.incrby(key, increment); - } - catch (error) { - this.logger.error(`Failed to increment key: ${key}`, error); - return 0; - } - } - /** - * Get multiple values at once - * @param keys - Array of cache keys - * @returns Array of values (null for missing keys) - */ - async mget(keys: string[]): Promise> { - if (keys.length === 0) { - return []; - } - try { - const values = await this.redis.mget(...keys); - return values.map((data) => { - if (!data) - return null; - try { - return JSON.parse(data) as T; - } - catch { - return data as unknown as T; - } - }); - } - catch (error) { - this.logger.error('Failed to get multiple cache keys', error); - return keys.map(() => null); - } - } - /** - * Set multiple values at once - * @param entries - Array of key-value pairs - * @param ttl - Time to live in seconds (optional) - */ - async mset(entries: Array<{ - key: string; - value: T; - }>, ttl?: number): Promise { - if (entries.length === 0) { - return; - } - try { - const pipeline = this.redis.pipeline(); - for (const { key, value } of entries) { - const serializedValue = typeof value === 'string' ? value : JSON.stringify(value); - if (ttl && ttl > 0) { - pipeline.set(key, serializedValue, 'EX', ttl); - } - else { - pipeline.set(key, serializedValue); - } - } - await pipeline.exec(); - } - catch (error) { - this.logger.error('Failed to set multiple cache keys', error); - } - } - /** - * Scan keys matching a pattern - * @param pattern - Pattern to match - * @param count - Approximate number of keys to return per iteration - * @returns Array of matching keys - */ - private async scanKeys(pattern: string, count = 100): Promise { - const keys: string[] = []; - let cursor = '0'; - do { - const [nextCursor, matchedKeys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', count); - cursor = nextCursor; - keys.push(...matchedKeys); - } while (cursor !== '0'); - return keys; - } - /** - * Get cache statistics - * @returns Cache statistics object - */ - async getStats(): Promise<{ - keys: number; - memory: string; - hits: number; - misses: number; - }> { - try { - const info = await this.redis.info('memory'); - const keyspaceInfo = await this.redis.info('keyspace'); - // Parse memory usage - const memoryMatch = info.match(/used_memory_human:(\S+)/); - const memory = memoryMatch ? memoryMatch[1] : 'unknown'; - // Parse key count - const dbMatch = keyspaceInfo.match(/db\d+:keys=(\d+)/); - const keys = dbMatch ? parseInt(dbMatch[1], 10) : 0; - // Parse hit/miss stats - const statsInfo = await this.redis.info('stats'); - const hitsMatch = statsInfo.match(/keyspace_hits:(\d+)/); - const missesMatch = statsInfo.match(/keyspace_misses:(\d+)/); - return { - keys, - memory, - hits: hitsMatch ? parseInt(hitsMatch[1], 10) : 0, - misses: missesMatch ? parseInt(missesMatch[1], 10) : 0, - }; - } - catch (error) { - this.logger.error('Failed to get cache stats', error); - return { - keys: 0, - memory: 'unknown', - hits: 0, - misses: 0, - }; - } - } - /** - * Clear all cache keys with the application prefix - */ - async clearAll(): Promise { - return this.delPattern('cache:*'); - } - /** - * Generate a cache key with prefix - * @param prefix - Key prefix - * @param parts - Key parts to join - * @returns Formatted cache key - */ - generateKey(prefix: string, ...parts: Array): string { - return `${prefix}:${parts.join(':')}`; - } - /** - * Get TTL constants for external use - */ - getTTLConstants(): typeof CACHE_TTL { - return CACHE_TTL; - } -} diff --git a/src/caching/decorators/cache.decorator.ts b/src/caching/decorators/cache.decorator.ts deleted file mode 100644 index c8e1e500..00000000 --- a/src/caching/decorators/cache.decorator.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { SetMetadata } from '@nestjs/common'; -export const CACHE_KEY_METADATA = 'cache:key'; -export const CACHE_TTL_METADATA = 'cache:ttl'; -export const CACHE_EVICT_METADATA = 'cache:evict'; -export const CACHE_PREFIX_METADATA = 'cache:prefix'; -export const CACHE_CONDITION_METADATA = 'cache:condition'; -/** - * Options for cacheable decorator - */ -export interface ICacheableOptions { - /** - * Time to live in seconds - */ - ttl?: number; - - /** - * Cache key prefix - */ - prefix?: string; - - /** - * Custom cache key generator - * If provided, this function will be used to generate the cache key - */ - keyGenerator?: (...args: any[]) => string; - - /** - * Condition to determine if result should be cached - * Return true to cache, false to skip caching - */ - condition?: (...args: any[]) => boolean; -} -/** - * Options for cache evict decorator - */ -export interface ICacheEvictOptions { - /** - * Pattern(s) to evict (supports wildcards) - */ - patterns: string | string[]; - - /** - * Whether to evict before method execution - * Default: false (evict after successful execution) - */ - beforeInvocation?: boolean; -} -/** - * Decorator to cache method result - * - * @param ttl - Time to live in seconds - * @param prefix - Optional key prefix - * - * @example - * ```typescript - * @Cacheable(300) // 5 minute TTL - * async findOne(id: string) { - * return this.repository.findOne(id); - * } - * - * @Cacheable({ ttl: 600, prefix: 'users' }) - * async getUserProfile(id: string) { - * return this.userRepository.findOne(id); - * } - * ``` - */ -export function Cacheable(ttlOrOptions?: number | ICacheableOptions): MethodDecorator { - const options: ICacheableOptions = - typeof ttlOrOptions === 'number' ? { ttl: ttlOrOptions } : (ttlOrOptions ?? {}); - - return ( - target: object, - propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor, - ) => { - if (options.ttl) { - SetMetadata(CACHE_TTL_METADATA, options.ttl)(target, propertyKey, descriptor); - } - - if (options.prefix) { - SetMetadata(CACHE_PREFIX_METADATA, options.prefix)(target, propertyKey, descriptor); - } - - if (options.keyGenerator) { - SetMetadata(CACHE_KEY_METADATA, options.keyGenerator)(target, propertyKey, descriptor); - } - - if (options.condition) { - SetMetadata(CACHE_CONDITION_METADATA, options.condition)(target, propertyKey, descriptor); - } - - return descriptor; - }; -} -/** - * Decorator to evict cache entries when method is executed - * - * @param patterns - Pattern(s) to evict (supports wildcards like 'cache:course:*') - * - * @example - * ```typescript - * @CacheEvict('cache:course:*') - * async update(id: string, dto: UpdateCourseDto) { - * return this.repository.update(id, dto); - * } - * - * @CacheEvict({ patterns: ['cache:user:*', 'cache:profile:*'], beforeInvocation: true }) - * async deleteUser(id: string) { - * await this.repository.delete(id); - * } - * ``` - */ -export function CacheEvict( - patternsOrOptions: string | string[] | ICacheEvictOptions, -): MethodDecorator { - const options: ICacheEvictOptions = - typeof patternsOrOptions === 'string' - ? { patterns: [patternsOrOptions] } - : Array.isArray(patternsOrOptions) - ? { patterns: patternsOrOptions } - : patternsOrOptions; - - return ( - target: object, - propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor, - ) => { - SetMetadata(CACHE_EVICT_METADATA, options)(target, propertyKey, descriptor); - return descriptor; - }; -} -/** - * Decorator to set a custom cache key for a method - * - * @param key - Custom cache key or key generator function - * - * @example - * ```typescript - * @CacheKey('featured-courses') - * @Cacheable(3600) - * async getFeaturedCourses() { - * return this.repository.findFeatured(); - * } - * - * @CacheKey((args) => `user:${args[0]}:profile`) - * @Cacheable(600) - * async getUserProfile(userId: string) { - * return this.userRepository.findOne(userId); - * } - * ``` - */ -export function CacheKey(key: string | ((...args: unknown[]) => string)): MethodDecorator { - return (target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => { - SetMetadata(CACHE_KEY_METADATA, key)(target, propertyKey, descriptor); - return descriptor; - }; -} -/** - * Decorator to set TTL for a cached method - * - * @param ttl - Time to live in seconds - * - * @example - * ```typescript - * @CacheTTL(3600) // 1 hour - * @Cacheable() - * async getStaticContent() { - * return this.contentRepository.findStatic(); - * } - * ``` - */ -export function CacheTTL(ttl: number): MethodDecorator { - return (target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => { - SetMetadata(CACHE_TTL_METADATA, ttl)(target, propertyKey, descriptor); - return descriptor; - }; -} -/** - * Decorator to set cache prefix for a method - * - * @param prefix - Cache key prefix - * - * @example - * ```typescript - * @CachePrefix('courses') - * @Cacheable(300) - * async getCourseById(id: string) { - * return this.courseRepository.findOne(id); - * } - * ``` - */ -export function CachePrefix(prefix: string): MethodDecorator { - return (target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => { - SetMetadata(CACHE_PREFIX_METADATA, prefix)(target, propertyKey, descriptor); - return descriptor; - }; -} -/** - * Decorator to conditionally cache based on method arguments - * - * @param condition - Function that returns true if result should be cached - * - * @example - * ```typescript - * @CacheCondition((result) => result.status === 'published') - * @Cacheable(300) - * async getCourse(id: string) { - * return this.courseRepository.findOne(id); - * } - * ``` - */ -export function CacheCondition(condition: (result: unknown, ...args: unknown[]) => boolean): MethodDecorator { - return (target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => { - SetMetadata(CACHE_CONDITION_METADATA, condition)(target, propertyKey, descriptor); - return descriptor; - }; -} diff --git a/src/caching/interceptors/cache.interceptor.ts b/src/caching/interceptors/cache.interceptor.ts deleted file mode 100644 index 23f8e953..00000000 --- a/src/caching/interceptors/cache.interceptor.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject, Optional, } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { Observable, of, from } from 'rxjs'; -import { mergeMap, tap } from 'rxjs/operators'; -import { CachingService } from '../caching.service'; -import { CacheAnalyticsService } from '../analytics/cache-analytics.service'; -import { - CACHE_KEY_METADATA, - CACHE_TTL_METADATA, - CACHE_EVICT_METADATA, - CACHE_PREFIX_METADATA, - CACHE_CONDITION_METADATA, - ICacheEvictOptions, -} from '../decorators/cache.decorator'; -import { CACHE_TTL } from '../caching.constants'; - -export interface ICacheInterceptorOptions { - /** - * Default TTL in seconds - */ - defaultTtl?: number; - - /** - * Default key prefix - */ - defaultPrefix?: string; - - /** - * Methods to cache (default: GET) - */ - methods?: string[]; - - /** - * Whether to track analytics - */ - trackAnalytics?: boolean; -} - -/** - * Intercepts cache request handling. - */ -@Injectable() -export class CacheInterceptor implements NestInterceptor { - private readonly defaultTtl: number; - private readonly defaultPrefix: string; - private readonly cachedMethods: string[]; - private readonly trackAnalytics: boolean; - - constructor( - private readonly cachingService: CachingService, - private readonly reflector: Reflector, - @Optional() @Inject('CACHE_INTERCEPTOR_OPTIONS') options?: ICacheInterceptorOptions, - @Optional() private readonly analyticsService?: CacheAnalyticsService, - ) { - this.defaultTtl = options?.defaultTtl ?? CACHE_TTL.COURSE_DETAILS; - this.defaultPrefix = options?.defaultPrefix ?? 'cache:http'; - this.cachedMethods = options?.methods ?? ['GET']; - this.trackAnalytics = options?.trackAnalytics ?? true; - } - - /** - * Executes intercept. - * @param context The context. - * @param next The next. - * @returns The resulting observable. - */ - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context.switchToHttp().getRequest(); - - // Only cache specified HTTP methods - if (!request || !this.cachedMethods.includes(request.method)) { - return next.handle(); - } - - // Get metadata from decorator - const handler = context.getHandler(); - const ttl = this.reflector.get(CACHE_TTL_METADATA, handler) ?? this.defaultTtl; - const prefix = this.reflector.get(CACHE_PREFIX_METADATA, handler) ?? this.defaultPrefix; - const customKey = this.reflector.get string)>( - CACHE_KEY_METADATA, - handler, - ); - const evictOptions = this.reflector.get(CACHE_EVICT_METADATA, handler); - const condition = this.reflector.get<(...args: any[]) => boolean>( - CACHE_CONDITION_METADATA, - handler, - ); - - // Generate cache key - const cacheKey = this.generateCacheKey(request, prefix, customKey); - - // Handle cache eviction - if (evictOptions) { - return this.handleEviction(next, evictOptions, cacheKey); - } - - // Check condition - if (condition && !condition(request.params, request.query, request.body)) { - return next.handle(); - } - - // Try to get from cache - return from(this.cachingService.get(cacheKey)).pipe( - mergeMap((cachedResponse) => { - if (cachedResponse !== null) { - // Cache hit - if (this.trackAnalytics && this.analyticsService) { - this.analyticsService.recordHit(cacheKey); - } - return of(cachedResponse); - } - // Get metadata from decorator - const handler = context.getHandler(); - const ttl = this.reflector.get(CACHE_TTL_METADATA, handler) ?? this.defaultTtl; - const prefix = this.reflector.get(CACHE_PREFIX_METADATA, handler) ?? this.defaultPrefix; - const customKey = this.reflector.get string)>(CACHE_KEY_METADATA, handler); - const evictOptions = this.reflector.get(CACHE_EVICT_METADATA, handler); - const condition = this.reflector.get<(...args: unknown[]) => boolean>(CACHE_CONDITION_METADATA, handler); - // Generate cache key - const cacheKey = this.generateCacheKey(request, prefix, customKey); - // Handle cache eviction - if (evictOptions) { - return this.handleEviction(next, evictOptions, cacheKey); - } - // Check condition - if (condition && !condition(request.params, request.query, request.body)) { - return next.handle(); - } - // Try to get from cache - return from(this.cachingService.get(cacheKey)).pipe(mergeMap((cachedResponse) => { - if (cachedResponse !== null) { - // Cache hit - if (this.trackAnalytics && this.analyticsService) { - this.analyticsService.recordHit(cacheKey); - } - return of(cachedResponse); - } - // Cache miss - execute handler and cache result - return next.handle().pipe(tap({ - next: (response) => { - // Cache the response - if (this.trackAnalytics && this.analyticsService) { - this.analyticsService.recordMiss(cacheKey); - } - from(this.cachingService.set(cacheKey, response, ttl)).subscribe(); - }, - })); - })); - } - /** - * Handle cache eviction before or after method execution - */ - private handleEviction(next: CallHandler, evictOptions: CacheEvictOptions, _currentKey: string): Observable { - const patterns = Array.isArray(evictOptions.patterns) - ? evictOptions.patterns - : [evictOptions.patterns]; - if (evictOptions.beforeInvocation) { - // Evict before method execution - return from(this.evictPatterns(patterns)).pipe(mergeMap(() => next.handle())); - } - // Evict after successful method execution - return next.handle().pipe(tap({ - next: () => { - from(this.evictPatterns(patterns)).subscribe(); - }, - }), - ); - }), - ); - } - - /** - * Handle cache eviction before or after method execution - */ - private handleEviction( - next: CallHandler, - evictOptions: ICacheEvictOptions, - _currentKey: string, - ): Observable { - const patterns = Array.isArray(evictOptions.patterns) - ? evictOptions.patterns - : [evictOptions.patterns]; - - if (evictOptions.beforeInvocation) { - // Evict before method execution - return from(this.evictPatterns(patterns)).pipe(mergeMap(() => next.handle())); - } - /** - * Evict cache entries matching patterns - */ - private async evictPatterns(patterns: string[]): Promise { - for (const pattern of patterns) { - await this.cachingService.delPattern(pattern); - } - } - /** - * Generate a cache key from request - */ - private generateCacheKey(request: unknown, prefix: string, customKey?: string | ((...args: unknown[]) => string)): string { - if (customKey) { - if (typeof customKey === 'function') { - return customKey(request.params, request.query, request.body); - } - return customKey; - } - const route = request.route?.path ?? request.url; - const params = JSON.stringify(request.params ?? {}); - const query = JSON.stringify(request.query ?? {}); - const userId = request.user?.id ?? 'anonymous'; - // Create a hash-like key from route, params, query, and user - const keyParts = [prefix, route.replace(/\//g, ':'), userId, this.hashString(params + query)]; - return keyParts.filter(Boolean).join(':'); - } - /** - * Simple string hash for cache key generation - */ - private hashString(str: string): string { - let hash = 0; - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32-bit integer - } - return Math.abs(hash).toString(36); - } -} -/** - * Factory function to create a configured cache interceptor - */ -export function createCacheInterceptor( - cachingService: CachingService, - reflector: Reflector, - options?: ICacheInterceptorOptions, - analyticsService?: CacheAnalyticsService, -): CacheInterceptor { - return new CacheInterceptor(cachingService, reflector, options, analyticsService); -} diff --git a/src/caching/invalidation/invalidation.service.spec.ts b/src/caching/invalidation/invalidation.service.spec.ts deleted file mode 100644 index 8e5e6d2c..00000000 --- a/src/caching/invalidation/invalidation.service.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { CacheInvalidationService } from './invalidation.service'; -import { CachingService } from '../caching.service'; -import { CacheStrategiesService } from '../strategies/cache-strategies.service'; -import { CACHE_EVENTS } from '../caching.constants'; -describe('CacheInvalidationService', () => { - let service: CacheInvalidationService; - let cachingService: jest.Mocked; - let eventEmitter: jest.Mocked; - beforeEach(async () => { - const mockCachingService = { - delPattern: jest.fn(), - del: jest.fn(), - clearAll: jest.fn(), - }; - const mockEventEmitter = { - emit: jest.fn(), - }; - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CacheInvalidationService, - CacheStrategiesService, - { - provide: CachingService, - useValue: mockCachingService, - }, - { - provide: EventEmitter2, - useValue: mockEventEmitter, - }, - ], - }).compile(); - service = module.get(CacheInvalidationService); - cachingService = module.get(CachingService); - eventEmitter = module.get(EventEmitter2); - }); - describe('invalidateByPattern', () => { - it('should invalidate by pattern', async () => { - cachingService.delPattern.mockResolvedValue(5); - const result = await service.invalidateByPattern('cache:course:*'); - expect(result.pattern).toBe('cache:course:*'); - expect(result.keysDeleted).toBe(5); - expect(cachingService.delPattern).toHaveBeenCalledWith('cache:course:*'); - }); - }); - describe('invalidateByPatterns', () => { - it('should invalidate multiple patterns', async () => { - cachingService.delPattern.mockResolvedValue(3); - const results = await service.invalidateByPatterns(['cache:course:*', 'cache:user:*']); - expect(results).toHaveLength(2); - expect(cachingService.delPattern).toHaveBeenCalledTimes(2); - }); - }); - describe('invalidateByTag', () => { - it('should invalidate by tag', async () => { - service.registerKeyWithTag('courses', 'cache:course:1'); - service.registerKeyWithTag('courses', 'cache:course:2'); - const result = await service.invalidateByTag('courses'); - expect(result).toBe(2); - expect(cachingService.del).toHaveBeenCalledTimes(2); - }); - it('should return 0 for non-existent tag', async () => { - const result = await service.invalidateByTag('nonexistent'); - expect(result).toBe(0); - }); - }); - describe('registerKeyWithTag', () => { - it('should register key with tag', () => { - service.registerKeyWithTag('courses', 'cache:course:1'); - const stats = service.getStats(); - expect(stats.registeredTags).toBe(1); - expect(stats.totalTrackedKeys).toBe(1); - }); - }); - describe('unregisterKeyFromTag', () => { - it('should unregister key from tag', () => { - service.registerKeyWithTag('courses', 'cache:course:1'); - service.unregisterKeyFromTag('courses', 'cache:course:1'); - const stats = service.getStats(); - expect(stats.totalTrackedKeys).toBe(0); - }); - }); - describe('invalidateCourse', () => { - it('should invalidate course cache', async () => { - cachingService.delPattern.mockResolvedValue(5); - const results = await service.invalidateCourse('course-123'); - expect(results.length).toBeGreaterThan(0); - expect(eventEmitter.emit).toHaveBeenCalledWith(CACHE_EVENTS.COURSE_UPDATED, { - courseId: 'course-123', - }); - }); - }); - describe('invalidateUser', () => { - it('should invalidate user cache', async () => { - cachingService.delPattern.mockResolvedValue(3); - const results = await service.invalidateUser('user-123'); - expect(results.length).toBeGreaterThan(0); - expect(eventEmitter.emit).toHaveBeenCalledWith(CACHE_EVENTS.USER_UPDATED, { - userId: 'user-123', - }); - }); - }); - describe('invalidateEnrollment', () => { - it('should invalidate enrollment cache', async () => { - cachingService.delPattern.mockResolvedValue(2); - const results = await service.invalidateEnrollment('enroll-123', 'course-123'); - expect(results.length).toBeGreaterThan(0); - expect(eventEmitter.emit).toHaveBeenCalledWith(CACHE_EVENTS.ENROLLMENT_UPDATED, { - enrollmentId: 'enroll-123', - courseId: 'course-123', - }); - }); - }); - describe('invalidateSearch', () => { - it('should invalidate search cache', async () => { - cachingService.delPattern.mockResolvedValue(10); - const results = await service.invalidateSearch(); - expect(results.length).toBeGreaterThan(0); - expect(eventEmitter.emit).toHaveBeenCalledWith(CACHE_EVENTS.SEARCH_INDEX_UPDATED); - }); - }); - describe('clearAll', () => { - it('should clear all cache', async () => { - cachingService.clearAll.mockResolvedValue(100); - const result = await service.clearAll(); - expect(result).toBe(100); - expect(cachingService.clearAll).toHaveBeenCalled(); - }); - }); - describe('getStats', () => { - it('should return stats', () => { - service.registerKeyWithTag('tag1', 'key1'); - service.registerKeyWithTag('tag1', 'key2'); - service.registerKeyWithTag('tag2', 'key3'); - const stats = service.getStats(); - expect(stats.registeredTags).toBe(2); - expect(stats.totalTrackedKeys).toBe(3); - }); - }); -}); diff --git a/src/caching/invalidation/invalidation.service.ts b/src/caching/invalidation/invalidation.service.ts deleted file mode 100644 index d1cada61..00000000 --- a/src/caching/invalidation/invalidation.service.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; -import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; -import { CachingService } from '../caching.service'; -import { CacheStrategiesService } from '../strategies/cache-strategies.service'; -import { CACHE_EVENTS } from '../caching.constants'; -export interface InvalidationResult { - pattern: string; - keysDeleted: number; -} - -/** - * Provides cache Invalidation operations. - */ -@Injectable() -export class CacheInvalidationService implements OnModuleInit, OnModuleDestroy { - private readonly logger = new Logger(CacheInvalidationService.name); - private readonly tagIndex: Map> = new Map(); - - constructor( - private readonly cachingService: CachingService, - private readonly strategiesService: CacheStrategiesService, - private readonly eventEmitter: EventEmitter2, - ) {} - - /** - * Executes on Module Init. - * @returns The operation result. - */ - onModuleInit() { - this.logger.log('Cache invalidation service initialized'); - } - - /** - * Executes on Module Destroy. - * @returns The operation result. - */ - onModuleDestroy() { - this.tagIndex.clear(); - } - - /** - * Invalidate cache by pattern - * @param pattern - Pattern to match (e.g., 'cache:course:*') - */ - async invalidateByPattern(pattern: string): Promise { - this.logger.debug(`Invalidating cache pattern: ${pattern}`); - const keysDeleted = await this.cachingService.delPattern(pattern); - - return { pattern, keysDeleted }; - } - - /** - * Invalidate cache by multiple patterns - * @param patterns - Array of patterns to invalidate - */ - async invalidateByPatterns(patterns: string[]): Promise { - const results: InvalidationResult[] = []; - - for (const pattern of patterns) { - const result = await this.invalidateByPattern(pattern); - results.push(result); - } - onModuleDestroy() { - this.tagIndex.clear(); - } - /** - * Invalidate cache by pattern - * @param pattern - Pattern to match (e.g., 'cache:course:*') - */ - async invalidateByPattern(pattern: string): Promise { - this.logger.debug(`Invalidating cache pattern: ${pattern}`); - const keysDeleted = await this.cachingService.delPattern(pattern); - return { pattern, keysDeleted }; - } - /** - * Invalidate cache by multiple patterns - * @param patterns - Array of patterns to invalidate - */ - async invalidateByPatterns(patterns: string[]): Promise { - const results: InvalidationResult[] = []; - for (const pattern of patterns) { - const result = await this.invalidateByPattern(pattern); - results.push(result); - } - return results; - } - /** - * Invalidate cache by tag - * @param tag - Tag to invalidate - */ - async invalidateByTag(tag: string): Promise { - const keys = this.tagIndex.get(tag); - if (!keys || keys.size === 0) { - return 0; - } - let deleted = 0; - for (const key of keys) { - await this.cachingService.del(key); - deleted++; - } - this.tagIndex.delete(tag); - this.logger.debug(`Invalidated ${deleted} keys for tag: ${tag}`); - return deleted; - } - /** - * Register a key with a tag for tag-based invalidation - * @param tag - Tag to associate with the key - * @param key - Cache key - */ - registerKeyWithTag(tag: string, key: string): void { - if (!this.tagIndex.has(tag)) { - this.tagIndex.set(tag, new Set()); - } - const tagSet = this.tagIndex.get(tag); - if (tagSet) { - tagSet.add(key); - } - } - - this.eventEmitter.emit(CACHE_EVENTS.ENROLLMENT_UPDATED, { enrollmentId, courseId }); - return this.invalidateByPatterns(patterns); - } - - /** - * Invalidate search cache - */ - async invalidateSearch(): Promise { - const patterns = ['cache:search:*']; - this.eventEmitter.emit(CACHE_EVENTS.SEARCH_INDEX_UPDATED); - return this.invalidateByPatterns(patterns); - } - - /** - * Clear all application cache - */ - async clearAll(): Promise { - return this.cachingService.clearAll(); - } - - // Event handlers for automatic invalidation - - /** - * Handles course Updated Event. - * @param payload The payload to process. - */ - @OnEvent(CACHE_EVENTS.COURSE_UPDATED) - async handleCourseUpdatedEvent(payload: { courseId: string }): Promise { - this.logger.debug(`Handling course updated event for: ${payload.courseId}`); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.COURSE_UPDATED); - await this.invalidateByPatterns(patterns); - } - - /** - * Handles course Deleted Event. - * @param payload The payload to process. - */ - @OnEvent(CACHE_EVENTS.COURSE_DELETED) - async handleCourseDeletedEvent(payload: { courseId: string }): Promise { - this.logger.debug(`Handling course deleted event for: ${payload.courseId}`); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.COURSE_DELETED); - await this.invalidateByPatterns(patterns); - } - - /** - * Handles user Updated Event. - * @param payload The payload to process. - */ - @OnEvent(CACHE_EVENTS.USER_UPDATED) - async handleUserUpdatedEvent(payload: { userId: string }): Promise { - this.logger.debug(`Handling user updated event for: ${payload.userId}`); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.USER_UPDATED); - await this.invalidateByPatterns(patterns); - } - - /** - * Handles user Deleted Event. - * @param payload The payload to process. - */ - @OnEvent(CACHE_EVENTS.USER_DELETED) - async handleUserDeletedEvent(payload: { userId: string }): Promise { - this.logger.debug(`Handling user deleted event for: ${payload.userId}`); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.USER_DELETED); - await this.invalidateByPatterns(patterns); - } - - /** - * Handles enrollment Created Event. - * @param payload The payload to process. - */ - @OnEvent(CACHE_EVENTS.ENROLLMENT_CREATED) - async handleEnrollmentCreatedEvent(payload: { - enrollmentId: string; - courseId: string; - }): Promise { - this.logger.debug(`Handling enrollment created event for: ${payload.enrollmentId}`); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.ENROLLMENT_CREATED); - await this.invalidateByPatterns(patterns); - } - - /** - * Handles enrollment Updated Event. - * @param payload The payload to process. - */ - @OnEvent(CACHE_EVENTS.ENROLLMENT_UPDATED) - async handleEnrollmentUpdatedEvent(payload: { - enrollmentId: string; - courseId?: string; - }): Promise { - this.logger.debug(`Handling enrollment updated event for: ${payload.enrollmentId}`); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.ENROLLMENT_UPDATED); - await this.invalidateByPatterns(patterns); - } - - /** - * Handles search Index Updated Event. - */ - @OnEvent(CACHE_EVENTS.SEARCH_INDEX_UPDATED) - async handleSearchIndexUpdatedEvent(): Promise { - this.logger.debug('Handling search index updated event'); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.SEARCH_INDEX_UPDATED); - await this.invalidateByPatterns(patterns); - } - - @OnEvent(CACHE_EVENTS.CATEGORY_UPDATED) - async handleCategoryUpdatedEvent(): Promise { - this.logger.debug('Handling category updated event'); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.CATEGORY_UPDATED); - await this.invalidateByPatterns(patterns); - } - - @OnEvent(CACHE_EVENTS.TAG_UPDATED) - async handleTagUpdatedEvent(): Promise { - this.logger.debug('Handling tag updated event'); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.TAG_UPDATED); - await this.invalidateByPatterns(patterns); - } - - @OnEvent(CACHE_EVENTS.LESSON_UPDATED) - async handleLessonUpdatedEvent(): Promise { - this.logger.debug('Handling lesson updated event'); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.LESSON_UPDATED); - await this.invalidateByPatterns(patterns); - } - - @OnEvent(CACHE_EVENTS.QUIZ_UPDATED) - async handleQuizUpdatedEvent(): Promise { - this.logger.debug('Handling quiz updated event'); - const patterns = this.strategiesService.getPatternsForEvent(CACHE_EVENTS.QUIZ_UPDATED); - await this.invalidateByPatterns(patterns); - } - - /** - * Get invalidation statistics - */ - getStats(): { - registeredTags: number; - totalTrackedKeys: number; - } { - let totalKeys = 0; - for (const keys of this.tagIndex.values()) { - totalKeys += keys.size; - } -} diff --git a/src/caching/strategies/cache-strategies.service.spec.ts b/src/caching/strategies/cache-strategies.service.spec.ts deleted file mode 100644 index 9b8fd9c8..00000000 --- a/src/caching/strategies/cache-strategies.service.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CacheStrategiesService } from './cache-strategies.service'; -import { CACHE_EVENTS } from '../caching.constants'; -describe('CacheStrategiesService', () => { - let service: CacheStrategiesService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [CacheStrategiesService], - }).compile(); - service = module.get(CacheStrategiesService); - }); - describe('getStrategy', () => { - it('should return course details strategy', () => { - const strategy = service.getStrategy('course:details'); - expect(strategy).toBeDefined(); - expect(strategy?.ttl).toBe(300); - expect(strategy?.prefix).toBe('cache:course'); - }); - it('should return user profile strategy', () => { - const strategy = service.getStrategy('user:profile'); - expect(strategy).toBeDefined(); - expect(strategy?.ttl).toBe(600); - expect(strategy?.prefix).toBe('cache:user:profile'); - }); - it('should return undefined for unknown strategy', () => { - const strategy = service.getStrategy('unknown:strategy'); - expect(strategy).toBeUndefined(); - }); - }); - describe('getTtl', () => { - it('should return TTL for known strategy', () => { - const ttl = service.getTtl('course:details'); - expect(ttl).toBe(300); - }); - it('should return default TTL for unknown strategy', () => { - const ttl = service.getTtl('unknown'); - expect(ttl).toBe(300); - }); - }); - describe('getPrefix', () => { - it('should return prefix for known strategy', () => { - const prefix = service.getPrefix('course:details'); - expect(prefix).toBe('cache:course'); - }); - it('should return default prefix for unknown strategy', () => { - const prefix = service.getPrefix('unknown'); - expect(prefix).toBe('cache'); - }); - }); - describe('getInvalidationEvents', () => { - it('should return events for course strategy', () => { - const events = service.getInvalidationEvents('course:details'); - expect(events).toContain(CACHE_EVENTS.COURSE_UPDATED); - expect(events).toContain(CACHE_EVENTS.COURSE_DELETED); - }); - }); - describe('getRelatedPatterns', () => { - it('should return patterns for strategy', () => { - const patterns = service.getRelatedPatterns('course:details'); - expect(patterns).toContain('cache:course:*'); - expect(patterns).toContain('cache:courses:list:*'); - }); - }); - describe('getStrategiesForEvent', () => { - it('should return strategies for COURSE_UPDATED event', () => { - const strategies = service.getStrategiesForEvent(CACHE_EVENTS.COURSE_UPDATED); - expect(strategies.length).toBeGreaterThan(0); - expect(strategies.some((s) => s.name === 'course:details')).toBe(true); - }); - }); - describe('getPatternsForEvent', () => { - it('should return patterns for COURSE_UPDATED event', () => { - const patterns = service.getPatternsForEvent(CACHE_EVENTS.COURSE_UPDATED); - expect(patterns.length).toBeGreaterThan(0); - expect(patterns.some((p) => p.includes('course'))).toBe(true); - }); - }); - describe('getAllStrategies', () => { - it('should return all registered strategies', () => { - const strategies = service.getAllStrategies(); - expect(strategies.length).toBeGreaterThan(0); - expect(strategies.some((s) => s.name === 'course:details')).toBe(true); - expect(strategies.some((s) => s.name === 'user:profile')).toBe(true); - }); - }); - describe('buildKey', () => { - it('should build key with parts', () => { - const key = service.buildKey('course:details', '123'); - expect(key).toBe('cache:course:123'); - }); - it('should build key with multiple parts', () => { - const key = service.buildKey('course:details', '123', 'modules'); - expect(key).toBe('cache:course:123:modules'); - }); - }); - describe('getDefaultStrategy', () => { - it('should return default strategy', () => { - const strategy = service.getDefaultStrategy(); - expect(strategy.name).toBe('default'); - expect(strategy.ttl).toBe(300); - }); - }); - describe('registerStrategy', () => { - it('should register new strategy', () => { - service.registerStrategy({ - name: 'custom:strategy', - ttl: 1000, - prefix: 'cache:custom', - invalidateOnEvents: [], - relatedPatterns: ['cache:custom:*'], - }); - const strategy = service.getStrategy('custom:strategy'); - expect(strategy).toBeDefined(); - expect(strategy?.ttl).toBe(1000); - }); - }); -}); diff --git a/src/caching/strategies/cache-strategies.service.ts b/src/caching/strategies/cache-strategies.service.ts deleted file mode 100644 index 5d4068b3..00000000 --- a/src/caching/strategies/cache-strategies.service.ts +++ /dev/null @@ -1,407 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { CACHE_TTL, CACHE_PREFIXES, CACHE_EVENTS } from '../caching.constants'; - -export interface ICacheStrategy { - ttl: number; - prefix: string; - invalidateOn: string[]; -} - -export interface ICacheStrategyConfig { - name: string; - ttl: number; - prefix: string; - invalidateOnEvents: string[]; - relatedPatterns: string[]; -} - -/** - * Provides cache Strategies operations. - */ -@Injectable() -export class CacheStrategiesService { - private readonly strategies: Map = new Map(); - - constructor() { - this.initializeStrategies(); - } - - private initializeStrategies(): void { - // Course strategies - this.registerStrategy({ - name: 'course:details', - ttl: CACHE_TTL.COURSE_DETAILS, - prefix: CACHE_PREFIXES.COURSE, - invalidateOnEvents: [CACHE_EVENTS.COURSE_UPDATED, CACHE_EVENTS.COURSE_DELETED], - relatedPatterns: ['cache:course:*', 'cache:courses:list:*'], - }); - - this.registerStrategy({ - name: 'course:metadata', - ttl: CACHE_TTL.COURSE_METADATA, - prefix: CACHE_PREFIXES.COURSE, - invalidateOnEvents: [CACHE_EVENTS.COURSE_UPDATED], - relatedPatterns: ['cache:course:*'], - }); - - this.registerStrategy({ - name: 'courses:list', - ttl: CACHE_TTL.COURSE_METADATA, - prefix: CACHE_PREFIXES.COURSES_LIST, - invalidateOnEvents: [ - CACHE_EVENTS.COURSE_UPDATED, - CACHE_EVENTS.COURSE_DELETED, - CACHE_EVENTS.ENROLLMENT_CREATED, - ], - relatedPatterns: ['cache:courses:list:*'], - }); - - // User strategies - this.registerStrategy({ - name: 'user:profile', - ttl: CACHE_TTL.USER_PROFILE, - prefix: CACHE_PREFIXES.USER_PROFILE, - invalidateOnEvents: [CACHE_EVENTS.USER_UPDATED, CACHE_EVENTS.USER_DELETED], - relatedPatterns: ['cache:user:*', 'cache:user:profile:*'], - }); - - this.registerStrategy({ - name: 'user:session', - ttl: CACHE_TTL.USER_SESSION, - prefix: CACHE_PREFIXES.USER, - invalidateOnEvents: [CACHE_EVENTS.USER_DELETED], - relatedPatterns: ['cache:user:*'], - }); - - // Search strategies - this.registerStrategy({ - name: 'search:results', - ttl: CACHE_TTL.SEARCH_RESULTS, - prefix: CACHE_PREFIXES.SEARCH, - invalidateOnEvents: [ - CACHE_EVENTS.COURSE_UPDATED, - CACHE_EVENTS.COURSE_DELETED, - CACHE_EVENTS.SEARCH_INDEX_UPDATED, - ], - relatedPatterns: ['cache:search:*'], - }); - - // Popular content - this.registerStrategy({ - name: 'popular:courses', - ttl: CACHE_TTL.POPULAR_COURSES, - prefix: CACHE_PREFIXES.POPULAR, - invalidateOnEvents: [CACHE_EVENTS.COURSE_UPDATED, CACHE_EVENTS.ENROLLMENT_CREATED], - relatedPatterns: ['cache:popular:*'], - }); - - // Enrollment strategies - this.registerStrategy({ - name: 'enrollment:data', - ttl: CACHE_TTL.ENROLLMENT_DATA, - prefix: CACHE_PREFIXES.ENROLLMENT, - invalidateOnEvents: [CACHE_EVENTS.ENROLLMENT_CREATED, CACHE_EVENTS.ENROLLMENT_UPDATED], - relatedPatterns: ['cache:enrollment:*'], - }); - - // Featured content - this.registerStrategy({ - name: 'featured:content', - ttl: CACHE_TTL.STATIC_CONTENT, - prefix: CACHE_PREFIXES.FEATURED, - invalidateOnEvents: [CACHE_EVENTS.COURSE_UPDATED], - relatedPatterns: ['cache:featured:*'], - }); - - // New strategies - this.registerStrategy({ - name: 'category:list', - ttl: CACHE_TTL.STATIC_CONTENT, - prefix: CACHE_PREFIXES.CATEGORY, - invalidateOnEvents: [CACHE_EVENTS.CATEGORY_UPDATED], - relatedPatterns: ['cache:category:*'], - }); - - this.registerStrategy({ - name: 'tag:list', - ttl: CACHE_TTL.STATIC_CONTENT, - prefix: CACHE_PREFIXES.TAG, - invalidateOnEvents: [CACHE_EVENTS.TAG_UPDATED], - relatedPatterns: ['cache:tag:*'], - }); - - this.registerStrategy({ - name: 'lesson:details', - ttl: CACHE_TTL.COURSE_DETAILS, - prefix: CACHE_PREFIXES.LESSON, - invalidateOnEvents: [CACHE_EVENTS.LESSON_UPDATED, CACHE_EVENTS.COURSE_UPDATED], - relatedPatterns: ['cache:lesson:*'], - }); - - this.registerStrategy({ - name: 'quiz:details', - ttl: CACHE_TTL.COURSE_DETAILS, - prefix: CACHE_PREFIXES.QUIZ, - invalidateOnEvents: [CACHE_EVENTS.QUIZ_UPDATED, CACHE_EVENTS.COURSE_UPDATED], - relatedPatterns: ['cache:quiz:*'], - }); - } - - /** - * Register a new cache strategy - */ - registerStrategy(config: ICacheStrategyConfig): void { - this.strategies.set(config.name, config); - } - - /** - * Get a strategy by name - */ - getStrategy(name: string): ICacheStrategyConfig | undefined { - return this.strategies.get(name); - } - - /** - * Get TTL for a strategy - */ - getTtl(strategyName: string): number { - const strategy = this.strategies.get(strategyName); - return strategy?.ttl ?? CACHE_TTL.COURSE_DETAILS; - } - - /** - * Get prefix for a strategy - */ - getPrefix(strategyName: string): string { - const strategy = this.strategies.get(strategyName); - return strategy?.prefix ?? 'cache'; - } - - /** - * Get invalidation events for a strategy - */ - getInvalidationEvents(strategyName: string): string[] { - const strategy = this.strategies.get(strategyName); - return strategy?.invalidateOnEvents ?? []; - } - - /** - * Get related patterns to invalidate when a strategy is invalidated - */ - getRelatedPatterns(strategyName: string): string[] { - const strategy = this.strategies.get(strategyName); - return strategy?.relatedPatterns ?? []; - } - - /** - * Get all strategies that should be invalidated for a given event - */ - getStrategiesForEvent(eventName: string): ICacheStrategyConfig[] { - const strategies: ICacheStrategyConfig[] = []; - - for (const strategy of this.strategies.values()) { - if (strategy.invalidateOnEvents.includes(eventName)) { - strategies.push(strategy); - } - } - private initializeStrategies(): void { - // Course strategies - this.registerStrategy({ - name: 'course:details', - ttl: CACHE_TTL.COURSE_DETAILS, - prefix: CACHE_PREFIXES.COURSE, - invalidateOnEvents: [CACHE_EVENTS.COURSE_UPDATED, CACHE_EVENTS.COURSE_DELETED], - relatedPatterns: ['cache:course:*', 'cache:courses:list:*'], - }); - this.registerStrategy({ - name: 'course:metadata', - ttl: CACHE_TTL.COURSE_METADATA, - prefix: CACHE_PREFIXES.COURSE, - invalidateOnEvents: [CACHE_EVENTS.COURSE_UPDATED], - relatedPatterns: ['cache:course:*'], - }); - this.registerStrategy({ - name: 'courses:list', - ttl: CACHE_TTL.COURSE_METADATA, - prefix: CACHE_PREFIXES.COURSES_LIST, - invalidateOnEvents: [ - CACHE_EVENTS.COURSE_UPDATED, - CACHE_EVENTS.COURSE_DELETED, - CACHE_EVENTS.ENROLLMENT_CREATED, - ], - relatedPatterns: ['cache:courses:list:*'], - }); - // User strategies - this.registerStrategy({ - name: 'user:profile', - ttl: CACHE_TTL.USER_PROFILE, - prefix: CACHE_PREFIXES.USER_PROFILE, - invalidateOnEvents: [CACHE_EVENTS.USER_UPDATED, CACHE_EVENTS.USER_DELETED], - relatedPatterns: ['cache:user:*', 'cache:user:profile:*'], - }); - this.registerStrategy({ - name: 'user:session', - ttl: CACHE_TTL.USER_SESSION, - prefix: CACHE_PREFIXES.USER, - invalidateOnEvents: [CACHE_EVENTS.USER_DELETED], - relatedPatterns: ['cache:user:*'], - }); - // Search strategies - this.registerStrategy({ - name: 'search:results', - ttl: CACHE_TTL.SEARCH_RESULTS, - prefix: CACHE_PREFIXES.SEARCH, - invalidateOnEvents: [ - CACHE_EVENTS.COURSE_UPDATED, - CACHE_EVENTS.COURSE_DELETED, - CACHE_EVENTS.SEARCH_INDEX_UPDATED, - ], - relatedPatterns: ['cache:search:*'], - }); - // Popular content - this.registerStrategy({ - name: 'popular:courses', - ttl: CACHE_TTL.POPULAR_COURSES, - prefix: CACHE_PREFIXES.POPULAR, - invalidateOnEvents: [CACHE_EVENTS.COURSE_UPDATED, CACHE_EVENTS.ENROLLMENT_CREATED], - relatedPatterns: ['cache:popular:*'], - }); - // Enrollment strategies - this.registerStrategy({ - name: 'enrollment:data', - ttl: CACHE_TTL.ENROLLMENT_DATA, - prefix: CACHE_PREFIXES.ENROLLMENT, - invalidateOnEvents: [CACHE_EVENTS.ENROLLMENT_CREATED, CACHE_EVENTS.ENROLLMENT_UPDATED], - relatedPatterns: ['cache:enrollment:*'], - }); - // Featured content - this.registerStrategy({ - name: 'featured:content', - ttl: CACHE_TTL.STATIC_CONTENT, - prefix: CACHE_PREFIXES.FEATURED, - invalidateOnEvents: [CACHE_EVENTS.COURSE_UPDATED], - relatedPatterns: ['cache:featured:*'], - }); - } - /** - * Register a new cache strategy - */ - registerStrategy(config: CacheStrategyConfig): void { - this.strategies.set(config.name, config); - } - /** - * Get a strategy by name - */ - getStrategy(name: string): CacheStrategyConfig | undefined { - return this.strategies.get(name); - } - /** - * Get TTL for a strategy - */ - getTtl(strategyName: string): number { - const strategy = this.strategies.get(strategyName); - return strategy?.ttl ?? CACHE_TTL.COURSE_DETAILS; - } - /** - * Get prefix for a strategy - */ - getPrefix(strategyName: string): string { - const strategy = this.strategies.get(strategyName); - return strategy?.prefix ?? 'cache'; - } - /** - * Get invalidation events for a strategy - */ - getInvalidationEvents(strategyName: string): string[] { - const strategy = this.strategies.get(strategyName); - return strategy?.invalidateOnEvents ?? []; - } - /** - * Get related patterns to invalidate when a strategy is invalidated - */ - getRelatedPatterns(strategyName: string): string[] { - const strategy = this.strategies.get(strategyName); - return strategy?.relatedPatterns ?? []; - } - /** - * Get all strategies that should be invalidated for a given event - */ - getStrategiesForEvent(eventName: string): CacheStrategyConfig[] { - const strategies: CacheStrategyConfig[] = []; - for (const strategy of this.strategies.values()) { - if (strategy.invalidateOnEvents.includes(eventName)) { - strategies.push(strategy); - } - } - return strategies; - } - /** - * Get all patterns to invalidate for a given event - */ - getPatternsForEvent(eventName: string): string[] { - const strategies = this.getStrategiesForEvent(eventName); - const patterns = new Set(); - for (const strategy of strategies) { - for (const pattern of strategy.relatedPatterns) { - patterns.add(pattern); - } - } - return Array.from(patterns); - } - /** - * Get all registered strategies - */ - getAllStrategies(): CacheStrategyConfig[] { - return Array.from(this.strategies.values()); - } - /** - * Build a cache key for a strategy - */ - buildKey(strategyName: string, ...parts: Array): string { - const prefix = this.getPrefix(strategyName); - return `${prefix}:${parts.join(':')}`; - } - /** - * Get default strategy configuration - */ - getDefaultStrategy(): CacheStrategyConfig { - return { - name: 'default', - ttl: CACHE_TTL.COURSE_DETAILS, - prefix: 'cache', - invalidateOnEvents: [], - relatedPatterns: ['cache:*'], - }; - } - - return Array.from(patterns); - } - - /** - * Get all registered strategies - */ - getAllStrategies(): ICacheStrategyConfig[] { - return Array.from(this.strategies.values()); - } - - /** - * Build a cache key for a strategy - */ - buildKey(strategyName: string, ...parts: Array): string { - const prefix = this.getPrefix(strategyName); - return `${prefix}:${parts.join(':')}`; - } - - /** - * Get default strategy configuration - */ - getDefaultStrategy(): ICacheStrategyConfig { - return { - name: 'default', - ttl: CACHE_TTL.COURSE_DETAILS, - prefix: 'cache', - invalidateOnEvents: [], - relatedPatterns: ['cache:*'], - }; - } -} diff --git a/src/caching/warming/cache-warming.service.ts b/src/caching/warming/cache-warming.service.ts deleted file mode 100644 index 3cc55615..00000000 --- a/src/caching/warming/cache-warming.service.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject, Optional, } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { CachingService } from '../caching.service'; -import { CacheStrategiesService } from '../strategies/cache-strategies.service'; -import { CACHE_TTL, CACHE_PREFIXES } from '../caching.constants'; - -export interface ICacheWarmingConfig { - /** - * Whether cache warming is enabled - */ - enabled: boolean; - - /** - * Warm popular courses on startup - */ - warmPopularCourses: boolean; - - /** - * Warm featured content on startup - */ - warmFeaturedContent: boolean; - - /** - * Number of popular courses to warm - */ - popularCoursesLimit: number; - - /** - * Warm system configuration - */ - warmSystemConfig: boolean; - - /** - * Delay before starting cache warming (ms) - */ - startupDelay: number; -} - -export interface IWarmedData { - key: string; - type: string; - timestamp: Date; -} -/** - * Interface for data providers that can be warmed - */ -export interface ICacheWarmableProvider { - /** - * Get data to warm into cache - */ - getWarmableData(): Promise>; -} - -/** - * Provides cache Warming operations. - */ -@Injectable() -export class CacheWarmingService implements OnModuleInit, OnModuleDestroy { - private readonly logger = new Logger(CacheWarmingService.name); - private readonly config: ICacheWarmingConfig; - private warmedKeys: Map = new Map(); - private warmupInterval?: NodeJS.Timeout; - private dataProviders: ICacheWarmableProvider[] = []; - - constructor( - private readonly cachingService: CachingService, - private readonly strategiesService: CacheStrategiesService, - private readonly configService: ConfigService, - @Optional() @Inject('CACHE_WARMING_CONFIG') config?: Partial, - ) { - this.config = { - enabled: this.configService.get('CACHE_WARMING_ENABLED') !== 'false', - warmPopularCourses: true, - warmFeaturedContent: true, - popularCoursesLimit: parseInt( - this.configService.get('CACHE_WARMING_POPULAR_LIMIT') || '10', - 10, - ), - warmSystemConfig: true, - startupDelay: parseInt(this.configService.get('CACHE_WARMING_DELAY') || '5000', 10), - ...config, - }; - } - - /** - * Register a data provider for cache warming - */ - registerDataProvider(provider: ICacheWarmableProvider): void { - this.dataProviders.push(provider); - } - - /** - * Executes on Module Init. - */ - async onModuleInit(): Promise { - if (!this.config.enabled) { - this.logger.log('Cache warming is disabled'); - return; - } - - // Delay warming to allow the application to fully start - setTimeout(() => { - this.warmCache().catch((error) => { - this.logger.error('Failed to warm cache on startup', error); - }); - }, this.config.startupDelay); - - // Schedule periodic refresh of warmed data - this.schedulePeriodicWarmup(); - } - - /** - * Executes on Module Destroy. - */ - onModuleDestroy(): void { - if (this.warmupInterval) { - clearInterval(this.warmupInterval); - } - async onModuleInit(): Promise { - if (!this.config.enabled) { - this.logger.log('Cache warming is disabled'); - return; - } - // Delay warming to allow the application to fully start - setTimeout(() => { - this.warmCache().catch((error) => { - this.logger.error('Failed to warm cache on startup', error); - }); - }, this.config.startupDelay); - // Schedule periodic refresh of warmed data - this.schedulePeriodicWarmup(); - } - onModuleDestroy(): void { - if (this.warmupInterval) { - clearInterval(this.warmupInterval); - } - this.warmedKeys.clear(); - } - /** - * Warm cache with critical data - */ - async warmCache(): Promise { - this.logger.log('Starting cache warming...'); - const startTime = Date.now(); - try { - // Warm data from registered providers - await this.warmFromProviders(); - // Warm built-in data types - if (this.config.warmPopularCourses) { - await this.warmPopularCourses(); - } - if (this.config.warmFeaturedContent) { - await this.warmFeaturedContent(); - } - if (this.config.warmSystemConfig) { - await this.warmSystemConfig(); - } - const duration = Date.now() - startTime; - this.logger.log(`Cache warming completed in ${duration}ms. Warmed ${this.warmedKeys.size} keys.`); - } - catch (error) { - this.logger.error('Cache warming failed', error); - throw error; - } - } - /** - * Warm data from registered providers - */ - private async warmFromProviders(): Promise { - for (const provider of this.dataProviders) { - try { - const items = await provider.getWarmableData(); - for (const item of items) { - await this.cachingService.set(item.key, item.data, item.ttl); - this.trackWarmedKey(item.key, 'provider'); - } - } - catch (error) { - this.logger.warn('Failed to warm data from provider', error); - } - } - } - /** - * Warm popular courses - * This is a placeholder - in production, this would fetch from CourseService - */ - private async warmPopularCourses(): Promise { - this.logger.debug('Warming popular courses...'); - // Placeholder: In a real implementation, this would inject CourseService - // and fetch actual popular courses - const popularCoursesKey = `${CACHE_PREFIXES.POPULAR}:courses`; - // Simulate warming with placeholder data - const placeholderData = { - courses: [], - warmedAt: new Date().toISOString(), - type: 'popular', - }; - await this.cachingService.set(popularCoursesKey, placeholderData, CACHE_TTL.POPULAR_COURSES); - this.trackWarmedKey(popularCoursesKey, 'popular_courses'); - this.logger.debug(`Warmed popular courses key: ${popularCoursesKey}`); - } - /** - * Warm featured content - */ - private async warmFeaturedContent(): Promise { - this.logger.debug('Warming featured content...'); - const featuredKey = `${CACHE_PREFIXES.FEATURED}:content`; - const placeholderData = { - content: [], - warmedAt: new Date().toISOString(), - type: 'featured', - }; - await this.cachingService.set(featuredKey, placeholderData, CACHE_TTL.STATIC_CONTENT); - this.trackWarmedKey(featuredKey, 'featured_content'); - this.logger.debug(`Warmed featured content key: ${featuredKey}`); - } - /** - * Warm system configuration - */ - private async warmSystemConfig(): Promise { - this.logger.debug('Warming system configuration...'); - const configKey = CACHE_PREFIXES.SYSTEM_CONFIG; - const configData = { - version: this.configService.get('npm_package_version') || '1.0.0', - environment: this.configService.get('NODE_ENV') || 'development', - warmedAt: new Date().toISOString(), - }; - await this.cachingService.set(configKey, configData, CACHE_TTL.STATIC_CONTENT); - this.trackWarmedKey(configKey, 'system_config'); - this.logger.debug(`Warmed system config key: ${configKey}`); - } - /** - * Schedule periodic cache warming - */ - private schedulePeriodicWarmup(): void { - // Refresh warmed data every 30 minutes - const interval = 30 * 60 * 1000; - this.warmupInterval = setInterval(async () => { - this.logger.debug('Running scheduled cache warming...'); - try { - await this.warmCache(); - } - catch (error) { - this.logger.error('Scheduled cache warming failed', error); - } - }, interval); - } - /** - * Track a warmed key - */ - private trackWarmedKey(key: string, type: string): void { - this.warmedKeys.set(key, { - key, - type, - timestamp: new Date(), - }); - } - /** - * Get statistics about warmed keys - */ - getStats(): { - totalKeys: number; - byType: Record; - lastWarmup: Date | null; - } { - const byType: Record = {}; - let lastWarmup: Date | null = null; - for (const warmed of this.warmedKeys.values()) { - byType[warmed.type] = (byType[warmed.type] || 0) + 1; - if (!lastWarmup || warmed.timestamp > lastWarmup) { - lastWarmup = warmed.timestamp; - } - } - return { - totalKeys: this.warmedKeys.size, - byType, - lastWarmup, - }; - } - /** - * Force refresh all warmed data - */ - async refreshAll(): Promise { - this.logger.log('Force refreshing all warmed data...'); - this.warmedKeys.clear(); - await this.warmCache(); - } - - return { - totalKeys: this.warmedKeys.size, - byType, - lastWarmup, - }; - } - - /** - * Force refresh all warmed data - */ - async refreshAll(): Promise { - this.logger.log('Force refreshing all warmed data...'); - this.warmedKeys.clear(); - await this.warmCache(); - } - - /** - * Check if a key was warmed - */ - isWarmed(key: string): boolean { - return this.warmedKeys.has(key); - } - - /** - * Get all warmed keys - */ - getWarmedKeys(): IWarmedData[] { - return Array.from(this.warmedKeys.values()); - } -} diff --git a/src/cdn/caching/edge-caching.service.ts b/src/cdn/caching/edge-caching.service.ts deleted file mode 100644 index e1d398f0..00000000 --- a/src/cdn/caching/edge-caching.service.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { CloudflareService } from '../providers/cloudflare.service'; - -export interface ICacheEntry { - url: string; - ttl: number; - lastModified: Date; - etag?: string; -} - -export interface IPurgeResult { - success: boolean; - purgedUrls: string[]; - failedUrls: string[]; - provider: string; -} - -/** - * Provides edge Caching operations. - */ -@Injectable() -export class EdgeCachingService { - private readonly logger = new Logger(EdgeCachingService.name); - - constructor(private cloudflareService: CloudflareService) {} - - /** - * Retrieves edge Url. - * @param originalUrl The original url. - * @param location The location. - * @returns The resulting string value. - */ - async getEdgeUrl(originalUrl: string, location?: string): Promise { - // In a real implementation, this would return the CDN URL for the optimal edge location - // For now, just return the original URL with CDN prefix - const cdnUrl = originalUrl.replace(/^https?:\/\/[^/]+/, 'https://cdn.example.com'); - - // Add location-based routing if needed - if (location) { - return `${cdnUrl}?location=${location}`; - } - - return cdnUrl; - } - - async purgeContent(contentId: string): Promise { - const urls = await this.getContentUrls(contentId); - return this.purgeUrls(urls); - } - - async purgeUrls(urls: string[]): Promise { - this.logger.log(`Purging ${urls.length} URLs from edge cache`); - - const results: IPurgeResult[] = []; - - // Purge from Cloudflare - try { - const cfResult = await this.cloudflareService.purgeUrls(urls); - results.push({ - success: cfResult.success, - purgedUrls: cfResult.purgedUrls, - failedUrls: cfResult.failedUrls, - provider: 'cloudflare', - }); - } catch (error) { - this.logger.error('Cloudflare purge failed:', error); - results.push({ - success: false, - purgedUrls: [], - failedUrls: urls, - provider: 'cloudflare', - }); - } - - // Combine results - const success = results.every((r) => r.success); - const purgedUrls = results.flatMap((r) => r.purgedUrls); - const failedUrls = results.flatMap((r) => r.failedUrls); - - return { - success, - purgedUrls, - failedUrls, - provider: 'all', - }; - } - - async purgeByTags(tags: string[]): Promise { - this.logger.log(`Purging content by tags: ${tags.join(', ')}`); - - // Get URLs associated with tags - const urls = await this.getUrlsByTags(tags); - - return this.purgeUrls(urls); - } - - async purgeByPattern(pattern: string): Promise { - this.logger.log(`Purging content by pattern: ${pattern}`); - - // Get URLs matching pattern - const urls = await this.getUrlsByPattern(pattern); - - return this.purgeUrls(urls); - } - - /** - * Warms cache. - * @param urls The urls. - */ - async warmCache(urls: string[]): Promise { - this.logger.log(`Warming cache for ${urls.length} URLs`); - - // Prefetch content to edge locations - for (const url of urls) { - try { - await this.prefetchToEdge(url); - } catch (error) { - this.logger.error(`Failed to prefetch ${url}:`, error); - } - } - } - - /** - * Retrieves cache Status. - * @param _url The url. - * @returns The resulting promise<{ - cached: boolean; - age?: number; - expires?: date; - last modified?: date; - }>. - */ - async getCacheStatus(_url: string): Promise<{ - cached: boolean; - age?: number; - expires?: Date; - lastModified?: Date; - }> { - // Check if URL is cached in edge - // This would typically involve HEAD requests to CDN endpoints - return { - cached: false, // Mock implementation - age: undefined, - expires: undefined, - lastModified: undefined, - }; - } - - async setCacheRules(rules: ICacheRule[]): Promise { - // Apply caching rules to CDN configuration - for (const rule of rules) { - await this.applyCacheRule(rule); - } - } - - private async getContentUrls(contentId: string): Promise { - // Implementation would fetch all URLs associated with content ID - // Including original, optimized versions, etc. - return [ - `https://cdn.example.com/${contentId}`, - `https://cdn.example.com/${contentId}_optimized.webp`, - `https://cdn.example.com/${contentId}_w640.webp`, - ]; - } - - private async getUrlsByTags(_tags: string[]): Promise { - // Implementation would query database for URLs with specific tags - return []; - } - - private async getUrlsByPattern(_pattern: string): Promise { - // Implementation would find URLs matching pattern - return []; - } - - private async prefetchToEdge(_url: string): Promise { - // Implementation would make requests to warm the cache - // This might involve calling CDN APIs or making HTTP requests - } - - private async applyCacheRule(rule: ICacheRule): Promise { - // Implementation would update CDN configuration - this.logger.log(`Applying cache rule: ${rule.pattern} -> TTL: ${rule.ttl}`); - } -} - -export interface ICacheRule { - pattern: string; - ttl: number; - headers?: Record; - queryString?: boolean; -} diff --git a/src/cdn/cdn.controller.ts b/src/cdn/cdn.controller.ts deleted file mode 100644 index b6235423..00000000 --- a/src/cdn/cdn.controller.ts +++ /dev/null @@ -1,675 +0,0 @@ -import { - Controller, - Post, - Get, - Delete, - Param, - Query, - Body, - IUploadedFile, - UseInterceptors, - HttpException, - HttpStatus, - Logger, - BadRequestException, -} from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiConsumes, - ApiBody, - ApiParam, - ApiQuery, -} from '@nestjs/swagger'; -import { IUploadedFile as IFileUpload } from '../common/types/file.types'; -import { CdnService } from './cdn.service'; -import { UploadContentDto } from './dto/upload-content.dto'; -import { ContentMetadata } from './entities/content-metadata.entity'; -import { - FileValidationService, - IFileValidationResult, -} from '../media/validation/file-validation.service'; -import { MalwareScanningService } from '../media/validation/malware-scanning.service'; -import { ImageProcessingService } from '../media/processing/image-processing.service'; -import { - ALLOWED_FILE_TYPES, - FILE_SIZE_LIMITS, -} from '../media/validation/file-validation.constants'; - -/** - * Exposes cdn endpoints. - */ -@ApiTags('CDN') -@Controller('cdn') -export class CdnController { - private readonly logger = new Logger(CdnController.name); - - constructor( - private readonly cdnService: CdnService, - private readonly fileValidation: FileValidationService, - private readonly malwareScanning: MalwareScanningService, - private readonly imageProcessing: ImageProcessingService, - ) {} - - /** - * Uploads content. - * @param file The file to process. - * @param options The options. - * @returns The resulting content metadata. - */ - @Post('upload') - @UseInterceptors(FileInterceptor('file')) - @ApiOperation({ summary: 'Upload content to CDN with full validation' }) - @ApiConsumes('multipart/form-data') - @ApiBody({ - description: 'Content upload with validation and optimization options', - type: UploadContentDto, - }) - @ApiResponse({ - status: 201, - description: 'Content uploaded successfully', - type: ContentMetadata, - }) - @ApiResponse({ status: 400, description: 'Validation failed or bad request' }) - @ApiResponse({ status: 403, description: 'Malware detected' }) - @ApiResponse({ status: 413, description: 'File too large' }) - @ApiResponse({ status: 415, description: 'Unsupported media type' }) - @ApiResponse({ status: 500, description: 'Internal server error' }) - async uploadContent( - @IUploadedFile() file: IFileUpload, - @Body() options: UploadContentDto, - ): Promise { - try { - if (!file) { - throw new HttpException('No file provided', HttpStatus.BAD_REQUEST); - } - - this.logger.log(`Uploading file: ${file.originalname} (${file.size} bytes)`); - - // Step 1: Validate file - const validationResult = await this.fileValidation.validateFile(file); - if (!validationResult.valid) { - this.logger.warn( - `File validation failed for ${file.originalname}:`, - validationResult.errors, - ); - throw new BadRequestException({ - message: 'File validation failed', - errors: validationResult.errors, - warnings: validationResult.warnings, - allowedTypes: Object.values(ALLOWED_FILE_TYPES).flat(), - sizeLimits: FILE_SIZE_LIMITS, - }); - } - - // Step 2: Malware scan - if (this.malwareScanning.isScanningAvailable()) { - this.logger.log(`Scanning file for malware: ${file.originalname}`); - const scanResult = await this.malwareScanning.scanFile(file); - - if (!scanResult.clean) { - const errorMsg = - scanResult.threats.length > 0 - ? `Malware detected: ${scanResult.threats.join(', ')}` - : 'File failed security scan'; - this.logger.error(`Malware detected in ${file.originalname}:`, scanResult.threats); - throw new HttpException(errorMsg, HttpStatus.FORBIDDEN); - } - } - } - - /** - * Validates file. - * @param file The file to process. - * @returns The resulting file validation result. - */ - @Post('validate') - @UseInterceptors(FileInterceptor('file')) - @ApiOperation({ summary: 'Validate file without uploading' }) - @ApiConsumes('multipart/form-data') - @ApiResponse({ - status: 200, - description: 'File validation result', - schema: { - type: 'object', - properties: { - valid: { type: 'boolean' }, - mimeType: { type: 'string' }, - fileType: { type: 'string' }, - size: { type: 'number' }, - maxSize: { type: 'number' }, - errors: { type: 'array', items: { type: 'string' } }, - warnings: { type: 'array', items: { type: 'string' } }, - metadata: { - type: 'object', - properties: { - width: { type: 'number' }, - height: { type: 'number' }, - format: { type: 'string' }, - hasAlpha: { type: 'boolean' }, - }, - }, - }, - }, - }) - @ApiResponse({ status: 400, description: 'No file provided' }) - async validateFile(@IUploadedFile() file: IFileUpload): Promise { - if (!file) { - throw new BadRequestException('No file provided'); - } - - this.logger.log(`Validating file: ${file.originalname}`); - return this.fileValidation.validateFile(file); - } - - /** - * Executes scan File. - * @param file The file to process. - * @returns The operation result. - */ - @Post('scan') - @UseInterceptors(FileInterceptor('file')) - @ApiOperation({ summary: 'Scan file for malware without uploading' }) - @ApiConsumes('multipart/form-data') - @ApiResponse({ - status: 200, - description: 'Malware scan result', - schema: { - type: 'object', - properties: { - clean: { type: 'boolean' }, - threats: { type: 'array', items: { type: 'string' } }, - scanTime: { type: 'number' }, - scannerVersion: { type: 'string' }, - error: { type: 'string' }, - }, - }, - }) - @ApiResponse({ status: 400, description: 'No file provided' }) - @ApiResponse({ status: 503, description: 'Scanning service not available' }) - async scanFile(@IUploadedFile() file: IFileUpload) { - if (!file) { - throw new BadRequestException('No file provided'); - } - - if (!this.malwareScanning.isScanningAvailable()) { - throw new HttpException( - 'Malware scanning service not available', - HttpStatus.SERVICE_UNAVAILABLE, - ); - } - - this.logger.log(`Scanning file: ${file.originalname}`); - return this.malwareScanning.scanFile(file); - } - - /** - * Returns allowed Types. - * @returns The operation result. - */ - @Get('allowed-types') - @ApiOperation({ summary: 'Get allowed file types and size limits' }) - @ApiResponse({ - status: 200, - description: 'Allowed file types and limits', - schema: { - type: 'object', - properties: { - allowedTypes: { type: 'object' }, - sizeLimits: { type: 'object' }, - dimensionLimits: { type: 'object' }, - }, - }, - }) - getAllowedTypes() { - return { - allowedTypes: ALLOWED_FILE_TYPES, - sizeLimits: { - image: this.formatBytes(FILE_SIZE_LIMITS.IMAGE_MAX_SIZE), - video: this.formatBytes(FILE_SIZE_LIMITS.VIDEO_MAX_SIZE), - document: this.formatBytes(FILE_SIZE_LIMITS.DOCUMENT_MAX_SIZE), - audio: this.formatBytes(FILE_SIZE_LIMITS.AUDIO_MAX_SIZE), - archive: this.formatBytes(FILE_SIZE_LIMITS.ARCHIVE_MAX_SIZE), - default: this.formatBytes(FILE_SIZE_LIMITS.DEFAULT_MAX_SIZE), - }, - dimensionLimits: { - minWidth: 1, - minHeight: 1, - maxWidth: 16384, - maxHeight: 16384, - maxPixels: 100_000_000, - }, - }; - } - - /** - * Executes compress Preview. - * @param file The file to process. - * @returns The operation result. - */ - @Post('compress-preview') - @UseInterceptors(FileInterceptor('file')) - @ApiOperation({ summary: 'Preview image compression without saving' }) - @ApiConsumes('multipart/form-data') - @ApiResponse({ - status: 200, - description: 'Compression preview result', - schema: { - type: 'object', - properties: { - originalSize: { type: 'number' }, - compressedSize: { type: 'number' }, - compressionRatio: { type: 'number' }, - width: { type: 'number' }, - height: { type: 'number' }, - format: { type: 'string' }, - }, - }, - }) - @ApiResponse({ status: 400, description: 'Invalid file or not an image' }) - async compressPreview(@IUploadedFile() file: IFileUpload) { - if (!file) { - throw new BadRequestException('No file provided'); - } - - if (!file.mimetype.startsWith('image/')) { - throw new BadRequestException('File is not an image'); - } - - try { - const result = await this.imageProcessing.compressImage(file.buffer); - return { - originalSize: result.originalSize, - compressedSize: result.size, - compressionRatio: result.compressionRatio, - width: result.width, - height: result.height, - format: result.format, - }; - } catch (error) { - this.logger.error('Compression preview failed:', error); - throw new BadRequestException('Failed to compress image'); - } - } - - /** - * Returns content Url. - * @param contentId The content identifier. - * @param optimize The optimize. - * @param width The width. - * @param height The height. - * @param quality The quality. - * @param format The format. - * @param userLocation The user location. - * @param bandwidth The bandwidth. - * @returns The operation result. - */ - @Get('content/:contentId') - @ApiOperation({ summary: 'Get optimized content URL' }) - @ApiParam({ name: 'contentId', description: 'Content identifier' }) - @ApiQuery({ name: 'optimize', required: false, type: Boolean }) - @ApiQuery({ name: 'width', required: false, type: Number }) - @ApiQuery({ name: 'height', required: false, type: Number }) - @ApiQuery({ name: 'quality', required: false, type: Number }) - @ApiQuery({ name: 'format', required: false, enum: ['webp', 'jpeg', 'png'] }) - @ApiQuery({ name: 'userLocation', required: false, type: String }) - @ApiQuery({ name: 'bandwidth', required: false, type: Number }) - @ApiResponse({ status: 200, description: 'Content URL retrieved successfully' }) - @ApiResponse({ status: 404, description: 'Content not found' }) - async getContentUrl( - @Param('contentId') contentId: string, - @Query('optimize') optimize?: string, - @Query('width') width?: string, - @Query('height') height?: string, - @Query('quality') quality?: string, - @Query('format') format?: 'webp' | 'jpeg' | 'png', - @Query('userLocation') userLocation?: string, - @Query('bandwidth') bandwidth?: string, - ): Promise<{ url: string; metadata?: any }> { - try { - const options = { - optimize: optimize === 'true', - width: width ? parseInt(width) : undefined, - height: height ? parseInt(height) : undefined, - quality: quality ? parseInt(quality) : undefined, - format, - userLocation, - bandwidth: bandwidth ? parseFloat(bandwidth) : undefined, - }; - - const url = await this.cdnService.deliverContent(contentId, options); - - return { url }; - } catch (error) { - this.logger.error(`Failed to get content URL for ${contentId}:`, error); - if (error.message.includes('not found')) { - throw new HttpException('Content not found', HttpStatus.NOT_FOUND); - } - throw new HttpException('Failed to retrieve content', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - /** - * Invalidates content. - * @param contentId The content identifier. - * @returns The operation result. - */ - @Delete('content/:contentId') - @ApiOperation({ summary: 'Invalidate content cache' }) - @ApiParam({ name: 'contentId', description: 'Content identifier' }) - @ApiResponse({ status: 200, description: 'Content cache invalidated successfully' }) - @ApiResponse({ status: 404, description: 'Content not found' }) - async invalidateContent(@Param('contentId') contentId: string): Promise<{ success: boolean }> { - try { - await this.cdnService.invalidateContent(contentId); - this.logger.log(`Invalidated cache for content: ${contentId}`); - return { success: true }; - } catch (error) { - this.logger.error(`Failed to invalidate content ${contentId}:`, error); - throw new HttpException('Failed to invalidate content', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - /** - * Returns health. - * @returns The resulting promise<{ - status: string; - providers: record; - timestamp: string; - }>. - */ - @Get('health') - @ApiOperation({ summary: 'Check CDN health status' }) - @ApiResponse({ status: 200, description: 'CDN health status' }) - async getHealth(): Promise<{ - status: string; - providers: Record; - timestamp: string; - }> { - try { - // In a real implementation, check actual provider connectivity - const providers = { - cloudflare: true, // Mock health check - aws: true, - }; - - return { - status: 'healthy', - providers, - timestamp: new Date().toISOString(), - }; - } catch (_error) { - console.error('health check failed'); - throw new HttpException('Health check failed', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - /** - * Returns analytics. - * @param startDate The start date. - * @param endDate The end date. - * @returns The operation result. - */ - @Get('analytics') - @ApiOperation({ summary: 'Get CDN analytics' }) - @ApiQuery({ name: 'startDate', required: false }) - @ApiQuery({ name: 'endDate', required: false }) - @ApiResponse({ status: 200, description: 'CDN analytics data' }) - async getAnalytics( - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - ): Promise { - try { - const start = startDate - ? new Date(startDate) - : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); - const end = endDate ? new Date(endDate) : new Date(); - - // In a real implementation, aggregate analytics from providers - return { - totalRequests: 0, - totalBandwidth: 0, - cacheHitRate: 0, - topContent: [], - period: { - start: start.toISOString(), - end: end.toISOString(), - }, - }) - @ApiResponse({ status: 400, description: 'No file provided' }) - @ApiResponse({ status: 503, description: 'Scanning service not available' }) - async scanFile( - @UploadedFile() - file: FileUpload) { - if (!file) { - throw new BadRequestException('No file provided'); - } - if (!this.malwareScanning.isScanningAvailable()) { - throw new HttpException('Malware scanning service not available', HttpStatus.SERVICE_UNAVAILABLE); - } - this.logger.log(`Scanning file: ${file.originalname}`); - return this.malwareScanning.scanFile(file); - } - @Get('allowed-types') - @ApiOperation({ summary: 'Get allowed file types and size limits' }) - @ApiResponse({ - status: 200, - description: 'Allowed file types and limits', - schema: { - type: 'object', - properties: { - allowedTypes: { type: 'object' }, - sizeLimits: { type: 'object' }, - dimensionLimits: { type: 'object' }, - }, - }, - }) - getAllowedTypes() { - return { - allowedTypes: ALLOWED_FILE_TYPES, - sizeLimits: { - image: this.formatBytes(FILE_SIZE_LIMITS.IMAGE_MAX_SIZE), - video: this.formatBytes(FILE_SIZE_LIMITS.VIDEO_MAX_SIZE), - document: this.formatBytes(FILE_SIZE_LIMITS.DOCUMENT_MAX_SIZE), - audio: this.formatBytes(FILE_SIZE_LIMITS.AUDIO_MAX_SIZE), - archive: this.formatBytes(FILE_SIZE_LIMITS.ARCHIVE_MAX_SIZE), - default: this.formatBytes(FILE_SIZE_LIMITS.DEFAULT_MAX_SIZE), - }, - dimensionLimits: { - minWidth: 1, - minHeight: 1, - maxWidth: 16384, - maxHeight: 16384, - maxPixels: 100000000, - }, - }; - } - @Post('compress-preview') - @UseInterceptors(FileInterceptor('file')) - @ApiOperation({ summary: 'Preview image compression without saving' }) - @ApiConsumes('multipart/form-data') - @ApiResponse({ - status: 200, - description: 'Compression preview result', - schema: { - type: 'object', - properties: { - originalSize: { type: 'number' }, - compressedSize: { type: 'number' }, - compressionRatio: { type: 'number' }, - width: { type: 'number' }, - height: { type: 'number' }, - format: { type: 'string' }, - }, - }, - }) - @ApiResponse({ status: 400, description: 'Invalid file or not an image' }) - async compressPreview( - @UploadedFile() - file: FileUpload) { - if (!file) { - throw new BadRequestException('No file provided'); - } - if (!file.mimetype.startsWith('image/')) { - throw new BadRequestException('File is not an image'); - } - try { - const result = await this.imageProcessing.compressImage(file.buffer); - return { - originalSize: result.originalSize, - compressedSize: result.size, - compressionRatio: result.compressionRatio, - width: result.width, - height: result.height, - format: result.format, - }; - } - catch (error) { - this.logger.error('Compression preview failed:', error); - throw new BadRequestException('Failed to compress image'); - } - } - @Get('content/:contentId') - @ApiOperation({ summary: 'Get optimized content URL' }) - @ApiParam({ name: 'contentId', description: 'Content identifier' }) - @ApiQuery({ name: 'optimize', required: false, type: Boolean }) - @ApiQuery({ name: 'width', required: false, type: Number }) - @ApiQuery({ name: 'height', required: false, type: Number }) - @ApiQuery({ name: 'quality', required: false, type: Number }) - @ApiQuery({ name: 'format', required: false, enum: ['webp', 'jpeg', 'png'] }) - @ApiQuery({ name: 'userLocation', required: false, type: String }) - @ApiQuery({ name: 'bandwidth', required: false, type: Number }) - @ApiResponse({ status: 200, description: 'Content URL retrieved successfully' }) - @ApiResponse({ status: 404, description: 'Content not found' }) - async getContentUrl( - @Param('contentId') - contentId: string, - @Query('optimize') - optimize?: string, - @Query('width') - width?: string, - @Query('height') - height?: string, - @Query('quality') - quality?: string, - @Query('format') - format?: 'webp' | 'jpeg' | 'png', - @Query('userLocation') - userLocation?: string, - @Query('bandwidth') - bandwidth?: string): Promise<{ - url: string; - metadata?: unknown; - }> { - try { - const options = { - optimize: optimize === 'true', - width: width ? parseInt(width) : undefined, - height: height ? parseInt(height) : undefined, - quality: quality ? parseInt(quality) : undefined, - format, - userLocation, - bandwidth: bandwidth ? parseFloat(bandwidth) : undefined, - }; - const url = await this.cdnService.deliverContent(contentId, options); - return { url }; - } - catch (error) { - this.logger.error(`Failed to get content URL for ${contentId}:`, error); - if (error.message.includes('not found')) { - throw new HttpException('Content not found', HttpStatus.NOT_FOUND); - } - throw new HttpException('Failed to retrieve content', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - @Delete('content/:contentId') - @ApiOperation({ summary: 'Invalidate content cache' }) - @ApiParam({ name: 'contentId', description: 'Content identifier' }) - @ApiResponse({ status: 200, description: 'Content cache invalidated successfully' }) - @ApiResponse({ status: 404, description: 'Content not found' }) - async invalidateContent( - @Param('contentId') - contentId: string): Promise<{ - success: boolean; - }> { - try { - await this.cdnService.invalidateContent(contentId); - this.logger.log(`Invalidated cache for content: ${contentId}`); - return { success: true }; - } - catch (error) { - this.logger.error(`Failed to invalidate content ${contentId}:`, error); - throw new HttpException('Failed to invalidate content', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - @Get('health') - @ApiOperation({ summary: 'Check CDN health status' }) - @ApiResponse({ status: 200, description: 'CDN health status' }) - async getHealth(): Promise<{ - status: string; - providers: Record; - timestamp: string; - }> { - try { - // In a real implementation, check actual provider connectivity - const providers = { - cloudflare: true, // Mock health check - aws: true, - }; - return { - status: 'healthy', - providers, - timestamp: new Date().toISOString(), - }; - } - catch (_error) { - console.error('health check failed'); - throw new HttpException('Health check failed', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - @Get('analytics') - @ApiOperation({ summary: 'Get CDN analytics' }) - @ApiQuery({ name: 'startDate', required: false }) - @ApiQuery({ name: 'endDate', required: false }) - @ApiResponse({ status: 200, description: 'CDN analytics data' }) - async getAnalytics( - @Query('startDate') - startDate?: string, - @Query('endDate') - endDate?: string): Promise { - try { - const start = startDate - ? new Date(startDate) - : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); - const end = endDate ? new Date(endDate) : new Date(); - // In a real implementation, aggregate analytics from providers - return { - totalRequests: 0, - totalBandwidth: 0, - cacheHitRate: 0, - topContent: [], - period: { - start: start.toISOString(), - end: end.toISOString(), - }, - }; - } - catch (_error) { - console.error('failed to retrieve'); - throw new HttpException('Failed to retrieve analytics', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - /** - * Format bytes to human readable string - */ - private formatBytes(bytes: number): string { - if (bytes === 0) - return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; - } -} diff --git a/src/cdn/cdn.module.ts b/src/cdn/cdn.module.ts deleted file mode 100644 index d7e5a4a7..00000000 --- a/src/cdn/cdn.module.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { CacheModule } from '@nestjs/cache-manager'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { MulterModule } from '@nestjs/platform-express'; -import { memoryStorage } from 'multer'; -import { CdnService } from './cdn.service'; -import { CdnController } from './cdn.controller'; -import { AssetOptimizationService } from './optimization/asset-optimization.service'; -import { EdgeCachingService } from './caching/edge-caching.service'; -import { GeoLocationService } from './geo/geo-location.service'; -import { CloudflareService } from './providers/cloudflare.service'; -import { AWSCloudFrontService } from './providers/aws-cloudfront.service'; -import { ContentMetadata } from './entities/content-metadata.entity'; -import { FileValidationService } from '../media/validation/file-validation.service'; -import { MalwareScanningService } from '../media/validation/malware-scanning.service'; -import { ImageProcessingService } from '../media/processing/image-processing.service'; - -/** - * Registers the cdn module. - */ -@Module({ - imports: [ - ConfigModule, - CacheModule.register(), - TypeOrmModule.forFeature([ContentMetadata]), - MulterModule.register({ - storage: memoryStorage(), - limits: { - fileSize: 500 * 1024 * 1024, // 500MB limit (largest for videos) - }, - }), - ], - controllers: [CdnController], - providers: [ - CdnService, - AssetOptimizationService, - EdgeCachingService, - GeoLocationService, - CloudflareService, - AWSCloudFrontService, - FileValidationService, - MalwareScanningService, - ImageProcessingService, - ], - exports: [ - CdnService, - AssetOptimizationService, - EdgeCachingService, - GeoLocationService, - CloudflareService, - AWSCloudFrontService, - FileValidationService, - ImageProcessingService, - ], -}) -export class CdnModule { -} diff --git a/src/cdn/cdn.service.ts b/src/cdn/cdn.service.ts deleted file mode 100644 index 55902dc9..00000000 --- a/src/cdn/cdn.service.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { ConfigService } from '@nestjs/config'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Cache } from 'cache-manager'; -import { AssetOptimizationService } from './optimization/asset-optimization.service'; -import { EdgeCachingService } from './caching/edge-caching.service'; -import { GeoLocationService } from './geo/geo-location.service'; -import { CloudflareService } from './providers/cloudflare.service'; -import { AWSCloudFrontService } from './providers/aws-cloudfront.service'; -import { ContentMetadata, ContentType, ContentStatus } from './entities/content-metadata.entity'; -import { IUploadedFile } from '../common/types/file.types'; - -export interface IContentDeliveryOptions { - optimize?: boolean; - quality?: number; - format?: 'webp' | 'jpeg' | 'png'; - width?: number; - height?: number; - userLocation?: string; - bandwidth?: number; - responsive?: boolean; -} - -/** - * Provides cdn operations. - */ -@Injectable() -export class CdnService { - private readonly logger = new Logger(CdnService.name); - constructor( - @Inject(CACHE_MANAGER) - private cacheManager: Cache, - @InjectRepository(ContentMetadata) - private contentMetadataRepository: Repository, - private assetOptimizationService: AssetOptimizationService, - private edgeCachingService: EdgeCachingService, - private geoLocationService: GeoLocationService, - private cloudflareService: CloudflareService, - private awsCloudFrontService: AWSCloudFrontService, - private configService: ConfigService, - ) {} - - async deliverContent(contentId: string, options: IContentDeliveryOptions = {}): Promise { - const cacheKey = `cdn:${contentId}:${JSON.stringify(options)}`; - - // Check cache first - const cachedUrl = await this.cacheManager.get(cacheKey); - if (cachedUrl) { - return cachedUrl; - } - async invalidateContent(contentId: string): Promise { - // Purge from edge caches - await this.edgeCachingService.purgeContent(contentId); - // Clear local cache - simplified approach - // In a real implementation, you might need to track cache keys separately - // or use a cache store that supports key pattern deletion - this.logger.warn(`Cache invalidation for ${contentId} - manual cleanup may be required`); - } - - // Update access statistics - await this.updateAccessStats(metadata); - - // Determine optimal delivery strategy - const optimalLocation = await this.geoLocationService.getOptimalLocation(options.userLocation); - - // Optimize content if needed - let deliveryUrl = metadata.cdnUrl || metadata.originalUrl; - if (options.optimize && metadata.contentType === ContentType.IMAGE) { - deliveryUrl = await this.assetOptimizationService.optimizeImage(deliveryUrl, options); - } - - // Apply bandwidth optimization - if (options.bandwidth) { - deliveryUrl = await this.optimizeForBandwidth(deliveryUrl); - } - - // Get edge-cached URL - const edgeUrl = await this.edgeCachingService.getEdgeUrl(deliveryUrl, optimalLocation); - - // Cache the result - await this.cacheManager.set(cacheKey, edgeUrl, 3600000); // 1 hour - - return edgeUrl; - } - - /** - * Invalidates content. - * @param contentId The content identifier. - */ - async invalidateContent(contentId: string): Promise { - // Purge from edge caches - await this.edgeCachingService.purgeContent(contentId); - - // Clear local cache - simplified approach - // In a real implementation, you might need to track cache keys separately - // or use a cache store that supports key pattern deletion - this.logger.warn(`Cache invalidation for ${contentId} - manual cleanup may be required`); - } - - /** - * Uploads content. - * @param file The file to process. - * @param options The options. - * @returns The resulting content metadata. - */ - async uploadContent( - file: IUploadedFile, - options: IContentDeliveryOptions = {}, - ): Promise { - try { - // Upload to primary CDN provider with failover - const uploadResult = await this.uploadWithFailover(file); - - // Create metadata entity - const contentId = this.generateContentId(); - const metadata = this.contentMetadataRepository.create({ - contentId, - originalUrl: uploadResult.url, - cdnUrl: uploadResult.url, - contentType: this.mapContentType(file.mimetype), - fileName: file.originalname, - mimeType: file.mimetype, - fileSize: file.size, - status: ContentStatus.READY, - etag: uploadResult.etag, - provider: uploadResult.provider, - optimizationSettings: options.optimize - ? { - width: options.width, - height: options.height, - quality: options.quality, - format: options.format, - responsive: options.responsive, - } - return metadata; - } - catch (error) { - this.logger.error('Upload failed:', error); - throw error; - } - } - private async getContentMetadata(contentId: string): Promise { - return this.contentMetadataRepository.findOne({ - where: { contentId }, - }); - } - } - - private async getContentMetadata(contentId: string): Promise { - return this.contentMetadataRepository.findOne({ - where: { contentId }, - }); - } - - private async storeContentMetadata(metadata: ContentMetadata): Promise { - await this.contentMetadataRepository.save(metadata); - } - - private async updateAccessStats(metadata: ContentMetadata): Promise { - metadata.accessCount += 1; - metadata.lastAccessedAt = new Date(); - await this.contentMetadataRepository.save(metadata); - } - - private async uploadWithFailover(file: IUploadedFile): Promise<{ - url: string; - etag?: string; - provider: string; - }> { - const preferAws = this.isAwsCdnConfigured(); - const primary = preferAws ? 'aws' : 'cloudflare'; - - try { - if (primary === 'aws') { - const result = await this.awsCloudFrontService.uploadFile(file); - return { ...result, provider: 'aws-cloudfront' }; - } - - const result = await this.cloudflareService.uploadFile(file); - return { ...result, provider: 'cloudflare' }; - } catch (error) { - this.logger.warn('Primary provider failed, trying fallback:', error); - - try { - if (primary === 'aws') { - const result = await this.cloudflareService.uploadFile(file); - return { ...result, provider: 'cloudflare' }; - } - - const result = await this.awsCloudFrontService.uploadFile(file); - return { ...result, provider: 'aws-cloudfront' }; - } catch (fallbackError) { - this.logger.error('All providers failed:', fallbackError); - throw new Error('All CDN providers failed to upload file'); - } - } - } - - private isAwsCdnConfigured(): boolean { - const bucket = - this.configService.get('AWS_S3_BUCKET', '') || - this.configService.get('AWS_S3_BUCKET_NAME', ''); - const distributionId = this.configService.get('AWS_CLOUDFRONT_DISTRIBUTION_ID', ''); - return Boolean(bucket && distributionId); - } - - private async optimizeContentAsync( - metadata: ContentMetadata, - options: IContentDeliveryOptions, - ): Promise { - try { - metadata.status = ContentStatus.PROCESSING; - await this.contentMetadataRepository.save(metadata); - - await this.assetOptimizationService.optimizeImage(metadata.cdnUrl, options); - - // Generate responsive variants if requested - let variants = []; - if (options.responsive) { - variants = await this.assetOptimizationService.generateResponsiveImages(metadata.cdnUrl); - } - - metadata.status = ContentStatus.OPTIMIZED; - metadata.optimizedSize = variants.reduce( - (total, variant) => total + variant.optimizedSize, - 0, - ); - metadata.variants = variants.map((v) => ({ - name: v.url.split('/').pop(), - url: v.url, - width: options.width || 0, - height: options.height || 0, - size: v.optimizedSize, - })); - - await this.contentMetadataRepository.save(metadata); - } catch (error) { - metadata.status = ContentStatus.FAILED; - metadata.errorMessage = error.message; - await this.contentMetadataRepository.save(metadata); - throw error; - } - } - - private async optimizeForBandwidth(url: string): Promise { - // Implementation would adjust quality/format based on bandwidth - // For now, return original URL - return url; - } - - private isImageFile(file: IUploadedFile): boolean { - return file.mimetype.startsWith('image/'); - } - - private getContentType(file: IUploadedFile): 'image' | 'video' | 'document' { - if (file.mimetype.startsWith('image/')) return 'image'; - if (file.mimetype.startsWith('video/')) return 'video'; - return 'document'; - } - - private generateContentId(): string { - return `cdn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - } - - private mapContentType(mimeType: string): ContentType { - if (mimeType.startsWith('image/')) return ContentType.IMAGE; - if (mimeType.startsWith('video/')) return ContentType.VIDEO; - if (mimeType.startsWith('audio/')) return ContentType.AUDIO; - return ContentType.DOCUMENT; - } -} diff --git a/src/cdn/geo/geo-location.service.ts b/src/cdn/geo/geo-location.service.ts deleted file mode 100644 index decbdb43..00000000 --- a/src/cdn/geo/geo-location.service.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -export interface ILocationInfo { - country: string; - region: string; - city: string; - latitude: number; - longitude: number; - timezone: string; - isp?: string; - connectionType?: string; -} - -export interface IEdgeLocation { - id: string; - name: string; - country: string; - latitude: number; - longitude: number; - provider: string; - priority: number; -} - -/** - * Provides geo Location operations. - */ -@Injectable() -export class GeoLocationService { - private readonly logger = new Logger(GeoLocationService.name); - private edgeLocations: IEdgeLocation[] = [ - { - id: 'us-east-1', - name: 'Virginia', - country: 'US', - latitude: 39.0438, - longitude: -77.4874, - provider: 'cloudflare', - priority: 1, - }, - { - id: 'us-west-1', - name: 'California', - country: 'US', - latitude: 37.7749, - longitude: -122.4194, - provider: 'cloudflare', - priority: 2, - }, - { - id: 'eu-west-1', - name: 'Ireland', - country: 'IE', - latitude: 53.1424, - longitude: -7.6921, - provider: 'cloudflare', - priority: 1, - }, - { - id: 'eu-central-1', - name: 'Germany', - country: 'DE', - latitude: 50.1109, - longitude: 8.6821, - provider: 'cloudflare', - priority: 2, - }, - { - id: 'ap-southeast-1', - name: 'Singapore', - country: 'SG', - latitude: 1.3521, - longitude: 103.8198, - provider: 'cloudflare', - priority: 1, - }, - { - id: 'ap-northeast-1', - name: 'Japan', - country: 'JP', - latitude: 35.6762, - longitude: 139.6503, - provider: 'cloudflare', - priority: 2, - }, - ]; - - constructor(private configService: ConfigService) {} - - async getLocationInfo(ipAddress: string): Promise { - try { - // In real implementation, use a geolocation service like MaxMind or IP-API - // For now, return mock data based on IP - return this.mockGeolocation(ipAddress); - } catch (error) { - this.logger.error(`Failed to get location for IP ${ipAddress}:`, error); - return null; - } - } - - /** - * Retrieves optimal Location. - * @param userLocation The user location. - * @returns The resulting string value. - */ - async getOptimalLocation(userLocation?: string): Promise { - if (!userLocation) { - // Default to primary edge location - return this.edgeLocations[0].id; - } - async getNearestEdgeLocations(userLocation: string, limit: number = 3): Promise { - const userCoords = await this.getCoordinates(userLocation); - if (!userCoords) { - return this.edgeLocations.slice(0, limit); - } - const sortedLocations = this.edgeLocations - .map((location) => ({ - ...location, - distance: this.calculateDistance(userCoords.latitude, userCoords.longitude, location.latitude, location.longitude), - })) - .sort((a, b) => a.distance - b.distance); - return sortedLocations.slice(0, limit); - } - async optimizeRouteForConnection(userLocation: string, connectionType: string): Promise { - const locations = await this.getNearestEdgeLocations(userLocation, 5); - // Adjust based on connection type - switch (connectionType.toLowerCase()) { - case 'mobile': - case '3g': - case '4g': - // Prefer locations with better mobile optimization - return locations[0].id; - case 'satellite': - // Prefer locations with lower latency for satellite - return locations[0].id; - default: - return locations[0].id; - } - } - - return optimalLocation.id; - } - - async getNearestEdgeLocations(userLocation: string, limit: number = 3): Promise { - const userCoords = await this.getCoordinates(userLocation); - if (!userCoords) { - return this.edgeLocations.slice(0, limit); - } - - const sortedLocations = this.edgeLocations - .map((location) => ({ - ...location, - distance: this.calculateDistance( - userCoords.latitude, - userCoords.longitude, - location.latitude, - location.longitude, - ), - })) - .sort((a, b) => a.distance - b.distance); - - return sortedLocations.slice(0, limit); - } - - /** - * Optimizes route For Connection. - * @param userLocation The user location. - * @param connectionType The connection type. - * @returns The resulting string value. - */ - async optimizeRouteForConnection(userLocation: string, connectionType: string): Promise { - const locations = await this.getNearestEdgeLocations(userLocation, 5); - - // Adjust based on connection type - switch (connectionType.toLowerCase()) { - case 'mobile': - case '3g': - case '4g': - // Prefer locations with better mobile optimization - return locations[0].id; - case 'satellite': - // Prefer locations with lower latency for satellite - return locations[0].id; - default: - return locations[0].id; - } - } - - /** - * Retrieves latency Estimates. - * @param userLocation The user location. - * @returns The resulting record. - */ - async getLatencyEstimates(userLocation: string): Promise> { - const userCoords = await this.getCoordinates(userLocation); - const estimates: Record = {}; - - for (const location of this.edgeLocations) { - const distance = this.calculateDistance( - userCoords.latitude, - userCoords.longitude, - location.latitude, - location.longitude, - ); - - // Rough estimate: 1ms per 100km - estimates[location.id] = Math.round(distance / 100); - } - - return estimates; - } - - private async getCoordinates( - location: string, - ): Promise<{ latitude: number; longitude: number } | null> { - // In real implementation, use geocoding service - // For now, return mock coordinates - const mockCoords: Record = { - 'new york': { latitude: 40.7128, longitude: -74.006 }, - london: { latitude: 51.5074, longitude: -0.1278 }, - tokyo: { latitude: 35.6762, longitude: 139.6503 }, - sydney: { latitude: -33.8688, longitude: 151.2093 }, - lagos: { latitude: 6.5244, longitude: 3.3792 }, - }; - - return mockCoords[location.toLowerCase()] || null; - } - - private calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { - const R = 6371; // Earth's radius in kilometers - const dLat = this.toRadians(lat2 - lat1); - const dLon = this.toRadians(lon2 - lon1); - - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(this.toRadians(lat1)) * - Math.cos(this.toRadians(lat2)) * - Math.sin(dLon / 2) * - Math.sin(dLon / 2); - - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; - } - - private toRadians(degrees: number): number { - return degrees * (Math.PI / 180); - } - - private mockGeolocation(_ipAddress: string): ILocationInfo { - // Mock implementation - in real app, use actual geolocation service - return { - country: 'US', - region: 'CA', - city: 'San Francisco', - latitude: 37.7749, - longitude: -122.4194, - timezone: 'America/Los_Angeles', - isp: 'Mock ISP', - connectionType: 'fiber', - }; - } - - /** - * Retrieves geo Stats. - * @returns The resulting promise<{ - total requests: number; - top countries: array<{ country: string; count: number }>; - average latency: number; - }>. - */ - async getGeoStats(): Promise<{ - totalRequests: number; - topCountries: Array<{ country: string; count: number }>; - averageLatency: number; - }> { - // Implementation would aggregate geolocation analytics - return { - totalRequests: 100000, - topCountries: [ - { country: 'US', count: 45000 }, - { country: 'UK', count: 15000 }, - { country: 'DE', count: 12000 }, - ], - averageLatency: 45, - }; - } -} diff --git a/src/cdn/optimization/asset-optimization.service.ts b/src/cdn/optimization/asset-optimization.service.ts deleted file mode 100644 index 26832e0d..00000000 --- a/src/cdn/optimization/asset-optimization.service.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import sharp from 'sharp'; -import { IContentDeliveryOptions } from '../cdn.service'; - -export interface IOptimizationResult { - url: string; - originalSize: number; - optimizedSize: number; - format: string; -} - -/** - * Provides asset Optimization operations. - */ -@Injectable() -export class AssetOptimizationService { - /** - * Optimizes image. - * @param contentId The content identifier. - * @param _options The options. - * @returns The resulting string value. - */ - async optimizeImage(contentId: string, _options: any): Promise { - try { - // Download image (in real implementation, you'd fetch from storage) - // For now, assume we have the buffer - const buffer = await this.downloadImage(contentId); - - let sharpInstance = sharp(buffer); - - // Apply optimizations - if (_options.width || _options.height) { - sharpInstance = sharpInstance.resize({ - width: _options.width, - height: _options.height, - fit: 'cover', - withoutEnlargement: true, - }); - } - - if (_options.quality) { - sharpInstance = sharpInstance.jpeg({ quality: _options.quality }); - } - - if (_options.format) { - switch (_options.format) { - case 'webp': - sharpInstance = sharpInstance.webp({ quality: _options.quality || 80 }); - break; - case 'png': - sharpInstance = sharpInstance.png({ quality: _options.quality || 80 }); - break; - case 'jpeg': - default: - sharpInstance = sharpInstance.jpeg({ quality: _options.quality || 80 }); - break; - } - } - - const optimizedBuffer = await sharpInstance.toBuffer(); - const optimizedUrl = await this.uploadOptimizedImage(optimizedBuffer, contentId, _options); - - return optimizedUrl; - } catch (error) { - console.error('Image optimization failed:', error); - return contentId; // Return original if optimization fails - } - } - - /** - * Optimizes video. - * @param contentId The content identifier. - * @param _options The options. - * @returns The resulting string value. - */ - async optimizeVideo(contentId: string, _options: any): Promise { - // Implementation for video optimization using ffmpeg - // For now, return original - return contentId; - } - - async generateResponsiveImages(contentId: string): Promise { - const results: IOptimizationResult[] = []; - const sizes = [ - { width: 320, suffix: 'sm' }, - { width: 640, suffix: 'md' }, - { width: 1024, suffix: 'lg' }, - { width: 1920, suffix: 'xl' }, - ]; - - const buffer = await this.downloadImage(contentId); - - for (const size of sizes) { - const optimized = await sharp(buffer) - .resize(size.width, null, { withoutEnlargement: true }) - .webp({ quality: 80 }) - .toBuffer(); - - const url = await this.uploadOptimizedImage(optimized, contentId, { - width: size.width, - format: 'webp', - }); - - results.push({ - url, - originalSize: buffer.length, - optimizedSize: optimized.length, - format: 'webp', - }); - } - - return results; - } - - private async downloadImage(_url: string): Promise { - // In real implementation, download from storage/CDN - // For now, return empty buffer - throw new Error('Download implementation needed'); - } - - private async uploadOptimizedImage( - buffer: Buffer, - originalUrl: string, - options: IContentDeliveryOptions, - ): Promise { - // In real implementation, upload to storage and return new URL - // For now, return modified URL - const suffix = this.generateSuffix(options); - return originalUrl.replace(/(\.[^.]+)$/, `_${suffix}$1`); - } - - private generateSuffix(options: IContentDeliveryOptions): string { - const parts = []; - if (options.width) parts.push(`w${options.width}`); - if (options.height) parts.push(`h${options.height}`); - if (options.quality) parts.push(`q${options.quality}`); - if (options.format) parts.push(options.format); - return parts.join('_'); - } - - /** - * Retrieves optimization Stats. - * @param _contentId The content identifier. - * @returns The resulting promise<{ - original size: number; - optimized size: number; - savings percentage: number; - formats: string[]; - }>. - */ - async getOptimizationStats(_contentId: string): Promise<{ - originalSize: number; - optimizedSize: number; - format: string; -} -@Injectable() -export class AssetOptimizationService { - async optimizeImage(contentId: string, _options: unknown): Promise { - try { - // Download image (in real implementation, you'd fetch from storage) - // For now, assume we have the buffer - const buffer = await this.downloadImage(contentId); - let sharpInstance = sharp(buffer); - // Apply optimizations - if (_options.width || _options.height) { - sharpInstance = sharpInstance.resize({ - width: _options.width, - height: _options.height, - fit: 'cover', - withoutEnlargement: true, - }); - } - if (_options.quality) { - sharpInstance = sharpInstance.jpeg({ quality: _options.quality }); - } - if (_options.format) { - switch (_options.format) { - case 'webp': - sharpInstance = sharpInstance.webp({ quality: _options.quality || 80 }); - break; - case 'png': - sharpInstance = sharpInstance.png({ quality: _options.quality || 80 }); - break; - case 'jpeg': - default: - sharpInstance = sharpInstance.jpeg({ quality: _options.quality || 80 }); - break; - } - } - const optimizedBuffer = await sharpInstance.toBuffer(); - const optimizedUrl = await this.uploadOptimizedImage(optimizedBuffer, contentId, _options); - return optimizedUrl; - } - catch (error) { - console.error('Image optimization failed:', error); - return contentId; // Return original if optimization fails - } - } - async optimizeVideo(contentId: string, _options: unknown): Promise { - // Implementation for video optimization using ffmpeg - // For now, return original - return contentId; - } - async generateResponsiveImages(contentId: string): Promise { - const results: OptimizationResult[] = []; - const sizes = [ - { width: 320, suffix: 'sm' }, - { width: 640, suffix: 'md' }, - { width: 1024, suffix: 'lg' }, - { width: 1920, suffix: 'xl' }, - ]; - const buffer = await this.downloadImage(contentId); - for (const size of sizes) { - const optimized = await sharp(buffer) - .resize(size.width, null, { withoutEnlargement: true }) - .webp({ quality: 80 }) - .toBuffer(); - const url = await this.uploadOptimizedImage(optimized, contentId, { - width: size.width, - format: 'webp', - }); - results.push({ - url, - originalSize: buffer.length, - optimizedSize: optimized.length, - format: 'webp', - }); - } - return results; - } - private async downloadImage(_url: string): Promise { - // In real implementation, download from storage/CDN - // For now, return empty buffer - throw new Error('Download implementation needed'); - } - private async uploadOptimizedImage(buffer: Buffer, originalUrl: string, options: ContentDeliveryOptions): Promise { - // In real implementation, upload to storage and return new URL - // For now, return modified URL - const suffix = this.generateSuffix(options); - return originalUrl.replace(/(\.[^.]+)$/, `_${suffix}$1`); - } - private generateSuffix(options: ContentDeliveryOptions): string { - const parts = []; - if (options.width) - parts.push(`w${options.width}`); - if (options.height) - parts.push(`h${options.height}`); - if (options.quality) - parts.push(`q${options.quality}`); - if (options.format) - parts.push(options.format); - return parts.join('_'); - } - async getOptimizationStats(_contentId: string): Promise<{ - originalSize: number; - optimizedSize: number; - savingsPercentage: number; - formats: string[]; - }> { - // Implementation would fetch stats from database/cache - return { - originalSize: 2048000, - optimizedSize: 512000, - savingsPercentage: 75, - formats: ['webp', 'jpeg'], - }; - } -} diff --git a/src/cdn/providers/aws-cloudfront.service.ts b/src/cdn/providers/aws-cloudfront.service.ts deleted file mode 100644 index 9a9e6fd8..00000000 --- a/src/cdn/providers/aws-cloudfront.service.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { CloudFrontClient, CreateInvalidationCommand, GetInvalidationCommand, } from '@aws-sdk/client-cloudfront'; -import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; - -export interface IFileUpload { - originalname: string; - buffer: Buffer; - mimetype: string; - size: number; -} - -export interface IAWSCloudFrontConfig { - accessKeyId: string; - secretAccessKey: string; - region: string; - distributionId: string; - bucketName?: string; -} - -export interface IUploadResult { - id: string; - url: string; - etag?: string; - size: number; -} - -export interface IPurgeResult { - success: boolean; - purgedUrls: string[]; - failedUrls: string[]; - invalidationId?: string; -} - -/** - * Provides aWSCloud Front operations. - */ -@Injectable() -export class AWSCloudFrontService { - private readonly logger = new Logger(AWSCloudFrontService.name); - private readonly cloudfrontClient: CloudFrontClient; - private readonly s3Client: S3Client; - private readonly config: IAWSCloudFrontConfig; - - constructor(private configService: ConfigService) { - this.config = { - accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID', ''), - secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY', ''), - region: this.configService.get('AWS_REGION', 'us-east-1'), - distributionId: this.configService.get('AWS_CLOUDFRONT_DISTRIBUTION_ID', ''), - bucketName: - this.configService.get('AWS_S3_BUCKET_NAME') || - this.configService.get('AWS_S3_BUCKET'), - }; - - this.cloudfrontClient = new CloudFrontClient({ - region: this.config.region, - credentials: { - accessKeyId: this.config.accessKeyId, - secretAccessKey: this.config.secretAccessKey, - }, - }); - - this.s3Client = new S3Client({ - region: this.config.region, - credentials: { - accessKeyId: this.config.accessKeyId, - secretAccessKey: this.config.secretAccessKey, - }, - }); - } - - async uploadFile(file: IFileUpload): Promise { - try { - this.logger.log(`Uploading file ${file.originalname} to AWS CloudFront/S3`); - - if (!this.config.bucketName) { - throw new Error('S3 bucket name not configured'); - } - - if (!this.config.distributionId) { - throw new Error('CloudFront distribution ID not configured'); - } - - const key = `uploads/${Date.now()}_${file.originalname}`; - - const command = new PutObjectCommand({ - Bucket: this.config.bucketName, - Key: key, - Body: file.buffer, - ContentType: file.mimetype, - ACL: 'public-read', // Make it publicly accessible - }); - - const result = await this.s3Client.send(command); - - const url = `https://${this.config.distributionId}.cloudfront.net/${key}`; - - return { - id: key, - url, - etag: result.ETag, - size: file.size, - }; - } catch (error) { - this.logger.error('AWS CloudFront upload failed:', error); - throw new Error(`Failed to upload file to AWS CloudFront: ${error.message}`); - } - } - - async purgeUrls(urls: string[]): Promise { - try { - this.logger.log(`Creating CloudFront invalidation for ${urls.length} URLs`); - - // Convert full URLs to paths relative to distribution - const paths = urls.map((url) => { - try { - const urlObj = new URL(url); - return urlObj.pathname; - } catch { - // If not a full URL, assume it's already a path - return url.startsWith('/') ? url : `/${url}`; - } - }); - - const command = new CreateInvalidationCommand({ - DistributionId: this.config.distributionId, - InvalidationBatch: { - CallerReference: `cdn-purge-${Date.now()}`, - Paths: { - Quantity: paths.length, - Items: paths, - }, - }, - }); - - const result = await this.cloudfrontClient.send(command); - - // Wait for invalidation to complete - await this.waitForInvalidation(result.Invalidation?.Id || ''); - - return { - success: true, - purgedUrls: urls, - failedUrls: [], - invalidationId: result.Invalidation?.Id, - }; - } catch (error) { - this.logger.error('CloudFront invalidation failed:', error); - return { - success: false, - purgedUrls: [], - failedUrls: urls, - }; - } - } - - async purgeEverything(): Promise { - try { - this.logger.log('Creating CloudFront invalidation for all content'); - - const command = new CreateInvalidationCommand({ - DistributionId: this.config.distributionId, - InvalidationBatch: { - CallerReference: `cdn-purge-all-${Date.now()}`, - Paths: { - Quantity: 1, - Items: ['/*'], // Invalidate all paths - }, - }, - }); - - const result = await this.cloudfrontClient.send(command); - - await this.waitForInvalidation(result.Invalidation?.Id || ''); - - return { - success: true, - purgedUrls: ['/*'], - failedUrls: [], - invalidationId: result.Invalidation?.Id, - }; - } catch (error) { - this.logger.error('CloudFront purge everything failed:', error); - return { - success: false, - purgedUrls: [], - failedUrls: ['/*'], - }; - } - } - - /** - * Retrieves usage Statistics. - * @param _startDate The start date. - * @param _endDate The end date. - * @returns The operation result. - */ - async getUsageStatistics(_startDate: Date, _endDate: Date): Promise { - // AWS CloudFront doesn't have direct metrics API in SDK - // Would need to use CloudWatch or external monitoring - // For now, return mock data - return { - requests: 100000, - bytesDownloaded: 5000000000, - errorRate: 0.01, - topUrls: [ - { url: '/index.html', requests: 50000 }, - { url: '/main.js', requests: 30000 }, - ], - }; - } - - /** - * Updates distribution Settings. - * @param _settings The settings. - */ - async updateDistributionSettings(_settings: any): Promise { - try { - // Get current distribution config - // This would require additional API calls to get and update distribution - // For now, return success - this.logger.log('Updating CloudFront distribution settings'); - } catch (error) { - this.logger.error('Failed to update distribution settings:', error); - } - } - - /** - * Creates origin Access Identity. - * @returns The resulting string value. - */ - async createOriginAccessIdentity(): Promise { - // Implementation would create CloudFront Origin Access Identity - // for secure S3 access - const identityId = `origin-access-identity-${Date.now()}`; - this.logger.log(`Created Origin Access Identity: ${identityId}`); - return identityId; - } - - private async waitForInvalidation(invalidationId: string): Promise { - const maxAttempts = 30; // 5 minutes with 10s intervals - let attempts = 0; - - while (attempts < maxAttempts) { - try { - const command = new GetInvalidationCommand({ - DistributionId: this.config.distributionId, - Id: invalidationId, - }); - this.s3Client = new S3Client({ - region: this.config.region, - credentials: { - accessKeyId: this.config.accessKeyId, - secretAccessKey: this.config.secretAccessKey, - }, - }); - } - async uploadFile(file: FileUpload): Promise { - try { - this.logger.log(`Uploading file ${file.originalname} to AWS CloudFront/S3`); - if (!this.config.bucketName) { - throw new Error('S3 bucket name not configured'); - } - const key = `uploads/${Date.now()}_${file.originalname}`; - const command = new PutObjectCommand({ - Bucket: this.config.bucketName, - Key: key, - Body: file.buffer, - ContentType: file.mimetype, - ACL: 'public-read', // Make it publicly accessible - }); - const result = await this.s3Client.send(command); - const url = `https://${this.config.distributionId}.cloudfront.net/${key}`; - return { - id: key, - url, - etag: result.ETag, - size: file.size, - }; - } - catch (error) { - this.logger.error('AWS CloudFront upload failed:', error); - throw new Error(`Failed to upload file to AWS CloudFront: ${error.message}`); - } - } - - throw new Error(`Invalidation ${invalidationId} did not complete within timeout`); - } - - /** - * Removes file. - * @param key The key. - * @returns Whether the operation succeeded. - */ - async deleteFile(key: string): Promise { - try { - if (!this.config.bucketName) { - throw new Error('S3 bucket name not configured'); - } - - const command = new DeleteObjectCommand({ - Bucket: this.config.bucketName, - Key: key, - }); - - await this.s3Client.send(command); - - // Invalidate the deleted file - await this.purgeUrls([`https://${this.config.distributionId}.cloudfront.net/${key}`]); - - return true; - } catch (error) { - this.logger.error(`Failed to delete file ${key}:`, error); - return false; - } - } - - /** - * Retrieves file Metadata. - * @param _key The key. - * @returns The operation result. - */ - async getFileMetadata(_key: string): Promise { - // Implementation would get object metadata from S3 - return { - size: 0, - lastModified: new Date(), - contentType: 'application/octet-stream', - }; - } -} diff --git a/src/cdn/providers/cloudflare.service.ts b/src/cdn/providers/cloudflare.service.ts deleted file mode 100644 index b0163a44..00000000 --- a/src/cdn/providers/cloudflare.service.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import axios, { AxiosInstance } from 'axios'; -import { IUploadedFile } from '../../common/types/file.types'; - -export interface ICloudflareConfig { - apiToken: string; - accountId: string; - zoneId: string; - baseUrl?: string; -} - -export interface IUploadResult { - id: string; - url: string; - etag?: string; - size: number; -} - -export interface IPurgeResult { - success: boolean; - purgedUrls: string[]; - failedUrls: string[]; -} - -/** - * Provides cloudflare operations. - */ -@Injectable() -export class CloudflareService { - private readonly logger = new Logger(CloudflareService.name); - private readonly httpClient: AxiosInstance; - private readonly config: ICloudflareConfig; - - constructor(private configService: ConfigService) { - this.config = { - apiToken: this.configService.get('CLOUDFLARE_API_TOKEN', ''), - accountId: this.configService.get('CLOUDFLARE_ACCOUNT_ID', ''), - zoneId: this.configService.get('CLOUDFLARE_ZONE_ID', ''), - baseUrl: 'https://api.cloudflare.com/client/v4', - }; - - this.httpClient = axios.create({ - baseURL: this.config.baseUrl, - headers: { - Authorization: `Bearer ${this.config.apiToken}`, - 'Content-Type': 'application/json', - }, - }); - } - - async uploadFile(file: IUploadedFile): Promise { - try { - this.logger.log(`Uploading file ${file.originalname} to Cloudflare`); - - // For images, use Cloudflare Images API - if (file.mimetype.startsWith('image/')) { - return this.uploadImage(file); - } - - // For other files, use R2 or Stream - return this.uploadToR2(file); - } catch (error) { - this.logger.error('Cloudflare upload failed:', error); - throw new Error(`Failed to upload file to Cloudflare: ${error.message}`); - } - } - - async purgeUrls(urls: string[]): Promise { - try { - this.logger.log(`Purging ${urls.length} URLs from Cloudflare`); - - const response = await this.httpClient.post(`/zones/${this.config.zoneId}/purge_cache`, { - files: urls, - }); - - if (response.data.success) { - return { - success: true, - purgedUrls: urls, - failedUrls: [], - }; - this.httpClient = axios.create({ - baseURL: this.config.baseUrl, - headers: { - Authorization: `Bearer ${this.config.apiToken}`, - 'Content-Type': 'application/json', - }, - }); - } - async uploadFile(file: UploadedFile): Promise { - try { - this.logger.log(`Uploading file ${file.originalname} to Cloudflare`); - // For images, use Cloudflare Images API - if (file.mimetype.startsWith('image/')) { - return this.uploadImage(file); - } - // For other files, use R2 or Stream - return this.uploadToR2(file); - } - catch (error) { - this.logger.error('Cloudflare upload failed:', error); - throw new Error(`Failed to upload file to Cloudflare: ${error.message}`); - } - } - async purgeUrls(urls: string[]): Promise { - try { - this.logger.log(`Purging ${urls.length} URLs from Cloudflare`); - const response = await this.httpClient.post(`/zones/${this.config.zoneId}/purge_cache`, { - files: urls, - }); - if (response.data.success) { - return { - success: true, - purgedUrls: urls, - failedUrls: [], - }; - } - else { - this.logger.error('Cloudflare purge failed:', response.data.errors); - return { - success: false, - purgedUrls: [], - failedUrls: urls, - }; - } - } - catch (error) { - this.logger.error('Cloudflare purge error:', error); - return { - success: false, - purgedUrls: [], - failedUrls: urls, - }; - } - } - async purgeEverything(): Promise { - try { - const response = await this.httpClient.post(`/zones/${this.config.zoneId}/purge_cache`, { - purge_everything: true, - }); - return response.data.success; - } - catch (error) { - this.logger.error('Cloudflare purge everything failed:', error); - return false; - } - } - async getAnalytics(startDate: Date, endDate: Date): Promise { - try { - const response = await this.httpClient.get(`/zones/${this.config.zoneId}/analytics/dashboard`, { - params: { - since: startDate.toISOString(), - until: endDate.toISOString(), - }, - }); - return response.data.result; - } - catch (error) { - this.logger.error('Failed to get Cloudflare analytics:', error); - return null; - } - } - async createCustomDomain(domain: string): Promise { - try { - const response = await this.httpClient.post(`/zones/${this.config.zoneId}/custom_certificates`, { - certificate: '', // Would need actual certificate - private_key: '', // Would need actual private key - bundle_method: 'ubiquitous', - }); - return response.data.success; - } - catch (error) { - this.logger.error(`Failed to create custom domain ${domain}:`, error); - return false; - } - } - async getZoneSettings(): Promise { - try { - const response = await this.httpClient.get(`/zones/${this.config.zoneId}/settings`); - return response.data.result; - } - catch (error) { - this.logger.error('Failed to get zone settings:', error); - return null; - } - } - async updateCacheSettings(settings: unknown): Promise { - try { - const response = await this.httpClient.patch(`/zones/${this.config.zoneId}/settings/cache_level`, { - value: settings.cacheLevel || 'aggressive', - }); - return response.data.success; - } - catch (error) { - this.logger.error('Failed to update cache settings:', error); - return false; - } - } - private async uploadImage(file: UploadedFile): Promise { - // Use Cloudflare Images API - // In real implementation, would use proper multipart/form-data - // For now, return mock result - const mockId = `cf_img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - return { - id: mockId, - url: `https://imagedelivery.net/${mockId}/${file.originalname}`, - size: file.size, - }; - } - } - - /** - * Executes purge Everything. - * @returns Whether the operation succeeded. - */ - async purgeEverything(): Promise { - try { - const response = await this.httpClient.post(`/zones/${this.config.zoneId}/purge_cache`, { - purge_everything: true, - }); - - return response.data.success; - } catch (error) { - this.logger.error('Cloudflare purge everything failed:', error); - return false; - } - } - - /** - * Retrieves analytics. - * @param startDate The start date. - * @param endDate The end date. - * @returns The operation result. - */ - async getAnalytics(startDate: Date, endDate: Date): Promise { - try { - const response = await this.httpClient.get( - `/zones/${this.config.zoneId}/analytics/dashboard`, - { - params: { - since: startDate.toISOString(), - until: endDate.toISOString(), - }, - }, - ); - - return response.data.result; - } catch (error) { - this.logger.error('Failed to get Cloudflare analytics:', error); - return null; - } - } - - /** - * Creates custom Domain. - * @param domain The domain. - * @returns Whether the operation succeeded. - */ - async createCustomDomain(domain: string): Promise { - try { - const response = await this.httpClient.post( - `/zones/${this.config.zoneId}/custom_certificates`, - { - certificate: '', // Would need actual certificate - private_key: '', // Would need actual private key - bundle_method: 'ubiquitous', - }, - ); - - return response.data.success; - } catch (error) { - this.logger.error(`Failed to create custom domain ${domain}:`, error); - return false; - } - } - - /** - * Retrieves zone Settings. - * @returns The operation result. - */ - async getZoneSettings(): Promise { - try { - const response = await this.httpClient.get(`/zones/${this.config.zoneId}/settings`); - - return response.data.result; - } catch (error) { - this.logger.error('Failed to get zone settings:', error); - return null; - } - } - - /** - * Updates cache Settings. - * @param settings The settings. - * @returns Whether the operation succeeded. - */ - async updateCacheSettings(settings: any): Promise { - try { - const response = await this.httpClient.patch( - `/zones/${this.config.zoneId}/settings/cache_level`, - { - value: settings.cacheLevel || 'aggressive', - }, - ); - - return response.data.success; - } catch (error) { - this.logger.error('Failed to update cache settings:', error); - return false; - } - } - - private async uploadImage(file: IUploadedFile): Promise { - // Use Cloudflare Images API - // In real implementation, would use proper multipart/form-data - // For now, return mock result - const mockId = `cf_img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - return { - id: mockId, - url: `https://imagedelivery.net/${mockId}/${file.originalname}`, - size: file.size, - }; - } - - private async uploadToR2(file: IUploadedFile): Promise { - // Use Cloudflare R2 for non-image files - // This would require R2 bucket configuration - // For now, return mock result - const mockId = `cf_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - return { - id: mockId, - url: `https://r2.example.com/${mockId}/${file.originalname}`, - size: file.size, - }; - } - - /** - * Retrieves image Variants. - * @param imageId The image identifier. - * @returns The matching results. - */ - async getImageVariants(imageId: string): Promise { - try { - const response = await this.httpClient.get( - `/accounts/${this.config.accountId}/images/v1/${imageId}/variants`, - ); - - return response.data.result?.variants || []; - } catch (error) { - this.logger.error(`Failed to get variants for image ${imageId}:`, error); - return []; - } - } - - /** - * Removes image. - * @param imageId The image identifier. - * @returns Whether the operation succeeded. - */ - async deleteImage(imageId: string): Promise { - try { - const response = await this.httpClient.delete( - `/accounts/${this.config.accountId}/images/v1/${imageId}`, - ); - - return response.data.success; - } catch (error) { - this.logger.error(`Failed to delete image ${imageId}:`, error); - return false; - } - } -} diff --git a/src/collaboration/collaboration.controller.ts b/src/collaboration/collaboration.controller.ts deleted file mode 100644 index 4390d183..00000000 --- a/src/collaboration/collaboration.controller.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - UseGuards, - Request, - ParseUUIDPipe, -} from '@nestjs/common'; -import { CollaborationService } from './collaboration.service'; -import { SharedDocumentService } from './documents/shared-document.service'; -import { WhiteboardService } from './whiteboard/whiteboard.service'; -import { VersionControlService } from './versioning/version-control.service'; -import { CollaborationPermissionsService, PermissionLevel, } from './permissions/collaboration-permissions.service'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { RolesGuard } from '../auth/guards/roles.guard'; -// import { IsString, IsNotEmpty } from 'class-validator'; - -/** - * Exposes collaboration endpoints. - */ -@Controller('collaboration') -@UseGuards(JwtAuthGuard, RolesGuard) -export class CollaborationController { - constructor(private readonly collaborationService: CollaborationService, private readonly sharedDocumentService: SharedDocumentService, private readonly whiteboardService: WhiteboardService, private readonly versionControlService: VersionControlService, private readonly permissionsService: CollaborationPermissionsService) { } - /** - * Initialize a new collaborative session - */ - @Post('session') - async createSession( - @Request() - req, - @Body() - body: { - sessionId: string; - resourceType: 'document' | 'whiteboard'; - }): Promise { - const { sessionId, resourceType } = body; - const userId = req.user.id; - const session = await this.collaborationService.initializeSession(sessionId, userId, resourceType); - return { - success: true, - sessionId, - resourceType, - session, - }; - } - /** - * Get collaborative document - */ - @Get('document/:id') - async getDocument( - @Param('id') - documentId: string, - @Request() - req): Promise { - const userId = req.user.id; - const hasPermission = await this.permissionsService.hasAccess(documentId, userId, PermissionLevel.READ); - if (!hasPermission) { - return { success: false, message: 'Insufficient permissions to access document' }; - } - const document = await this.sharedDocumentService.getDocument(documentId); - return { - success: true, - document, - }; - } - - const whiteboard = await this.whiteboardService.getWhiteboard(whiteboardId); - - return { - success: true, - whiteboard, - }; - } - - /** - * Update collaborative document - */ - @Put('document/:id') - async updateDocument( - @Param('id', ParseUUIDPipe) documentId: string, - @Request() req, - @Body() body: { operation: any }, - ): Promise { - const { operation } = body; - const userId = req.user.id; - const hasPermission = await this.permissionsService.hasAccess( - documentId, - userId, - PermissionLevel.WRITE, - ); - - if (!hasPermission) { - return { success: false, message: 'Insufficient permissions to modify document' }; - } - - const document = await this.sharedDocumentService.applyOperation(documentId, userId, operation); - - return { - success: true, - document, - }; - } - - /** - * Update collaborative whiteboard - */ - @Put('whiteboard/:id') - async updateWhiteboard( - @Param('id', ParseUUIDPipe) whiteboardId: string, - @Request() req, - @Body() body: { operation: any }, - ): Promise { - const { operation } = body; - const userId = req.user.id; - const hasPermission = await this.permissionsService.hasAccess( - whiteboardId, - userId, - PermissionLevel.WRITE, - ); - - if (!hasPermission) { - return { success: false, message: 'Insufficient permissions to modify whiteboard' }; - } - /** - * Update collaborative whiteboard - */ - @Put('whiteboard/:id') - async updateWhiteboard( - @Param('id') - whiteboardId: string, - @Request() - req, - @Body() - body: { - operation: unknown; - }): Promise { - const { operation } = body; - const userId = req.user.id; - const hasPermission = await this.permissionsService.hasAccess(whiteboardId, userId, PermissionLevel.WRITE); - if (!hasPermission) { - return { success: false, message: 'Insufficient permissions to modify whiteboard' }; - } - const whiteboard = await this.whiteboardService.applyOperation(whiteboardId, userId, operation); - return { - success: true, - whiteboard, - }; - } - /** - * Get document history - */ - @Get('document/:id/history') - async getDocumentHistory( - @Param('id') - documentId: string, - @Request() - req): Promise { - const userId = req.user.id; - const hasPermission = await this.permissionsService.hasAccess(documentId, userId, PermissionLevel.READ); - if (!hasPermission) { - return { success: false, message: 'Insufficient permissions to access history' }; - } - const history = await this.sharedDocumentService.getDocumentHistory(documentId); - return { - success: true, - history, - }; - } - /** - * Get whiteboard history - */ - @Get('whiteboard/:id/history') - async getWhiteboardHistory( - @Param('id') - whiteboardId: string, - @Request() - req): Promise { - const userId = req.user.id; - const hasPermission = await this.permissionsService.hasAccess(whiteboardId, userId, PermissionLevel.READ); - if (!hasPermission) { - return { success: false, message: 'Insufficient permissions to access history' }; - } - const history = await this.whiteboardService.getWhiteboardHistory(whiteboardId); - return { - success: true, - history, - }; - } - /** - * Get version history for a session - */ - @Get('version-history/:sessionId') - async getVersionHistory( - @Param('sessionId') - sessionId: string, - @Request() - req): Promise { - const userId = req.user.id; - const hasPermission = await this.permissionsService.hasAccess(sessionId, userId, PermissionLevel.READ); - if (!hasPermission) { - return { success: false, message: 'Insufficient permissions to access version history' }; - } - const history = await this.versionControlService.getVersionHistory(sessionId); - return { - success: true, - history, - }; - } - /** - * Get current version of a session - */ - @Get('version-current/:sessionId') - async getCurrentVersion( - @Param('sessionId') - sessionId: string, - @Request() - req): Promise { - const userId = req.user.id; - const hasPermission = await this.permissionsService.hasAccess(sessionId, userId, PermissionLevel.READ); - if (!hasPermission) { - return { success: false, message: 'Insufficient permissions to access current version' }; - } - const currentVersion = await this.versionControlService.getCurrentVersion(sessionId); - return { - success: true, - currentVersion, - }; - } - /** - * Grant permissions to a user - */ - @Post('permission/:resourceId/user/:userId') - async grantPermission( - @Param('resourceId') - resourceId: string, - @Param('userId') - userId: string, - @Request() - req, - @Body() - body: { - permission: PermissionLevel; - }): Promise { - const adminUserId = req.user.id; - const { permission } = body; - // Check if the requesting user has admin permissions - const isAdmin = await this.permissionsService.hasAccess(resourceId, adminUserId, PermissionLevel.ADMIN); - if (!isAdmin) { - return { success: false, message: 'Insufficient permissions to grant permissions' }; - } - const grantedPermission = await this.permissionsService.grantAccess(resourceId, userId, permission, adminUserId); - return { - success: true, - permission: grantedPermission, - }; - } - /** - * Revoke permissions from a user - */ - @Delete('permission/:resourceId/user/:userId') - async revokePermission( - @Param('resourceId') - resourceId: string, - @Param('userId') - userId: string, - @Request() - req): Promise { - const adminUserId = req.user.id; - // Check if the requesting user has admin permissions - const isAdmin = await this.permissionsService.hasAccess(resourceId, adminUserId, PermissionLevel.ADMIN); - if (!isAdmin) { - return { success: false, message: 'Insufficient permissions to revoke permissions' }; - } - const revoked = await this.permissionsService.revokeAccess(resourceId, userId); - return { - success: true, - revoked, - }; - } - /** - * Get users with access to a resource - */ - @Get('users/:resourceId') - async getUsersForResource( - @Param('resourceId') - resourceId: string, - @Request() - req): Promise { - const userId = req.user.id; - const hasPermission = await this.permissionsService.hasAccess(resourceId, userId, PermissionLevel.READ); - if (!hasPermission) { - return { success: false, message: 'Insufficient permissions to view resource users' }; - } - const users = await this.permissionsService.getUsersForResource(resourceId); - return { - success: true, - users, - }; - } -} diff --git a/src/collaboration/collaboration.module.ts b/src/collaboration/collaboration.module.ts deleted file mode 100644 index bd017fb3..00000000 --- a/src/collaboration/collaboration.module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SharedDocumentService } from './documents/shared-document.service'; -import { WhiteboardService } from './whiteboard/whiteboard.service'; -import { VersionControlService } from './versioning/version-control.service'; -import { CollaborationPermissionsService } from './permissions/collaboration-permissions.service'; -import { CollaborationService } from './collaboration.service'; -import { CollaborationGateway } from './gateway/collaboration.gateway'; -import { CollaborationController } from './collaboration.controller'; - -/** - * Registers the collaboration module. - */ -@Module({ - imports: [], - controllers: [CollaborationController], - providers: [ - CollaborationService, - SharedDocumentService, - WhiteboardService, - VersionControlService, - CollaborationPermissionsService, - CollaborationGateway, - ], - exports: [ - CollaborationService, - SharedDocumentService, - WhiteboardService, - VersionControlService, - CollaborationPermissionsService, - ], -}) -export class CollaborationModule { -} diff --git a/src/collaboration/collaboration.service.ts b/src/collaboration/collaboration.service.ts deleted file mode 100644 index db288e79..00000000 --- a/src/collaboration/collaboration.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { SharedDocumentService } from './documents/shared-document.service'; -import { WhiteboardService } from './whiteboard/whiteboard.service'; -import { VersionControlService } from './versioning/version-control.service'; -import { CollaborationPermissionsService } from './permissions/collaboration-permissions.service'; - -/** - * Provides collaboration operations. - */ -@Injectable() -export class CollaborationService { - constructor(private readonly sharedDocumentService: SharedDocumentService, private readonly whiteboardService: WhiteboardService, private readonly versionControlService: VersionControlService, private readonly permissionsService: CollaborationPermissionsService) { } - /** - * Initialize a new collaborative session - */ - async initializeSession(sessionId: string, userId: string, resourceType: 'document' | 'whiteboard'): Promise { - // Set up initial permissions and session tracking - await this.permissionsService.grantAccess(sessionId, userId); - if (resourceType === 'document') { - return await this.sharedDocumentService.initializeDocument(sessionId); - } - else if (resourceType === 'whiteboard') { - return await this.whiteboardService.initializeWhiteboard(sessionId); - } - throw new Error(`Unsupported resource type: ${resourceType}`); - } - /** - * Handle incoming collaborative changes - */ - async handleCollaborativeChange(sessionId: string, userId: string, operation: unknown, resourceType: 'document' | 'whiteboard'): Promise { - // Check permissions - const hasPermission = await this.permissionsService.hasAccess(sessionId, userId); - if (!hasPermission) { - throw new Error('User does not have permission to modify this resource'); - } - if (resourceType === 'document') { - return await this.sharedDocumentService.applyOperation(sessionId, userId, operation); - } - else if (resourceType === 'whiteboard') { - return await this.whiteboardService.applyOperation(sessionId, userId, operation); - } - throw new Error(`Unsupported resource type: ${resourceType}`); - } - /** - * Track changes for version control - */ - async trackChange(sessionId: string, userId: string, change: unknown): Promise { - return await this.versionControlService.recordChange(sessionId, userId, change); - } -} diff --git a/src/collaboration/documents/shared-document.service.ts b/src/collaboration/documents/shared-document.service.ts deleted file mode 100644 index 5c84a0a4..00000000 --- a/src/collaboration/documents/shared-document.service.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; - -export interface IDocumentOperation { - id: string; - type: 'insert' | 'delete' | 'update'; - position: number; - content?: string; - length?: number; - timestamp: number; - userId: string; -} - -export interface ICollaborativeDocument { - id: string; - content: string; - operations: IDocumentOperation[]; - collaborators: string[]; - createdAt: Date; - updatedAt: Date; -} - -/** - * Provides shared Document operations. - */ -@Injectable() -export class SharedDocumentService { - private readonly logger = Logger; - private documents: Map = new Map(); - - /** - * Initialize a new collaborative document - */ - async initializeDocument(documentId: string): Promise { - const document: ICollaborativeDocument = { - id: documentId, - content: '', - operations: [], - collaborators: [], - createdAt: new Date(), - updatedAt: new Date(), - }; - - this.documents.set(documentId, document); - this.logger.log(`Initialized document ${documentId}`); - - return document; - } - - /** - * Get a collaborative document - */ - async getDocument(documentId: string): Promise { - return this.documents.get(documentId) || null; - } - - /** - * Apply an operation to a document using operational transformation - */ - async applyOperation( - documentId: string, - userId: string, - operation: Omit, - ): Promise { - const document = this.documents.get(documentId); - if (!document) { - throw new Error(`Document ${documentId} not found`); - } - - // Add metadata to the operation - const opWithMetadata: IDocumentOperation = { - ...operation, - id: uuidv4(), - timestamp: Date.now(), - userId, - }; - - // Transform the operation against concurrent operations - const transformedOp = this.transformOperation(opWithMetadata, document.operations); - - // Apply the transformed operation to the document content - document.content = this.applyOperationToContent(document.content, transformedOp); - - // Add the operation to the document's operation history - document.operations.push(transformedOp); - document.updatedAt = new Date(); - - // Add user to collaborators if not already present - if (!document.collaborators.includes(userId)) { - document.collaborators.push(userId); - } - - this.logger.log(`Applied operation ${transformedOp.id} to document ${documentId}`); - - return document; - } - - /** - * Transform an operation against a list of concurrent operations - */ - private transformOperation( - operation: IDocumentOperation, - concurrentOperations: IDocumentOperation[], - ): IDocumentOperation { - let transformedOp = { ...operation }; - - for (const concurrentOp of concurrentOperations) { - // Only transform if operations affect overlapping positions - if (this.operationsOverlap(transformedOp, concurrentOp)) { - transformedOp = this.transformSingleOperation(transformedOp, concurrentOp); - } - } - - return transformedOp; - } - - /** - * Check if two operations overlap in their effect on document content - */ - private operationsOverlap(op1: IDocumentOperation, op2: IDocumentOperation): boolean { - // Operations don't overlap if one happens before the other ends - if (op1.type === 'insert' && op2.type === 'insert') { - // Two inserts at the same position need transformation - return op1.position === op2.position; - } - - if (op1.type === 'insert') { - // Insert and another operation overlap if insert position is within or adjacent to other operation - return ( - op2.position <= op1.position && - op1.position <= op2.position + (op2.length || op2.content?.length || 0) - ); - } - - if (op2.type === 'insert') { - // Same as above but reversed - return ( - op1.position <= op2.position && - op2.position <= op1.position + (op1.length || op1.content?.length || 0) - ); - } - - // Both are delete/update operations - overlap if ranges intersect - const op1End = op1.position + (op1.length || op1.content?.length || 0); - const op2End = op2.position + (op2.length || op2.content?.length || 0); - return !(op1.position >= op2End || op2.position >= op1End); - } - - /** - * Transform a single operation against a concurrent operation - */ - private transformSingleOperation( - operation: IDocumentOperation, - concurrentOp: IDocumentOperation, - ): IDocumentOperation { - const transformedOp = { ...operation }; - - // Adjust positions based on concurrent operations - if (concurrentOp.type === 'insert' && operation.position >= concurrentOp.position) { - // If concurrent operation inserted text before our operation, adjust position - transformedOp.position += concurrentOp.content ? concurrentOp.content.length : 0; - } else if (concurrentOp.type === 'delete') { - const concurrentEnd = concurrentOp.position + concurrentOp.length; - - if (operation.position >= concurrentEnd) { - // Operation is after the deleted range, adjust position - transformedOp.position -= concurrentOp.length; - } else if (operation.position > concurrentOp.position) { - // Operation starts within the deleted range, clamp to start of deletion - transformedOp.position = concurrentOp.position; - } - // If operation starts before the deletion, position stays the same - } - - return transformedOp; - } - - /** - * Apply an operation to document content - */ - private applyOperationToContent(content: string, operation: IDocumentOperation): string { - switch (operation.type) { - case 'insert': - if (operation.content !== undefined) { - return ( - content.slice(0, operation.position) + - operation.content + - content.slice(operation.position) - ); - } - // Add metadata to the operation - const opWithMetadata: DocumentOperation = { - ...operation, - id: uuidv4(), - timestamp: Date.now(), - userId, - }; - // Transform the operation against concurrent operations - const transformedOp = this.transformOperation(opWithMetadata, document.operations); - // Apply the transformed operation to the document content - document.content = this.applyOperationToContent(document.content, transformedOp); - // Add the operation to the document's operation history - document.operations.push(transformedOp); - document.updatedAt = new Date(); - // Add user to collaborators if not already present - if (!document.collaborators.includes(userId)) { - document.collaborators.push(userId); - } - this.logger.log(`Applied operation ${transformedOp.id} to document ${documentId}`); - return document; - } - /** - * Transform an operation against a list of concurrent operations - */ - private transformOperation(operation: DocumentOperation, concurrentOperations: DocumentOperation[]): DocumentOperation { - let transformedOp = { ...operation }; - for (const concurrentOp of concurrentOperations) { - // Only transform if operations affect overlapping positions - if (this.operationsOverlap(transformedOp, concurrentOp)) { - transformedOp = this.transformSingleOperation(transformedOp, concurrentOp); - } - } - return transformedOp; - } - } - - /** - * Resolve conflicts between simultaneous edits - */ - async resolveConflicts( - documentId: string, - operations: IDocumentOperation[], - ): Promise { - const document = this.documents.get(documentId); - if (!document) { - throw new Error(`Document ${documentId} not found`); - } - /** - * Transform a single operation against a concurrent operation - */ - private transformSingleOperation(operation: DocumentOperation, concurrentOp: DocumentOperation): DocumentOperation { - const transformedOp = { ...operation }; - // Adjust positions based on concurrent operations - if (concurrentOp.type === 'insert' && operation.position >= concurrentOp.position) { - // If concurrent operation inserted text before our operation, adjust position - transformedOp.position += concurrentOp.content ? concurrentOp.content.length : 0; - } - else if (concurrentOp.type === 'delete') { - const concurrentEnd = concurrentOp.position + concurrentOp.length; - if (operation.position >= concurrentEnd) { - // Operation is after the deleted range, adjust position - transformedOp.position -= concurrentOp.length; - } - else if (operation.position > concurrentOp.position) { - // Operation starts within the deleted range, clamp to start of deletion - transformedOp.position = concurrentOp.position; - } - // If operation starts before the deletion, position stays the same - } - return transformedOp; - } - - document.updatedAt = new Date(); - - return document; - } - - /** - * Get document history - */ - async getDocumentHistory(documentId: string): Promise { - const document = this.documents.get(documentId); - if (!document) { - throw new Error(`Document ${documentId} not found`); - } -} diff --git a/src/collaboration/gateway/collaboration.gateway.ts b/src/collaboration/gateway/collaboration.gateway.ts deleted file mode 100644 index df7c8cfc..00000000 --- a/src/collaboration/gateway/collaboration.gateway.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, MessageBody, ConnectedSocket, } from '@nestjs/websockets'; -import { COLLABORATION_EVENTS } from '../constants/collaboration-events.constants'; -import { Server, Socket } from 'socket.io'; -import { Logger, UseGuards } from '@nestjs/common'; -import { CollaborationService } from '../collaboration.service'; -import { WsThrottlerGuard } from '../../common/guards/ws-throttler.guard'; -import { SharedDocumentService } from '../documents/shared-document.service'; -import { WhiteboardService } from '../whiteboard/whiteboard.service'; -import { VersionControlService } from '../versioning/version-control.service'; -import { CollaborationPermissionsService, PermissionLevel, } from '../permissions/collaboration-permissions.service'; -import { wsManager } from '../../common/utils/websocket.utils'; - -export interface ICollaborativeOperation { - sessionId: string; - userId: string; - resourceType: 'document' | 'whiteboard'; - operation: any; - timestamp: number; -} -@WebSocketGateway({ - cors: { - origin: '*', - methods: ['GET', 'POST'], - credentials: true, - }, -}) -@UseGuards(WsThrottlerGuard) -export class CollaborationGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { - @WebSocketServer() - server: Server; - private logger: Logger = new Logger('CollaborationGateway'); - constructor(private readonly collaborationService: CollaborationService, private readonly sharedDocumentService: SharedDocumentService, private readonly whiteboardService: WhiteboardService, private readonly versionControlService: VersionControlService, private readonly permissionsService: CollaborationPermissionsService) { } - afterInit(_server: Server): void { - this.logger.log('Collaboration Gateway initialized'); - } - async handleConnection(_server: unknown, - @ConnectedSocket() - client: Socket): Promise { - if (wsManager.getTotalConnections() >= 5000) { - client.emit('error', { message: 'Server is at maximum capacity' }); - client.disconnect(true); - return; - } - this.logger.log(`Client connected: ${client.id}`); - // Optionally authenticate the user here based on token - // const token = client.handshake.auth.token; - // const user = await this.authService.validateToken(token); - } - } - - @SubscribeMessage(COLLABORATION_EVENTS.COLLABORATIVE_OPERATION) - async handleCollaborativeOperation( - @MessageBody() operation: ICollaborativeOperation, - @ConnectedSocket() client: Socket, - ): Promise { - const { sessionId, userId, resourceType, operation: opData } = operation; - - try { - // Validate permissions - const hasPermission = await this.permissionsService.hasAccess( - sessionId, - userId, - resourceType === 'document' ? PermissionLevel.WRITE : PermissionLevel.WRITE, - ); - - if (!hasPermission) { - client.emit('error', { message: 'Insufficient permissions to perform operation' }); - return; - } - - // Process the operation based on resource type - let result: any; - if (resourceType === 'document') { - result = await this.sharedDocumentService.applyOperation( - sessionId, - userId, - operation.operation, - ); - } else if (resourceType === 'whiteboard') { - result = await this.whiteboardService.applyOperation( - sessionId, - userId, - operation.operation, - ); - } - - // Record the change for version control - await this.collaborationService.trackChange(sessionId, userId, { - operation: operation.operation, - resourceType, - result, - }); - - // Broadcast the operation to all other clients in the session - client.to(sessionId).emit(COLLABORATION_EVENTS.OPERATION_APPLIED, { - operation: opData, - userId, - timestamp: Date.now(), - result, - }); - - this.logger.log(`Operation applied in session ${sessionId} by user ${userId}`); - } catch (error) { - this.logger.error(`Error applying operation: ${error.message}`); - client.emit('error', { message: error.message }); - } - @SubscribeMessage(COLLABORATION_EVENTS.JOIN_SESSION) - async handleJoinSession( - @MessageBody() - data: { sessionId: string; userId: string; resourceType: string; operations: any[] }, - @ConnectedSocket() client: Socket, - ): Promise { - const { sessionId, userId, resourceType } = data; - - try { - // Only admins/owners can resolve conflicts - const hasPermission = await this.permissionsService.hasAccess( - sessionId, - userId, - PermissionLevel.ADMIN, - ); - if (!hasPermission) { - client.emit('error', { message: 'Insufficient permissions to resolve conflicts' }); - return; - } - - let result: any; - if (resourceType === 'document') { - result = await this.sharedDocumentService.resolveConflicts(sessionId, data.operations); - } else if (resourceType === 'whiteboard') { - result = await this.whiteboardService.resolveConflicts(sessionId, data.operations); - } - - // Broadcast resolved state to all clients - this.server.to(sessionId).emit(COLLABORATION_EVENTS.CONFLICT_RESOLVED, { - sessionId, - resourceType, - resolvedState: result, - }); - - this.logger.log(`Conflict resolved in session ${sessionId}`); - } catch (error) { - this.logger.error(`Error resolving conflict: ${error.message}`); - client.emit('error', { message: error.message }); - } -} diff --git a/src/collaboration/permissions/collaboration-permissions.service.ts b/src/collaboration/permissions/collaboration-permissions.service.ts deleted file mode 100644 index 653eec3b..00000000 --- a/src/collaboration/permissions/collaboration-permissions.service.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -export enum PermissionLevel { - READ = 'read', - WRITE = 'write', - ADMIN = 'admin', - OWNER = 'owner' -} - -export interface IResourcePermission { - resourceId: string; - userId: string; - permission: PermissionLevel; - grantedAt: Date; - grantedBy: string; -} - -export interface ICollaborativeResource { - id: string; - type: 'document' | 'whiteboard' | 'project'; - ownerId: string; - permissions: IResourcePermission[]; - createdAt: Date; - updatedAt: Date; -} - -/** - * Provides collaboration Permissions operations. - */ -@Injectable() -export class CollaborationPermissionsService { - private readonly logger = Logger; - private resources: Map = new Map(); - private defaultPermission: PermissionLevel = PermissionLevel.READ; - - /** - * Grant permission to a user for a resource - */ - async grantAccess( - resourceId: string, - userId: string, - permission: PermissionLevel = PermissionLevel.WRITE, - grantedBy: string = 'system', - ): Promise { - let resource = this.resources.get(resourceId); - - if (!resource) { - resource = { - id: resourceId, - type: this.getResourceType(resourceId), - ownerId: userId, - permissions: [], - createdAt: new Date(), - updatedAt: new Date(), - }; - this.resources.set(resourceId, resource); - } - - // Check if user already has permission - const existingPermission = resource.permissions.find((p) => p.userId === userId); - - if (existingPermission) { - // Update existing permission - existingPermission.permission = permission; - existingPermission.grantedAt = new Date(); - existingPermission.grantedBy = grantedBy; - } else { - // Add new permission - const newPermission: IResourcePermission = { - resourceId, - userId, - permission, - grantedAt: new Date(), - grantedBy, - }; - resource.permissions.push(newPermission); - } - /** - * Check if a user has access to a resource - */ - async hasAccess(resourceId: string, userId: string, requiredPermission: PermissionLevel = PermissionLevel.READ): Promise { - const userPermission = this.getUserPermission(resourceId, userId); - if (!userPermission) { - return false; - } - return this.checkPermissionLevel(userPermission.permission, requiredPermission); - } - /** - * Get user's permission for a resource - */ - getUserPermission(resourceId: string, userId: string): ResourcePermission | undefined { - const resource = this.resources.get(resourceId); - if (!resource) { - return undefined; - } - return resource.permissions.find((p) => p.userId === userId); - } - /** - * Get all users with access to a resource - */ - async getUsersForResource(resourceId: string): Promise { - const resource = this.resources.get(resourceId); - if (!resource) { - return []; - } - return [...resource.permissions]; - } - - return this.checkPermissionLevel(userPermission.permission, requiredPermission); - } - - /** - * Get user's permission for a resource - */ - getUserPermission(resourceId: string, userId: string): IResourcePermission | undefined { - const resource = this.resources.get(resourceId); - if (!resource) { - return undefined; - } - - return resource.permissions.find((p) => p.userId === userId); - } - - /** - * Get all users with access to a resource - */ - async getUsersForResource(resourceId: string): Promise { - const resource = this.resources.get(resourceId); - if (!resource) { - return []; - } - - return [...resource.permissions]; - } - - /** - * Get all resources a user has access to - */ - async getResourcesForUser( - userId: string, - minPermission: PermissionLevel = PermissionLevel.READ, - ): Promise { - const userResources: ICollaborativeResource[] = []; - - for (const resource of this.resources.values()) { - const userPermission = resource.permissions.find((p) => p.userId === userId); - - if (userPermission && this.checkPermissionLevel(userPermission.permission, minPermission)) { - userResources.push({ ...resource }); - } - } - - return userResources; - } - - /** - * Update user's permission level - */ - async updatePermission( - resourceId: string, - userId: string, - newPermission: PermissionLevel, - updatedBy: string, - ): Promise { - const resource = this.resources.get(resourceId); - if (!resource) { - return null; - } - /** - * Check if one permission level meets or exceeds another - */ - private checkPermissionLevel(userPermission: PermissionLevel, requiredPermission: PermissionLevel): boolean { - const permissionLevels: PermissionLevel[] = [ - PermissionLevel.READ, - PermissionLevel.WRITE, - PermissionLevel.ADMIN, - PermissionLevel.OWNER, - ]; - const userIndex = permissionLevels.indexOf(userPermission); - const requiredIndex = permissionLevels.indexOf(requiredPermission); - return userIndex >= requiredIndex; - } - - userPermission.permission = newPermission; - userPermission.grantedAt = new Date(); - userPermission.grantedBy = updatedBy; - - resource.updatedAt = new Date(); - - this.logger.log( - `Updated permission to ${newPermission} for user ${userId} on resource ${resourceId}`, - ); - - return { ...userPermission }; - } - - /** - * Invite a user to collaborate on a resource - */ - async inviteUser( - resourceId: string, - inviterId: string, - inviteeId: string, - permission: PermissionLevel = PermissionLevel.WRITE, - ): Promise { - // Check if inviter has admin or owner permissions - const inviterPermission = this.getUserPermission(resourceId, inviterId); - - if ( - !inviterPermission || - !this.checkPermissionLevel(inviterPermission.permission, PermissionLevel.ADMIN) - ) { - throw new Error( - `User ${inviterId} does not have permission to invite users to resource ${resourceId}`, - ); - } - /** - * Set default permission level for new resources - */ - setDefaultPermission(permission: PermissionLevel): void { - this.defaultPermission = permission; - } - /** - * Get default permission level - */ - getDefaultPermission(): PermissionLevel { - return this.defaultPermission; - } -} diff --git a/src/collaboration/versioning/version-control.service.ts b/src/collaboration/versioning/version-control.service.ts deleted file mode 100644 index cdf87d9b..00000000 --- a/src/collaboration/versioning/version-control.service.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; - -export interface IChangeRecord { - id: string; - sessionId: string; - userId: string; - change: any; - operationType: string; - timestamp: number; - previousValue?: any; - newValue?: any; - versionNumber: number; -} - -export interface IVersionHistory { - sessionId: string; - versions: IChangeRecord[]; - currentVersion: number; - createdAt: Date; - updatedAt: Date; -} - -/** - * Provides version Control operations. - */ -@Injectable() -export class VersionControlService { - private readonly logger = Logger; - private histories: Map = new Map(); - - /** - * Record a change in the version history - */ - async recordChange(sessionId: string, userId: string, change: any): Promise { - let history = this.histories.get(sessionId); - - if (!history) { - history = { - sessionId, - versions: [], - currentVersion: 0, - createdAt: new Date(), - updatedAt: new Date(), - }; - this.histories.set(sessionId, history); - } - - const versionNumber = history.currentVersion + 1; - const changeRecord: IChangeRecord = { - id: uuidv4(), - sessionId, - userId, - change, - operationType: this.getOperationType(change), - timestamp: Date.now(), - previousValue: this.getPreviousValue(history), - newValue: this.getNewValue(change), - versionNumber, - }; - - history.versions.push(changeRecord); - history.currentVersion = versionNumber; - history.updatedAt = new Date(); - - this.logger.log( - `Recorded change ${changeRecord.id} for session ${sessionId}, version ${versionNumber}`, - ); - - return changeRecord; - } - - /** - * Get the version history for a session - */ - async getVersionHistory(sessionId: string): Promise { - const history = this.histories.get(sessionId); - if (!history) { - return []; - } - - return [...history.versions]; - } - - /** - * Get a specific version of a session - */ - async getVersion(sessionId: string, versionNumber: number): Promise { - const history = this.histories.get(sessionId); - if (!history) { - return null; - } - - const version = history.versions.find((v) => v.versionNumber === versionNumber); - return version || null; - } - - /** - * Get the current version of a session - */ - async getCurrentVersion(sessionId: string): Promise { - const history = this.histories.get(sessionId); - if (!history) { - return null; - } - /** - * Revert to a specific version - */ - async revertToVersion(sessionId: string, versionNumber: number): Promise { - const history = this.histories.get(sessionId); - if (!history) { - throw new Error(`No history found for session ${sessionId}`); - } - const versionIndex = history.versions.findIndex((v) => v.versionNumber === versionNumber); - if (versionIndex === -1) { - throw new Error(`Version ${versionNumber} not found for session ${sessionId}`); - } - // Keep only versions up to the specified version - history.versions = history.versions.slice(0, versionIndex + 1); - history.currentVersion = versionNumber; - history.updatedAt = new Date(); - this.logger.log(`Reverted session ${sessionId} to version ${versionNumber}`); - return [...history.versions]; - } - - return history.versions[history.versions.length - 1]; - } - - /** - * Revert to a specific version - */ - async revertToVersion(sessionId: string, versionNumber: number): Promise { - const history = this.histories.get(sessionId); - if (!history) { - throw new Error(`No history found for session ${sessionId}`); - } - /** - * Get change statistics - */ - async getChangeStatistics(sessionId: string): Promise<{ - totalChanges: number; - changesByUser: Map; - changesOverTime: Array<{ - date: Date; - count: number; - }>; - }> { - const history = this.histories.get(sessionId); - if (!history) { - return { - totalChanges: 0, - changesByUser: new Map(), - changesOverTime: [], - }; - } - const changesByUser = new Map(); - const changesOverTime: Array<{ - date: Date; - count: number; - }> = []; - for (const version of history.versions) { - // Count changes by user - const userCount = changesByUser.get(version.userId) || 0; - changesByUser.set(version.userId, userCount + 1); - // Group by day for time series - const _dateStr = new Date(version.timestamp).toDateString(); - const timeEntry = changesOverTime.find((entry) => entry.date.toDateString() === new Date(version.timestamp).toDateString()); - if (timeEntry) { - timeEntry.count++; - } - else { - changesOverTime.push({ - date: new Date(version.timestamp), - count: 1, - }); - } - } - return { - totalChanges: history.versions.length, - changesByUser, - changesOverTime, - }; - } - - // Keep only versions up to the specified version - history.versions = history.versions.slice(0, versionIndex + 1); - history.currentVersion = versionNumber; - history.updatedAt = new Date(); - - this.logger.log(`Reverted session ${sessionId} to version ${versionNumber}`); - - return [...history.versions]; - } - - /** - * Compare two versions - */ - async compareVersions( - sessionId: string, - version1: number, - version2: number, - ): Promise<{ - differences: any; - version1Data: IChangeRecord | null; - version2Data: IChangeRecord | null; - }> { - const v1 = await this.getVersion(sessionId, version1); - const v2 = await this.getVersion(sessionId, version2); - - const differences = this.calculateDifferences(v1?.change, v2?.change); - - return { - differences, - version1Data: v1, - version2Data: v2, - }; - } - - /** - * Get change statistics - */ - async getChangeStatistics(sessionId: string): Promise<{ - totalChanges: number; - changesByUser: Map; - changesOverTime: Array<{ date: Date; count: number }>; - }> { - const history = this.histories.get(sessionId); - if (!history) { - return { - totalChanges: 0, - changesByUser: new Map(), - changesOverTime: [], - }; - } - - const changesByUser = new Map(); - const changesOverTime: Array<{ date: Date; count: number }> = []; - - for (const version of history.versions) { - // Count changes by user - const userCount = changesByUser.get(version.userId) || 0; - changesByUser.set(version.userId, userCount + 1); - - // Group by day for time series - const timeEntry = changesOverTime.find( - (entry) => entry.date.toDateString() === new Date(version.timestamp).toDateString(), - ); - - if (timeEntry) { - timeEntry.count++; - } else { - changesOverTime.push({ - date: new Date(version.timestamp), - count: 1, - }); - } - } - /** - * Extract the new value from a change - */ - private getNewValue(change: unknown): unknown { - if (change.newValue !== undefined) { - return change.newValue; - } - if (change.data !== undefined) { - return change.data; - } - return change; - } - /** - * Calculate differences between two states - */ - private calculateDifferences(state1: unknown, state2: unknown): unknown { - // This is a simplified difference calculation - // A production implementation would use a more sophisticated diff algorithm - return { - state1, - state2, - hasChanges: JSON.stringify(state1) !== JSON.stringify(state2), - }; - } - return 'unknown'; - } - - /** - * Extract the previous value from history - */ - private getPreviousValue(history: IVersionHistory): any { - // In a real implementation, this would retrieve the previous state - // For now, we'll return the last recorded value if available - if (history.versions.length > 0) { - return history.versions[history.versions.length - 1].newValue; - } - return undefined; - } - - /** - * Extract the new value from a change - */ - private getNewValue(change: any): any { - if (change.newValue !== undefined) { - return change.newValue; - } - if (change.data !== undefined) { - return change.data; - } - return change; - } - - /** - * Calculate differences between two states - */ - private calculateDifferences(state1: any, state2: any): any { - // This is a simplified difference calculation - // A production implementation would use a more sophisticated diff algorithm - return { - state1, - state2, - hasChanges: JSON.stringify(state1) !== JSON.stringify(state2), - }; - } -} diff --git a/src/collaboration/whiteboard/whiteboard.service.ts b/src/collaboration/whiteboard/whiteboard.service.ts deleted file mode 100644 index 080199fc..00000000 --- a/src/collaboration/whiteboard/whiteboard.service.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; - -export interface IDrawingElement { - id: string; - type: 'line' | 'rectangle' | 'circle' | 'text' | 'freehand'; - x: number; - y: number; - width?: number; - height?: number; - points?: Array<{ x: number; y: number }>; // For freehand drawings - text?: string; // For text elements - color: string; - strokeWidth: number; - userId: string; - timestamp: number; -} - -export interface IWhiteboardOperation { - id: string; - type: 'addElement' | 'removeElement' | 'updateElement' | 'clearBoard'; - element?: IDrawingElement; - elementId?: string; - userId: string; - timestamp: number; -} - -export interface ICollaborativeWhiteboard { - id: string; - elements: IDrawingElement[]; - operations: IWhiteboardOperation[]; - collaborators: string[]; - createdAt: Date; - updatedAt: Date; -} - -/** - * Provides whiteboard operations. - */ -@Injectable() -export class WhiteboardService { - private readonly logger = Logger; - private whiteboards: Map = new Map(); - - /** - * Initialize a new collaborative whiteboard - */ - async initializeWhiteboard(whiteboardId: string): Promise { - const whiteboard: ICollaborativeWhiteboard = { - id: whiteboardId, - elements: [], - operations: [], - collaborators: [], - createdAt: new Date(), - updatedAt: new Date(), - }; - - this.whiteboards.set(whiteboardId, whiteboard); - this.logger.log(`Initialized whiteboard ${whiteboardId}`); - - return whiteboard; - } - - /** - * Get a collaborative whiteboard - */ - async getWhiteboard(whiteboardId: string): Promise { - return this.whiteboards.get(whiteboardId) || null; - } - - /** - * Apply an operation to a whiteboard - */ - async applyOperation( - whiteboardId: string, - userId: string, - operation: Omit, - ): Promise { - const whiteboard = this.whiteboards.get(whiteboardId); - if (!whiteboard) { - throw new Error(`Whiteboard ${whiteboardId} not found`); - } - - // Add metadata to the operation - const opWithMetadata: IWhiteboardOperation = { - ...operation, - id: uuidv4(), - timestamp: Date.now(), - userId, - }; - - // Apply the operation to the whiteboard - this.applyOperationToWhiteboard(whiteboard, opWithMetadata); - - // Add the operation to the whiteboard's operation history - whiteboard.operations.push(opWithMetadata); - whiteboard.updatedAt = new Date(); - - // Add user to collaborators if not already present - if (!whiteboard.collaborators.includes(userId)) { - whiteboard.collaborators.push(userId); - } - - this.logger.log(`Applied operation ${opWithMetadata.id} to whiteboard ${whiteboardId}`); - - return whiteboard; - } - - /** - * Apply an operation to the whiteboard state - */ - private applyOperationToWhiteboard( - whiteboard: ICollaborativeWhiteboard, - operation: IWhiteboardOperation, - ): void { - switch (operation.type) { - case 'addElement': - if (operation.element) { - whiteboard.elements.push({ ...operation.element }); - } - break; - - case 'removeElement': - if (operation.elementId) { - whiteboard.elements = whiteboard.elements.filter((el) => el.id !== operation.elementId); - } - break; - - case 'updateElement': - if (operation.elementId && operation.element) { - const index = whiteboard.elements.findIndex((el) => el.id === operation.elementId); - if (index !== -1) { - whiteboard.elements[index] = { ...operation.element }; - } - } - break; - - case 'clearBoard': - whiteboard.elements = []; - break; - } - } - - /** - * Transform an operation against concurrent operations - */ - private transformOperation( - operation: IWhiteboardOperation, - concurrentOperations: IWhiteboardOperation[], - ): IWhiteboardOperation { - // For whiteboard operations, transformation is simpler than text operations - // We mainly need to handle cases where elements are removed while others try to update them - let transformedOp = { ...operation }; - - for (const concurrentOp of concurrentOperations) { - // If a concurrent operation removes an element that this operation tries to update - if ( - concurrentOp.type === 'removeElement' && - operation.type === 'updateElement' && - concurrentOp.elementId === operation.elementId - ) { - // Convert the update to an add operation since the element was removed - transformedOp = { - ...transformedOp, - type: 'addElement', - elementId: undefined, - }; - this.whiteboards.set(whiteboardId, whiteboard); - this.logger.log(`Initialized whiteboard ${whiteboardId}`); - return whiteboard; - } - - return transformedOp; - } - - /** - * Resolve conflicts between simultaneous whiteboard edits - */ - async resolveConflicts( - whiteboardId: string, - operations: IWhiteboardOperation[], - ): Promise { - const whiteboard = this.whiteboards.get(whiteboardId); - if (!whiteboard) { - throw new Error(`Whiteboard ${whiteboardId} not found`); - } - /** - * Apply an operation to a whiteboard - */ - async applyOperation(whiteboardId: string, userId: string, operation: Omit): Promise { - const whiteboard = this.whiteboards.get(whiteboardId); - if (!whiteboard) { - throw new Error(`Whiteboard ${whiteboardId} not found`); - } - // Add metadata to the operation - const opWithMetadata: WhiteboardOperation = { - ...operation, - id: uuidv4(), - timestamp: Date.now(), - userId, - }; - // Apply the operation to the whiteboard - this.applyOperationToWhiteboard(whiteboard, opWithMetadata); - // Add the operation to the whiteboard's operation history - whiteboard.operations.push(opWithMetadata); - whiteboard.updatedAt = new Date(); - // Add user to collaborators if not already present - if (!whiteboard.collaborators.includes(userId)) { - whiteboard.collaborators.push(userId); - } - this.logger.log(`Applied operation ${opWithMetadata.id} to whiteboard ${whiteboardId}`); - return whiteboard; - } - - whiteboard.updatedAt = new Date(); - - return whiteboard; - } - - /** - * Get whiteboard history - */ - async getWhiteboardHistory(whiteboardId: string): Promise { - const whiteboard = this.whiteboards.get(whiteboardId); - if (!whiteboard) { - throw new Error(`Whiteboard ${whiteboardId} not found`); - } - - return [...whiteboard.operations]; - } - - /** - * Add a drawing element to the whiteboard - */ - async addElement( - whiteboardId: string, - element: Omit, - userId: string, - ): Promise { - const whiteboard = this.whiteboards.get(whiteboardId); - if (!whiteboard) { - throw new Error(`Whiteboard ${whiteboardId} not found`); - } - - const newElement: IDrawingElement = { - ...element, - id: uuidv4(), - timestamp: Date.now(), - userId, - }; - - whiteboard.elements.push(newElement); - whiteboard.updatedAt = new Date(); - - // Record the operation - const operation: IWhiteboardOperation = { - id: uuidv4(), - type: 'addElement', - element: newElement, - userId, - timestamp: Date.now(), - }; - whiteboard.operations.push(operation); - - return newElement; - } - - /** - * Remove a drawing element from the whiteboard - */ - async removeElement(whiteboardId: string, elementId: string, userId: string): Promise { - const whiteboard = this.whiteboards.get(whiteboardId); - if (!whiteboard) { - throw new Error(`Whiteboard ${whiteboardId} not found`); - } - /** - * Get whiteboard history - */ - async getWhiteboardHistory(whiteboardId: string): Promise { - const whiteboard = this.whiteboards.get(whiteboardId); - if (!whiteboard) { - throw new Error(`Whiteboard ${whiteboardId} not found`); - } - return [...whiteboard.operations]; - } - /** - * Add a drawing element to the whiteboard - */ - async addElement(whiteboardId: string, element: Omit, userId: string): Promise { - const whiteboard = this.whiteboards.get(whiteboardId); - if (!whiteboard) { - throw new Error(`Whiteboard ${whiteboardId} not found`); - } - const newElement: DrawingElement = { - ...element, - id: uuidv4(), - timestamp: Date.now(), - userId, - }; - whiteboard.elements.push(newElement); - whiteboard.updatedAt = new Date(); - // Record the operation - const operation: WhiteboardOperation = { - id: uuidv4(), - type: 'addElement', - element: newElement, - userId, - timestamp: Date.now(), - }; - whiteboard.operations.push(operation); - return newElement; - } - /** - * Remove a drawing element from the whiteboard - */ - async removeElement(whiteboardId: string, elementId: string, userId: string): Promise { - const whiteboard = this.whiteboards.get(whiteboardId); - if (!whiteboard) { - throw new Error(`Whiteboard ${whiteboardId} not found`); - } - const elementIndex = whiteboard.elements.findIndex((el) => el.id === elementId); - if (elementIndex === -1) { - return false; - } - whiteboard.elements.splice(elementIndex, 1); - whiteboard.updatedAt = new Date(); - // Record the operation - const operation: WhiteboardOperation = { - id: uuidv4(), - type: 'removeElement', - elementId, - userId, - timestamp: Date.now(), - }; - whiteboard.operations.push(operation); - return true; - } - - whiteboard.elements.splice(elementIndex, 1); - whiteboard.updatedAt = new Date(); - - // Record the operation - const operation: IWhiteboardOperation = { - id: uuidv4(), - type: 'removeElement', - elementId, - userId, - timestamp: Date.now(), - }; - whiteboard.operations.push(operation); - - return true; - } -} diff --git a/src/common/csrf/csrf.controller.ts b/src/common/csrf/csrf.controller.ts deleted file mode 100644 index f5ccdf7a..00000000 --- a/src/common/csrf/csrf.controller.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Controller, Get, Post, UseGuards, Req, Res, HttpStatus, HttpCode } from '@nestjs/common'; -import { Request, Response } from 'express'; -import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; -import { CsrfService } from './csrf.service'; - -/** - * Exposes csrf endpoints. - */ -@ApiTags('CSRF') -@Controller('csrf') -export class CsrfController { - constructor(private readonly csrfService: CsrfService) {} - - /** - * Returns csrf Token. - * @param req The req. - * @param res The res. - */ - @Get('token') - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: 'Get CSRF token' }) - @ApiResponse({ status: 200, description: 'CSRF token generated successfully' }) - getCsrfToken(@Req() req: Request, @Res() res: Response): void { - const sessionId = this.getSessionId(req); - const token = this.csrfService.generateToken(sessionId); - - res.setHeader('X-CSRF-Token', token); - res.json({ csrfToken: token }); - } - - /** - * Validates csrf Token. - * @param req The req. - * @returns The operation result. - */ - @Post('validate') - @UseGuards(JwtAuthGuard) - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Validate CSRF token' }) - @ApiResponse({ status: 200, description: 'CSRF token is valid' }) - @ApiResponse({ status: 400, description: 'Invalid CSRF token' }) - validateCsrfToken(@Req() req: Request): { valid: boolean } { - const sessionId = this.getSessionId(req); - const token = req.body?.csrfToken || req.headers['x-csrf-token']; - - const isValid = this.csrfService.validateToken(sessionId, token); - return { valid: isValid }; - } - - /** - * Invalidates csrf Token. - * @param req The req. - * @returns The operation result. - */ - @Post('invalidate') - @UseGuards(JwtAuthGuard) - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Invalidate CSRF token' }) - @ApiResponse({ status: 200, description: 'CSRF token invalidated successfully' }) - invalidateCsrfToken(@Req() req: Request): { message: string } { - const sessionId = this.getSessionId(req); - this.csrfService.invalidateToken(sessionId); - - return { message: 'CSRF token invalidated successfully' }; - } - - private getSessionId(req: Request): string { - if ((req as any).session?.id) { - return (req as any).session.id; - } -} diff --git a/src/common/csrf/csrf.module.ts b/src/common/csrf/csrf.module.ts deleted file mode 100644 index be535de5..00000000 --- a/src/common/csrf/csrf.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; -import { CsrfMiddleware } from '../middleware/csrf.middleware'; -import { CsrfService } from './csrf.service'; -import { CsrfController } from './csrf.controller'; - -/** - * Registers the csrf module. - */ -@Module({ - providers: [CsrfService], - controllers: [CsrfController], - exports: [CsrfService], -}) -export class CsrfModule { - /** - * Executes configure. - * @param consumer The consumer. - * @returns The operation result. - */ - configure(consumer: MiddlewareConsumer) { - consumer.apply(CsrfMiddleware).forRoutes({ path: '*', method: RequestMethod.ALL }); - } -} diff --git a/src/common/csrf/csrf.service.ts b/src/common/csrf/csrf.service.ts deleted file mode 100644 index 52e7b8f0..00000000 --- a/src/common/csrf/csrf.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { v4 as uuidv4 } from 'uuid'; - -/** - * Provides csrf operations. - */ -@Injectable() -export class CsrfService { - private readonly csrfTokens = new Map(); - private readonly tokenExpiryTime: number; - - constructor(private configService: ConfigService) { - this.tokenExpiryTime = this.configService.get('CSRF_TOKEN_EXPIRY', 3600000); // 1 hour default - } - - /** - * Generates token. - * @param sessionId The session identifier. - * @returns The resulting string value. - */ - generateToken(sessionId: string): string { - const token = uuidv4(); - const expires = Date.now() + this.tokenExpiryTime; - - this.csrfTokens.set(sessionId, { token, expires }); - return token; - } - - /** - * Validates token. - * @param sessionId The session identifier. - * @param token The token value. - * @returns Whether the operation succeeded. - */ - validateToken(sessionId: string, token: string): boolean { - const storedToken = this.csrfTokens.get(sessionId); - - if (!storedToken || storedToken.expires <= Date.now()) { - return false; - } - - return storedToken.token === token; - } - - /** - * Invalidates token. - * @param sessionId The session identifier. - */ - invalidateToken(sessionId: string): void { - this.csrfTokens.delete(sessionId); - } - - /** - * Retrieves token. - * @param sessionId The session identifier. - * @returns The operation result. - */ - getToken(sessionId: string): string | null { - const storedToken = this.csrfTokens.get(sessionId); - - if (!storedToken || storedToken.expires <= Date.now()) { - return null; - } - - return storedToken.token; - } - - /** - * Executes cleanup Expired Tokens. - */ - cleanupExpiredTokens(): void { - const now = Date.now(); - for (const [sessionId, tokenData] of this.csrfTokens.entries()) { - if (tokenData.expires <= now) { - this.csrfTokens.delete(sessionId); - } - getToken(sessionId: string): string | null { - const storedToken = this.csrfTokens.get(sessionId); - if (!storedToken || storedToken.expires <= Date.now()) { - return null; - } - return storedToken.token; - } - cleanupExpiredTokens(): void { - const now = Date.now(); - for (const [sessionId, tokenData] of this.csrfTokens.entries()) { - if (tokenData.expires <= now) { - this.csrfTokens.delete(sessionId); - } - } - } -} diff --git a/src/common/database/database.module.ts b/src/common/database/database.module.ts deleted file mode 100644 index 98484ce4..00000000 --- a/src/common/database/database.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { TransactionService } from './transaction.service'; -import { TransactionalInterceptor } from './transactional.interceptor'; -import { DatabasePoolModule } from '../../database/database-pool.module'; -import { ShardingModule } from './sharding/sharding.module'; - -/** - * Database Module - * Provides transaction management services globally - */ -@Global() -@Module({ - imports: [DatabasePoolModule, ShardingModule], - providers: [TransactionService, TransactionalInterceptor], - exports: [TransactionService, TransactionalInterceptor, DatabasePoolModule, ShardingModule], -}) -export class DatabaseModule { -} diff --git a/src/common/database/examples/booking-transaction.example.ts b/src/common/database/examples/booking-transaction.example.ts deleted file mode 100644 index af8e12bc..00000000 --- a/src/common/database/examples/booking-transaction.example.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { TransactionService } from '../transaction.service'; -/** - * Example: Booking Transaction - * Demonstrates atomic booking operations (consulting sessions, courses, etc.) - */ -@Injectable() -export class BookingTransactionExample { - private readonly logger = new Logger(BookingTransactionExample.name); - - constructor(private readonly transactionService: TransactionService) {} - - /** - * Book a consulting session - * Ensures slot is reserved, payment is processed, and notification is sent atomically - */ - async bookConsultingSession( - userId: string, - consultantId: string, - slotId: string, - amount: number, - ): Promise { - return this.transactionService.runInTransaction(async (manager) => { - // 1. Check and lock the slot - const slot = await manager.query( - 'SELECT * FROM consulting_slots WHERE id = $1 AND status = $2 FOR UPDATE', - [slotId, 'available'], - ); - - if (!slot || slot.length === 0) { - throw new Error('Slot not available'); - } - - // 2. Check user balance - const user = await manager.query('SELECT balance FROM users WHERE id = $1 FOR UPDATE', [ - userId, - ]); - - if (!user || user.length === 0 || user[0].balance < amount) { - throw new Error('Insufficient balance'); - } - - // 3. Deduct payment from user - await manager.query('UPDATE users SET balance = balance - $1 WHERE id = $2', [ - amount, - userId, - ]); - - // 4. Add payment to consultant - await manager.query('UPDATE users SET balance = balance + $1 WHERE id = $2', [ - amount, - consultantId, - ]); - - // 5. Mark slot as booked - await manager.query( - 'UPDATE consulting_slots SET status = $1, booked_by = $2, booked_at = NOW() WHERE id = $3', - ['booked', userId, slotId], - ); - - // 6. Create booking record - const booking = await manager.query( - 'INSERT INTO bookings (user_id, consultant_id, slot_id, amount, status) VALUES ($1, $2, $3, $4, $5) RETURNING *', - [userId, consultantId, slotId, amount, 'confirmed'], - ); - - // 7. Create notification - await manager.query( - 'INSERT INTO notifications (user_id, type, message) VALUES ($1, $2, $3)', - [userId, 'booking_confirmed', `Your session with consultant ${consultantId} is confirmed`], - ); - - await manager.query( - 'INSERT INTO notifications (user_id, type, message) VALUES ($1, $2, $3)', - [consultantId, 'new_booking', `New booking from user ${userId}`], - ); - - this.logger.log(`Booking created: ${booking[0].id}`); - - return booking[0]; - }); - } - - /** - * Cancel booking with refund - */ - async cancelBooking(bookingId: string, refundAmount: number): Promise { - return this.transactionService.runInTransaction(async (manager) => { - // 1. Get booking details - const booking = await manager.query( - 'SELECT * FROM bookings WHERE id = $1 AND status = $2 FOR UPDATE', - [bookingId, 'confirmed'], - ); - - if (!booking || booking.length === 0) { - throw new Error('Booking not found or already cancelled'); - } - - const { user_id: userId, consultant_id: consultantId, slot_id: slotId } = booking[0]; - - // 2. Refund user - await manager.query('UPDATE users SET balance = balance + $1 WHERE id = $2', [ - refundAmount, - userId, - ]); - - // 3. Deduct from consultant - await manager.query('UPDATE users SET balance = balance - $1 WHERE id = $2', [ - refundAmount, - consultantId, - ]); - - // 4. Free up the slot - await manager.query( - 'UPDATE consulting_slots SET status = $1, booked_by = NULL, booked_at = NULL WHERE id = $2', - ['available', slotId], - ); - - // 5. Update booking status - await manager.query( - 'UPDATE bookings SET status = $1, cancelled_at = NOW(), refund_amount = $2 WHERE id = $3', - ['cancelled', refundAmount, bookingId], - ); - - // 6. Create notifications - await manager.query( - 'INSERT INTO notifications (user_id, type, message) VALUES ($1, $2, $3)', - [userId, 'booking_cancelled', `Your booking has been cancelled. Refund: $${refundAmount}`], - ); - - this.logger.log(`Booking cancelled: ${bookingId}`); - - return { success: true, bookingId, refundAmount }; - }); - } - - /** - * Reschedule booking - */ - async rescheduleBooking(bookingId: string, newSlotId: string): Promise { - return this.transactionService.runInTransaction(async (manager) => { - // 1. Get current booking - const booking = await manager.query( - 'SELECT * FROM bookings WHERE id = $1 AND status = $2 FOR UPDATE', - [bookingId, 'confirmed'], - ); - - if (!booking || booking.length === 0) { - throw new Error('Booking not found'); - } - - const { slot_id: oldSlotId, user_id: userId } = booking[0]; - - // 2. Check new slot availability - const newSlot = await manager.query( - 'SELECT * FROM consulting_slots WHERE id = $1 AND status = $2 FOR UPDATE', - [newSlotId, 'available'], - ); - - if (!newSlot || newSlot.length === 0) { - throw new Error('New slot not available'); - } - - // 3. Free old slot - await manager.query( - 'UPDATE consulting_slots SET status = $1, booked_by = NULL WHERE id = $2', - ['available', oldSlotId], - ); - - // 4. Book new slot - await manager.query( - 'UPDATE consulting_slots SET status = $1, booked_by = $2, booked_at = NOW() WHERE id = $3', - ['booked', userId, newSlotId], - ); - - // 5. Update booking - await manager.query( - 'UPDATE bookings SET slot_id = $1, rescheduled_at = NOW() WHERE id = $2', - [newSlotId, bookingId], - ); - - // 6. Create notification - await manager.query( - 'INSERT INTO notifications (user_id, type, message) VALUES ($1, $2, $3)', - [userId, 'booking_rescheduled', 'Your booking has been rescheduled'], - ); - - this.logger.log(`Booking rescheduled: ${bookingId}`); - - return { success: true, bookingId, newSlotId }; - }); - } -} diff --git a/src/common/database/examples/payment-transaction.example.ts b/src/common/database/examples/payment-transaction.example.ts deleted file mode 100644 index cbee5218..00000000 --- a/src/common/database/examples/payment-transaction.example.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { TransactionService } from '../transaction.service'; -// import { Transactional } from '../transactional.decorator'; -/** - * Example: Payment Transaction - * Demonstrates atomic payment processing with transaction management - */ -@Injectable() -export class PaymentTransactionExample { - private readonly logger = new Logger(PaymentTransactionExample.name); - - constructor( - private readonly transactionService: TransactionService, - // @InjectRepository(Payment) private paymentRepo: Repository, - // @InjectRepository(User) private userRepo: Repository, - // @InjectRepository(Transaction) private transactionRepo: Repository, - ) {} - - /** - * Process payment with transaction - * Ensures all steps succeed or all fail together - */ - async processPayment(userId: string, amount: number, recipientId: string): Promise { - return this.transactionService.runInTransaction(async (manager) => { - // 1. Deduct from sender - const sender = await manager.query( - 'UPDATE users SET balance = balance - $1 WHERE id = $2 AND balance >= $1 RETURNING *', - [amount, userId], - ); - - if (!sender || sender.length === 0) { - throw new Error('Insufficient balance'); - } - - // 2. Add to recipient - await manager.query('UPDATE users SET balance = balance + $1 WHERE id = $2', [ - amount, - recipientId, - ]); - - // 3. Create payment record - const payment = await manager.query( - 'INSERT INTO payments (user_id, recipient_id, amount, status) VALUES ($1, $2, $3, $4) RETURNING *', - [userId, recipientId, amount, 'completed'], - ); - - // 4. Create transaction log - await manager.query( - 'INSERT INTO transaction_logs (payment_id, type, amount) VALUES ($1, $2, $3)', - [payment[0].id, 'payment', amount], - ); - - this.logger.log(`Payment processed: ${amount} from ${userId} to ${recipientId}`); - - return payment[0]; - }); - } - - /** - * Process payment with retry on deadlock - */ - async processPaymentWithRetry(userId: string, amount: number, recipientId: string): Promise { - return this.transactionService.runWithRetry( - async (manager) => { - return this.processPaymentLogic(manager, userId, amount, recipientId); - }, - 3, // max retries - 1000, // initial delay - ); - } - - /** - * Process payment with serializable isolation - * Prevents concurrent modifications - */ - async processPaymentSerializable( - userId: string, - amount: number, - recipientId: string, - ): Promise { - return this.transactionService.runWithIsolationLevel('SERIALIZABLE', async (manager) => { - return this.processPaymentLogic(manager, userId, amount, recipientId); - }); - } - - /** - * Refund payment (rollback scenario) - */ - async refundPayment(paymentId: string): Promise { - return this.transactionService.runInTransaction(async (manager) => { - // 1. Get payment details - const payment = await manager.query('SELECT * FROM payments WHERE id = $1 AND status = $2', [ - paymentId, - 'completed', - ]); - - if (!payment || payment.length === 0) { - throw new Error('Payment not found or already refunded'); - } - - const { user_id: userId, recipient_id: recipientId, amount } = payment[0]; - - // 2. Reverse the payment - await manager.query('UPDATE users SET balance = balance + $1 WHERE id = $2', [ - amount, - userId, - ]); - - await manager.query('UPDATE users SET balance = balance - $1 WHERE id = $2', [ - amount, - recipientId, - ]); - - // 3. Update payment status - await manager.query('UPDATE payments SET status = $1, refunded_at = NOW() WHERE id = $2', [ - 'refunded', - paymentId, - ]); - - // 4. Create refund log - await manager.query( - 'INSERT INTO transaction_logs (payment_id, type, amount) VALUES ($1, $2, $3)', - [paymentId, 'refund', amount], - ); - - this.logger.log(`Payment refunded: ${paymentId}`); - - return { success: true, paymentId }; - }); - } - - /** - * Helper method for payment logic - */ - private async processPaymentLogic( - manager: any, - userId: string, - amount: number, - recipientId: string, - ): Promise { - const sender = await manager.query( - 'UPDATE users SET balance = balance - $1 WHERE id = $2 AND balance >= $1 RETURNING *', - [amount, userId], - ); - - if (!sender || sender.length === 0) { - throw new Error('Insufficient balance'); - } -} diff --git a/src/common/database/examples/voting-transaction.example.ts b/src/common/database/examples/voting-transaction.example.ts deleted file mode 100644 index 92065033..00000000 --- a/src/common/database/examples/voting-transaction.example.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { TransactionService } from '../transaction.service'; -/** - * Example: DAO Voting Transaction - * Demonstrates atomic voting operations with proper locking - */ -@Injectable() -export class VotingTransactionExample { - private readonly logger = new Logger(VotingTransactionExample.name); - - constructor(private readonly transactionService: TransactionService) {} - - /** - * Cast a vote in DAO proposal - * Ensures vote is recorded, counts are updated, and user can't double-vote - */ - async castVote( - userId: string, - proposalId: string, - voteType: 'for' | 'against' | 'abstain', - votingPower: number, - ): Promise { - return this.transactionService.runWithIsolationLevel( - 'SERIALIZABLE', // Prevent concurrent voting issues - async (manager) => { - // 1. Check if user already voted - const existingVote = await manager.query( - 'SELECT * FROM votes WHERE user_id = $1 AND proposal_id = $2', - [userId, proposalId], - ); - - if (existingVote && existingVote.length > 0) { - throw new Error('User has already voted on this proposal'); - } - - // 2. Check if proposal is still active - const proposal = await manager.query( - 'SELECT * FROM proposals WHERE id = $1 AND status = $2 FOR UPDATE', - [proposalId, 'active'], - ); - - if (!proposal || proposal.length === 0) { - throw new Error('Proposal not found or not active'); - } - - // 3. Verify user has voting power - const user = await manager.query('SELECT voting_power FROM users WHERE id = $1', [userId]); - - if (!user || user.length === 0 || user[0].voting_power < votingPower) { - throw new Error('Insufficient voting power'); - } - - // 4. Record the vote - const vote = await manager.query( - 'INSERT INTO votes (user_id, proposal_id, vote_type, voting_power) VALUES ($1, $2, $3, $4) RETURNING *', - [userId, proposalId, voteType, votingPower], - ); - - // 5. Update proposal vote counts - const updateField = - voteType === 'for' - ? 'votes_for' - : voteType === 'against' - ? 'votes_against' - : 'votes_abstain'; - await manager.query( - `UPDATE proposals SET ${updateField} = ${updateField} + $1, total_votes = total_votes + $1 WHERE id = $2`, - [votingPower, proposalId], - ); - - // 6. Check if proposal reached quorum - const updatedProposal = await manager.query('SELECT * FROM proposals WHERE id = $1', [ - proposalId, - ]); - - const { total_votes: totalVotes, quorum_required: quorumRequired } = updatedProposal[0]; - - if (totalVotes >= quorumRequired) { - await manager.query('UPDATE proposals SET quorum_reached = true WHERE id = $1', [ - proposalId, - ]); - - this.logger.log(`Proposal ${proposalId} reached quorum`); - } - - // 7. Create activity log - await manager.query( - 'INSERT INTO activity_logs (user_id, action, entity_type, entity_id) VALUES ($1, $2, $3, $4)', - [userId, 'vote_cast', 'proposal', proposalId], - ); - - this.logger.log(`Vote cast: ${userId} voted ${voteType} on ${proposalId}`); - - return vote[0]; - }, - ); - } - - /** - * Change vote (if allowed) - */ - async changeVote( - userId: string, - proposalId: string, - newVoteType: 'for' | 'against' | 'abstain', - ): Promise { - return this.transactionService.runInTransaction(async (manager) => { - // 1. Get existing vote - const existingVote = await manager.query( - 'SELECT * FROM votes WHERE user_id = $1 AND proposal_id = $2 FOR UPDATE', - [userId, proposalId], - ); - - if (!existingVote || existingVote.length === 0) { - throw new Error('No existing vote found'); - } - - const { vote_type: oldVoteType, voting_power: votingPower } = existingVote[0]; - - if (oldVoteType === newVoteType) { - throw new Error('Vote type is the same'); - } - - // 2. Check if proposal allows vote changes - const proposal = await manager.query( - 'SELECT * FROM proposals WHERE id = $1 AND status = $2 AND allow_vote_change = true FOR UPDATE', - [proposalId, 'active'], - ); - - if (!proposal || proposal.length === 0) { - throw new Error('Proposal does not allow vote changes or is not active'); - } - - // 3. Update vote counts - remove old vote - const oldField = - oldVoteType === 'for' - ? 'votes_for' - : oldVoteType === 'against' - ? 'votes_against' - : 'votes_abstain'; - await manager.query(`UPDATE proposals SET ${oldField} = ${oldField} - $1 WHERE id = $2`, [ - votingPower, - proposalId, - ]); - - // 4. Update vote counts - add new vote - const newField = - newVoteType === 'for' - ? 'votes_for' - : newVoteType === 'against' - ? 'votes_against' - : 'votes_abstain'; - await manager.query(`UPDATE proposals SET ${newField} = ${newField} + $1 WHERE id = $2`, [ - votingPower, - proposalId, - ]); - - // 5. Update vote record - await manager.query( - 'UPDATE votes SET vote_type = $1, changed_at = NOW() WHERE user_id = $2 AND proposal_id = $3', - [newVoteType, userId, proposalId], - ); - - // 6. Log the change - await manager.query( - 'INSERT INTO activity_logs (user_id, action, entity_type, entity_id, metadata) VALUES ($1, $2, $3, $4, $5)', - [ - userId, - 'vote_changed', - 'proposal', - proposalId, - JSON.stringify({ from: oldVoteType, to: newVoteType }), - ], - ); - - this.logger.log( - `Vote changed: ${userId} changed from ${oldVoteType} to ${newVoteType} on ${proposalId}`, - ); - - return { success: true, oldVoteType, newVoteType }; - }); - } - - /** - * Execute proposal (after voting period ends) - */ - async executeProposal(proposalId: string): Promise { - return this.transactionService.runInTransaction(async (manager) => { - // 1. Get proposal with lock - const proposal = await manager.query( - 'SELECT * FROM proposals WHERE id = $1 AND status = $2 FOR UPDATE', - [proposalId, 'active'], - ); - - if (!proposal || proposal.length === 0) { - throw new Error('Proposal not found or not active'); - } - - const { - votes_for: votesFor, - votes_against: votesAgainst, - total_votes: totalVotes, - quorum_required: quorumRequired, - approval_threshold: approvalThreshold, - } = proposal[0]; - - // 2. Check if voting period ended - const now = new Date(); - const endTime = new Date(proposal[0].voting_end_time); - - if (now < endTime) { - throw new Error('Voting period has not ended'); - } - - // 3. Check quorum - if (totalVotes < quorumRequired) { - await manager.query('UPDATE proposals SET status = $1, executed_at = NOW() WHERE id = $2', [ - 'failed_quorum', - proposalId, - ]); - - return { success: false, reason: 'Quorum not reached' }; - } - - // 4. Check approval threshold - const approvalRate = (votesFor / totalVotes) * 100; - - if (approvalRate < approvalThreshold) { - await manager.query('UPDATE proposals SET status = $1, executed_at = NOW() WHERE id = $2', [ - 'rejected', - proposalId, - ]); - - return { success: false, reason: 'Approval threshold not met' }; - } - - // 5. Execute proposal actions - const actions = await manager.query( - 'SELECT * FROM proposal_actions WHERE proposal_id = $1 ORDER BY execution_order', - [proposalId], - ); - - for (const action of actions) { - // Execute each action (transfer funds, update settings, etc.) - await this.executeProposalAction(manager, action); - } - - // 6. Mark proposal as executed - await manager.query('UPDATE proposals SET status = $1, executed_at = NOW() WHERE id = $2', [ - 'executed', - proposalId, - ]); - - // 7. Log execution - await manager.query( - 'INSERT INTO activity_logs (action, entity_type, entity_id, metadata) VALUES ($1, $2, $3, $4)', - [ - 'proposal_executed', - 'proposal', - proposalId, - JSON.stringify({ - votes_for: votesFor, - votes_against: votesAgainst, - total_votes: totalVotes, - }), - ], - ); - - this.logger.log(`Proposal executed: ${proposalId}`); - - return { success: true, proposalId, status: 'executed' }; - }); - } - - /** - * Helper to execute individual proposal actions - */ - private async executeProposalAction(manager: any, action: any): Promise { - const { action_type: actionType, parameters } = action; - - switch (actionType) { - case 'transfer_funds': - await manager.query('UPDATE users SET balance = balance + $1 WHERE id = $2', [ - parameters.amount, - parameters.recipient, - ]); - break; - - case 'update_setting': - await manager.query('UPDATE settings SET value = $1 WHERE key = $2', [ - parameters.value, - parameters.key, - ]); - break; - - // Add more action types as needed - default: - this.logger.warn(`Unknown action type: ${actionType}`); - } -} diff --git a/src/common/database/sharding.spec.ts b/src/common/database/sharding.spec.ts deleted file mode 100644 index 8c283494..00000000 --- a/src/common/database/sharding.spec.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { Test } from '@nestjs/testing'; -import { ShardRouter } from './sharding/router/shard.router'; -import { ShardHash } from './sharding/hash/shard.hash'; -import { ShardDataSourceManager } from './sharding/datasource/shard-datasource.manager'; -import { CrossShardQueryCoordinator } from './sharding/coordinator/cross-shard-query-coordinator'; -import { ShardTransactionService } from './sharding/shard-transaction.service'; -import { ShardAwareRepository } from './sharding/repository/shard-aware-repository'; -import { ShardConfig, ShardingConfig } from './sharding/config/shard.config'; - -describe('ShardHash', () => { - let shardHash: ShardHash; - - beforeEach(() => { - const shards = ['shard_00', 'shard_01', 'shard_02']; - const weights = new Map([ - ['shard_00', 100], - ['shard_01', 100], - ['shard_02', 100], - ]); - shardHash = new ShardHash(shards, weights, 150); - }); - - it('should be defined', () => { - expect(shardHash).toBeDefined(); - }); - - it('should route key to same shard consistently', () => { - const key = 'test_key_123'; - const shard1 = shardHash.getShard(key); - const shard2 = shardHash.getShard(key); - expect(shard1).toBe(shard2); - }); - - it('should distribute keys across shards', () => { - const distribution = new Map(); - const testKeys = 1000; - - for (let i = 0; i < testKeys; i++) { - const key = `key_${i}`; - const shard = shardHash.getShard(key); - distribution.set(shard, (distribution.get(shard) || 0) + 1); - } - - expect(distribution.size).toBeGreaterThan(1); - const average = testKeys / distribution.size; - for (const count of distribution.values()) { - expect(count).toBeGreaterThan(average * 0.5); - expect(count).toBeLessThan(average * 1.5); - } - }); - - it('should get all shards', () => { - const shards = shardHash.getAllShards(); - expect(shards).toContain('shard_00'); - expect(shards).toContain('shard_01'); - expect(shards).toContain('shard_02'); - }); - - it('should get replica shards', () => { - const replicas = shardHash.getShards('test_key', 2); - expect(replicas.length).toBeLessThanOrEqual(2); - expect(replicas.length).toBeGreaterThan(0); - }); -}); - -describe('ShardRouter', () => { - let shardRouter: ShardRouter; - const mockConfig = { - strategy: 'key-based' as const, - keyField: 'tenantId', - shardKeyExtractor: 'extractShardKey', - hashAlgorithm: 'murmur3' as const, - virtualNodesPerShard: 150, - defaultShard: 'shard_00', - fallbackOnShardFailure: true, - maxCrossShardRetries: 2, - enableCache: false, - cacheTtl: 300, - shards: { - shard_00: { - id: 'shard_00', - name: 'Primary Shard', - host: 'localhost', - port: 5432, - database: 'teachlink_shard_00', - username: 'postgres', - password: 'postgres', - weight: 100, - type: 'master' as const, - readOnly: false, - status: 'active' as const, - maxConnections: 30, - minConnections: 5, - timeout: 5000, - retryAttempts: 3, - }, - shard_01: { - id: 'shard_01', - name: 'Shard 1', - host: 'localhost', - port: 5433, - database: 'teachlink_shard_01', - username: 'postgres', - password: 'postgres', - weight: 100, - type: 'master' as const, - readOnly: false, - status: 'active' as const, - maxConnections: 30, - minConnections: 5, - timeout: 5000, - retryAttempts: 3, - }, - }, - shardGroups: { - primary: { - id: 'primary', - name: 'Primary Group', - shards: ['shard_00', 'shard_01'], - strategy: 'hash' as const, - replication: false, - readFromReplicas: false, - replicaReadStrategy: 'round-robin' as const, - }, - }, - shardMappings: {}, - } as ShardingConfig; - - beforeEach(() => { - shardRouter = new ShardRouter(mockConfig); - }); - - it('should be defined', () => { - expect(shardRouter).toBeDefined(); - }); - - it('should route key to shard', () => { - const shardId = shardRouter.route('test_key'); - expect(shardId).toBeDefined(); - expect(['shard_00', 'shard_01']).toContain(shardId); - }); - - it('should route key consistently', () => { - const key = 'consistent_key'; - const shard1 = shardRouter.route(key); - const shard2 = shardRouter.route(key); - expect(shard1).toBe(shard2); - }); - - it('should get active shards', () => { - const activeShards = shardRouter.getActiveShards(); - expect(activeShards.length).toBeGreaterThan(0); - }); - - it('should check shard status', () => { - expect(shardRouter.isShardActive('shard_00')).toBe(true); - expect(shardRouter.isShardActive('nonexistent')).toBe(false); - }); - - it('should add and remove mapping', () => { - shardRouter.addMapping('custom_key', 'shard_00'); - const shardId = shardRouter.route('custom_key'); - expect(shardId).toBe('shard_00'); - }); - - it('should get shard config', () => { - const config = shardRouter.getShardConfig('shard_00'); - expect(config).toBeDefined(); - expect(config?.id).toBe('shard_00'); - }); -}); - -describe('ShardAwareRepository', () => { - let repository: TestShardRepository; - let mockShardRouter: any; - let mockDataSourceManager: any; - let mockQueryCoordinator: any; - - class TestShardRepository extends ShardAwareRepository { - constructor() { - super(mockShardRouter, mockDataSourceManager, mockQueryCoordinator, 'test_table'); - } - } - - beforeEach(() => { - mockShardRouter = { - route: jest.fn().mockReturnValue('shard_00'), - getActiveShards: jest.fn().mockReturnValue(['shard_00', 'shard_01']), - getAllShards: jest.fn().mockReturnValue(['shard_00', 'shard_01']), - isShardActive: jest.fn().mockReturnValue(true), - } as any; - - mockDataSourceManager = { - query: jest.fn().mockResolvedValue([]), - runOnShard: jest.fn().mockImplementation(async (shardId: string, fn: any) => fn({})), - getDataSource: jest.fn(), - getManager: jest.fn(), - createQueryRunner: jest.fn(), - getActiveShardIds: jest.fn().mockReturnValue(['shard_00', 'shard_01']), - isShardAvailable: jest.fn().mockReturnValue(true), - getShardHealth: jest.fn().mockResolvedValue({ available: true }), - destroy: jest.fn(), - } as any; - - mockQueryCoordinator = { - executeCrossShardQuery: jest.fn().mockResolvedValue([]), - executeCrossShardAggregation: jest.fn().mockResolvedValue({}), - executeCrossShardTransaction: jest.fn().mockResolvedValue({ success: true }), - getDistribution: jest.fn(), - getClusterHealth: jest.fn(), - } as any; - - repository = new TestShardRepository(); - }); - - it('should be defined', () => { - expect(repository).toBeDefined(); - }); - - it('should find one by shard key', async () => { - const mockResult = { id: '1', name: 'Test' }; - (mockDataSourceManager.query as jest.Mock).mockResolvedValue([mockResult]); - - const result = await repository.findOneByShardKey('test_key'); - expect(result).toEqual(mockResult); - }); - - it('should insert on shard', async () => { - const mockResult = { id: '1', name: 'Test' }; - (mockDataSourceManager.query as jest.Mock).mockResolvedValue([mockResult]); - - const result = await repository.insertOnShard('tenant_1', { name: 'Test' }); - expect(result).toEqual(mockResult); - }); -}); diff --git a/src/common/database/sharding/coordinator/cross-shard-query-coordinator.ts b/src/common/database/sharding/coordinator/cross-shard-query-coordinator.ts deleted file mode 100644 index b002ca13..00000000 --- a/src/common/database/sharding/coordinator/cross-shard-query-coordinator.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { ShardRouter } from '../router/shard.router'; -import { ShardDataSourceManager } from '../datasource/shard-datasource.manager'; -import { ShardAwareQueryRunner } from '../runner/shard-aware-query-runner'; -import { Logger } from '@nestjs/common'; - -/** - * Cross-Shard Query Coordinator - * Manages and coordinates queries across multiple database shards - */ -export class CrossShardQueryCoordinator { - private readonly logger = new Logger(CrossShardQueryCoordinator.name); - private shardResultsCache: Map = new Map(); - - constructor( - private shardRouter: ShardRouter, - private dataSourceManager: ShardDataSourceManager, - ) {} - - /** - * Execute a query that spans multiple shards - */ - async executeCrossShardQuery(options: { - query: string; - shardKey?: string; - shardGroup?: string; - allShards?: boolean; - aggregationStrategy?: 'merge' | 'union' | 'aggregate' | 'first'; - parameters?: any[]; - timeout?: number; - }): Promise { - const { - query, - shardKey, - shardGroup = 'primary', - allShards = false, - aggregationStrategy = 'merge', - parameters = [], - timeout = 30000, - } = options; - - // Determine which shards to query - let targetShards: string[]; - - if (shardKey && !allShards) { - // Route to specific shard(s) - targetShards = this.shardRouter.routeReplicas(shardKey, shardGroup); - } else { - // Query all active shards - targetShards = this.shardRouter.getActiveShards(shardGroup); - } - - if (targetShards.length === 0) { - throw new Error('No active shards available for query'); - } - - this.logger.debug(`Executing cross-shard query on ${targetShards.length} shards`); - - // Execute query on all target shards in parallel - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Query timeout exceeded')), timeout), - ); - - const queryPromises = targetShards.map((shardId) => - this.executeQueryOnShard(shardId, query, parameters), - ); - - let results: any[]; - - try { - const shardResults = await Promise.race([Promise.allSettled(queryPromises), timeoutPromise]); - - // Process results - const successfulResults: any[][] = []; - const errors: Error[] = []; - - for (let i = 0; i < shardResults.length; i++) { - const result = shardResults[i]; - const shardId = targetShards[i]; - - if (result.status === 'fulfilled') { - successfulResults.push(result.value); - this.logger.debug(`Query successful on shard ${shardId}: ${result.value.length} rows`); - } else { - errors.push(new Error(`Shard ${shardId}: ${result.reason.message}`)); - this.logger.warn(`Query failed on shard ${shardId}:`, result.reason); - } - } - - results = successfulResults.flat(); - - // Handle partial failures - if (errors.length > 0 && results.length === 0) { - throw new Error(`All shard queries failed: ${errors.map((e) => e.message).join(', ')}`); - } - - if (errors.length > 0) { - this.logger.warn( - `Partial failure: ${errors.length} shards failed, ${successfulResults.length} succeeded`, - ); - } - } catch (error) { - this.logger.error('Cross-shard query failed:', error); - throw error; - } - - // Apply aggregation strategy - return this.aggregateResults(results, aggregationStrategy); - } - - /** - * Execute a cross-shard aggregation query - */ - async executeCrossShardAggregation( - aggregationQueries: { - shardId?: string; - query: string; - parameters?: any[]; - mergeKey?: string; - }[], - ): Promise { - const results = new Map(); - - // Execute queries in parallel - const queryPromises = aggregationQueries.map(async (queryConfig) => { - const shardIds = queryConfig.shardId - ? [queryConfig.shardId] - : this.shardRouter.getActiveShards(); - - const shardResults = await Promise.all( - shardIds.map((shardId) => - this.executeQueryOnShard(shardId, queryConfig.query, queryConfig.parameters || []), - ), - ); - - const mergedResults = shardResults.flat(); - - if (queryConfig.mergeKey) { - results.set(queryConfig.mergeKey, mergedResults); - } - - return { key: queryConfig.mergeKey || 'result', data: mergedResults }; - }); - - const queryResults = await Promise.all(queryPromises); - - // Merge results into aggregation object - const aggregationResult: any = {}; - - for (const result of queryResults) { - aggregationResult[result.key] = result.data; - } - - return aggregationResult as T; - } - - /** - * Execute distributed transaction across shards - */ - async executeCrossShardTransaction( - operations: Array<{ - shardKey: string; - query: string; - parameters?: any[]; - }>, - ): Promise { - const shardGroups = new Map< - string, - Array<{ - query: string; - parameters?: any[]; - }> - >(); - - // Group operations by shard - for (const operation of operations) { - const shardId = this.shardRouter.route(operation.shardKey); - const shardOps = shardGroups.get(shardId) || []; - shardOps.push({ query: operation.query, parameters: operation.parameters }); - shardGroups.set(shardId, shardOps); - } - - const coordinator = new ShardAwareQueryRunner(this.dataSourceManager); - - try { - // Create query runners for all involved shards - await coordinator.createQueryRunners(Array.from(shardGroups.keys())); - - // Start transactions on all shards - await coordinator.startTransactions(); - - // Execute operations on each shard - for (const [shardId, ops] of shardGroups.entries()) { - for (const op of ops) { - await coordinator.query(shardId, op.query, op.parameters); - } - } - - // Commit all transactions - await coordinator.commitTransactions(); - - return { success: true } as T; - } catch (error) { - // Rollback on error - await coordinator.rollbackTransactions(); - throw error; - } finally { - await coordinator.releaseAll(); - } - } - - /** - * Execute query on a specific shard - */ - private async executeQueryOnShard( - shardId: string, - query: string, - parameters: any[], - ): Promise { - const dataSource = this.dataSourceManager.getDataSource(shardId); - if (!dataSource) { - throw new Error(`Shard ${shardId} not available`); - } - - try { - const result = await dataSource.query(query, parameters); - return result || []; - } catch (error) { - this.logger.error(`Query execution failed on shard ${shardId}:`, error); - throw new Error(`Shard ${shardId} query error: ${error.message}`); - } - } - - /** - * Aggregate results from multiple shards - */ - private aggregateResults(results: any[], strategy: string): T[] { - switch (strategy) { - case 'union': - return this.unionResults(results); - case 'aggregate': - return this.aggregateNumericResults(results); - case 'first': - return results.length > 0 ? results[0] : []; - case 'merge': - default: - return this.mergeResults(results); - } - } - - /** - * Merge results (remove duplicates based on id) - */ - private mergeResults(results: any[]): any[] { - const seen = new Set(); - return results.filter((item) => { - const id = item.id || item._id; - if (id && seen.has(id)) { - return false; - } - if (id) { - seen.add(id); - } - return true; - }); - } - - /** - * Union results (all records, including duplicates) - */ - private unionResults(results: any[]): any[] { - return results; - } - - /** - * Aggregate numeric results (sum, avg, count) - */ - private aggregateNumericResults(results: any[]): any[] { - if (results.length === 0) { - return []; - } - - const aggregated: any = {}; - - for (const result of results) { - for (const [key, value] of Object.entries(result)) { - if (typeof value === 'number') { - if (!aggregated[key]) { - aggregated[key] = 0; - } - aggregated[key] += value; - } else if (!aggregated[key]) { - aggregated[key] = value; - } - } - } - - return [aggregated]; - } - - /** - * Get shard distribution statistics - */ - getDistribution(): Map { - return this.shardRouter.getDistribution(); - } - - /** - * Get health status of all shards - */ - async getClusterHealth(): Promise<{ - totalShards: number; - activeShards: number; - inactiveShards: number; - shardHealth: Array<{ - shardId: string; - available: boolean; - latency?: number; - }>; - }> { - const shardIds = this.shardRouter.getAllShards(); - const shardHealth = await Promise.all( - shardIds.map(async (shardId) => ({ - shardId, - ...(await this.dataSourceManager.getShardHealth(shardId)), - })), - ); - - const activeShards = shardHealth.filter((s) => s.available).length; - const inactiveShards = shardHealth.filter((s) => !s.available).length; - - return { - totalShards: shardIds.length, - activeShards, - inactiveShards, - shardHealth, - }; - } -} diff --git a/src/common/database/sharding/decorators/shard-aware.decorator.ts b/src/common/database/sharding/decorators/shard-aware.decorator.ts deleted file mode 100644 index 4db0e3b0..00000000 --- a/src/common/database/sharding/decorators/shard-aware.decorator.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; -import { ShardRouter } from '../router/shard.router'; - -/** - * Extract shard key from request or context - */ -export function extractShardKey( - data: any, - context?: ExecutionContext, - defaultValue?: string, -): string { - if (typeof data === 'string') { - return data; - } - - if (typeof data === 'object' && data !== null) { - // Check common shard key fields - const possibleKeys = ['shardKey', 'tenantId', 'userId', 'id', 'businessId']; - for (const key of possibleKeys) { - if (data[key]) { - return String(data[key]); - } - } - } - - return defaultValue || 'default'; -} - -/** - * Param decorator for shard key injection - */ -export const ShardKey = createParamDecorator((data: unknown, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const shardKey = request.body?.shardKey || request.query?.shardKey || request.params?.shardKey; - - return shardKey || extractShardKey(request.body || request.params); -}); - -/** - * Shard-aware decorator - */ -export function ShardAware(options?: { shardGroup?: string; fallback?: boolean }) { - return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { - const originalMethod = descriptor.value; - - descriptor.value = async function (...args: any[]) { - const shardRouter: ShardRouter = this.shardRouter; - const shardGroup = options?.shardGroup || 'primary'; - - // Extract shard key from arguments - let shardKey = 'default'; - if (args.length > 0) { - shardKey = extractShardKey(args[0], undefined, 'default'); - } - - try { - // If method has a shardKey parameter, use it - const hasShardKey = - propertyKey.toString().includes('shardKey') || - propertyKey.toString().includes('Sharding'); - - if (hasShardKey && shardRouter) { - const targetShard = shardRouter.route(shardKey, shardGroup); - this.logger?.log?.(`Routing to shard: ${targetShard} for key: ${shardKey}`); - } - } catch (error) { - if (!options?.fallback) { - throw error; - } - } - - return originalMethod.apply(this, args); - }; - - return descriptor; - }; -} diff --git a/src/common/database/sharding/examples/cross-shard-sync.example.ts b/src/common/database/sharding/examples/cross-shard-sync.example.ts deleted file mode 100644 index 7646c4ef..00000000 --- a/src/common/database/sharding/examples/cross-shard-sync.example.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ShardTransactionService, ShardDataSourceManager } from '../index'; - -/** - * Example: Cross-Shard Data Synchronization - * Demonstrates how to perform operations across multiple shards - */ -@Injectable() -export class ShardSyncExample { - private readonly logger = new Logger(ShardSyncExample.name); - - constructor( - private shardTransactionService: ShardTransactionService, - private dataSourceManager: ShardDataSourceManager, - ) {} - - /** - * Synchronize user data across shards (e.g., broadcast admin users) - */ - async syncAdminUsersToAllShards(adminData: any): Promise { - const allShards = this.getAvailableShards(); - const operations = allShards.map((shardId) => ({ - shardId, - operation: async (manager: any) => { - return manager.query( - 'INSERT INTO users (id, username, email, role, is_admin) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO UPDATE SET role = $4, is_admin = $5', - [adminData.id, adminData.username, adminData.email, adminData.role, true], - ); - }, - })); - - return this.shardTransactionService.parallelShardOperations(operations); - } - - /** - * Aggregate statistics across all shards - */ - async getGlobalStatistics(): Promise<{ - totalUsers: number; - totalOrders: number; - totalRevenue: number; - }> { - const userResults = await this.shardTransactionService.crossShardQuery( - 'SELECT COUNT(*) as count FROM users', - { allShards: true, aggregationStrategy: 'aggregate' }, - ); - - const orderResults = await this.shardTransactionService.crossShardQuery( - 'SELECT COUNT(*) as count FROM orders', - { allShards: true, aggregationStrategy: 'aggregate' }, - ); - - const revenueResults = await this.shardTransactionService.crossShardQuery( - "SELECT COALESCE(SUM(amount), 0) as total FROM orders WHERE status = 'completed'", - { allShards: true, aggregationStrategy: 'aggregate' }, - ); - - return { - totalUsers: parseInt((userResults[0] as any)?.count || '0', 10), - totalOrders: parseInt((orderResults[0] as any)?.count || '0', 10), - totalRevenue: parseFloat((revenueResults[0] as any)?.total || '0'), - }; - } - - /** - * Perform cross-shard search - */ - async searchAcrossShards(keyword: string): Promise { - const searchQuery = ` - SELECT id, username, email, 'users' as type - FROM users - WHERE username ILIKE $1 OR email ILIKE $1 - UNION ALL - SELECT id, title as username, '' as email, 'courses' as type - FROM courses - WHERE title ILIKE $1 - `; - - return this.shardTransactionService.crossShardQuery(searchQuery, { - allShards: true, - parameters: [`%${keyword}%`], - aggregationStrategy: 'merge', - }); - } - - /** - * Replicate data to backup shard - */ - async replicateToBackup( - sourceShard: string, - targetShard: string, - tables: string[], - ): Promise { - for (const table of tables) { - const data = await this.dataSourceManager.query(sourceShard, `SELECT * FROM ${table}`); - - for (const record of data as any[]) { - const { id, ...insertData } = record; - const columns = Object.keys(insertData).join(', '); - const values = Object.values(insertData); - const placeholders = values.map((_: any, i: number) => `$${i + 1}`).join(', '); - - await this.dataSourceManager.query( - targetShard, - `INSERT INTO ${table} (${columns}) VALUES (${placeholders})`, - values, - ); - } - } - } - - /** - * Execute distributed transaction (e.g., transfer between tenants on different shards) - */ - async executeDistributedTransfer( - fromShardKey: string, - toShardKey: string, - amount: number, - ): Promise { - return this.shardTransactionService.runCrossShard([ - { - shardKey: fromShardKey, - query: 'UPDATE accounts SET balance = balance - $1 WHERE user_id = $2', - parameters: [amount, fromShardKey], - }, - { - shardKey: toShardKey, - query: 'UPDATE accounts SET balance = balance + $1 WHERE user_id = $2', - parameters: [amount, toShardKey], - }, - ]); - } - - /** - * Get shard health and distribution - */ - async getShardHealthReport(): Promise { - return this.shardTransactionService.crossShardQuery( - 'SELECT 1 as status, pg_database_size(current_database()) as size', - { - allShards: true, - aggregationStrategy: 'merge', - }, - ); - } - - private getAvailableShards(): string[] { - return this.dataSourceManager.getActiveShardIds(); - } -} diff --git a/src/common/database/sharding/examples/shard-payment.example.ts b/src/common/database/sharding/examples/shard-payment.example.ts deleted file mode 100644 index 38a82099..00000000 --- a/src/common/database/sharding/examples/shard-payment.example.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ShardTransactionService } from '../index'; - -/** - * Example: Shard-Aware Payment Processing - * Demonstrates payment processing with shard routing based on tenant - */ -@Injectable() -export class ShardPaymentExample { - private readonly logger = new Logger(ShardPaymentExample.name); - - constructor(private shardTransactionService: ShardTransactionService) {} - - /** - * Process payment for a tenant (auto-routes to correct shard) - */ - async processPayment(tenantId: string, userId: string, amount: number): Promise { - return this.shardTransactionService.runOnShard(tenantId, async (manager: any) => { - // 1. Check tenant balance - const tenant = await manager.query('SELECT balance FROM tenants WHERE id = $1 FOR UPDATE', [ - tenantId, - ]); - - if (!tenant || tenant.length === 0) { - throw new Error('Tenant not found'); - } - - if (tenant[0].balance < amount) { - throw new Error('Insufficient tenant balance'); - } - - // 2. Deduct from tenant - await manager.query('UPDATE tenants SET balance = balance - $1 WHERE id = $2', [ - amount, - tenantId, - ]); - - // 3. Create payment record - const payment = await manager.query( - `INSERT INTO payments (tenant_id, user_id, amount, status) - VALUES ($1, $2, $3, $4) RETURNING *`, - [tenantId, userId, amount, 'completed'], - ); - - this.logger.log(`Payment processed: ${payment[0].id}`); - - return payment[0]; - }); - } - - /** - * Execute cross-tenant payment (between different shards) - */ - async processCrossTenantPayment( - fromTenantId: string, - toTenantId: string, - amount: number, - ): Promise { - return this.shardTransactionService.runCrossShard([ - { - shardKey: fromTenantId, - query: 'UPDATE tenants SET balance = balance - $1 WHERE id = $2', - parameters: [amount, fromTenantId], - }, - { - shardKey: toTenantId, - query: 'UPDATE tenants SET balance = balance + $1 WHERE id = $2', - parameters: [amount, toTenantId], - }, - ]); - } - - /** - * Get payment report for a tenant - */ - async getTenantPaymentReport(tenantId: string): Promise { - return this.shardTransactionService.runOnShard(tenantId, async (manager: any) => { - const payments = await manager.query( - 'SELECT * FROM payments WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT 100', - [tenantId], - ); - - const total = await manager.query( - "SELECT SUM(amount) as total FROM payments WHERE tenant_id = $1 AND status = 'completed'", - [tenantId], - ); - - return { - payments, - totalAmount: total[0]?.total || 0, - }; - }); - } - - /** - * Get global payment statistics (across all shards) - */ - async getGlobalPaymentStats(): Promise { - const count = await this.shardTransactionService.crossShardQuery( - "SELECT COUNT(*) as count FROM payments WHERE status = 'completed'", - { allShards: true, aggregationStrategy: 'aggregate' }, - ); - const total = await this.shardTransactionService.crossShardQuery( - "SELECT SUM(amount) as total FROM payments WHERE status = 'completed'", - { allShards: true, aggregationStrategy: 'aggregate' }, - ); - const avg = await this.shardTransactionService.crossShardQuery( - "SELECT AVG(amount) as avg FROM payments WHERE status = 'completed'", - { allShards: true, aggregationStrategy: 'aggregate' }, - ); - - return { - totalPayments: parseInt((count[0] as any)?.count || '0', 10), - totalAmount: parseFloat((total[0] as any)?.total || '0'), - averageAmount: parseFloat((avg[0] as any)?.avg || '0'), - }; - } -} diff --git a/src/common/database/sharding/examples/shard-user-repository.example.ts b/src/common/database/sharding/examples/shard-user-repository.example.ts deleted file mode 100644 index 30ea77ad..00000000 --- a/src/common/database/sharding/examples/shard-user-repository.example.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ShardRouter, CrossShardQueryCoordinator, ShardDataSourceManager } from '..'; - -/** - * Example: Shard-Aware User Repository - * Demonstrates how to use sharding for user data distribution - */ -@Injectable() -export class UserShardRepository { - private readonly logger = new Logger(UserShardRepository.name); - - constructor( - private shardRouter: ShardRouter, - private dataSourceManager: ShardDataSourceManager, - private queryCoordinator: CrossShardQueryCoordinator, - ) {} - - /** - * Add user to appropriate shard based on tenantId - */ - async createUser(tenantId: string, userData: any): Promise { - const shardId = this.shardRouter.route(tenantId); - const results = await this.dataSourceManager.query( - shardId, - `INSERT INTO users (tenant_id, username, email, created_at) - VALUES ($1, $2, $3, NOW()) RETURNING *`, - [tenantId, userData.username, userData.email], - ); - return results[0]; - } - - /** - * Find user by tenant (routes to correct shard) - */ - async findByTenant(tenantId: string): Promise { - const shardId = this.shardRouter.route(tenantId); - return this.dataSourceManager.query(shardId, 'SELECT * FROM users WHERE tenant_id = $1', [ - tenantId, - ]); - } - - /** - * Get total user count across all shards - */ - async getTotalUserCount(): Promise { - const results = await this.queryCoordinator.executeCrossShardQuery<{ count: string }>({ - query: 'SELECT COUNT(*) as count FROM users', - allShards: true, - aggregationStrategy: 'aggregate', - }); - return parseInt(results[0]?.count || '0', 10); - } - - /** - * Find user across all shards (expensive - use only when necessary) - */ - async findUserGlobally(userId: string): Promise { - const results = await this.queryCoordinator.executeCrossShardQuery({ - query: 'SELECT * FROM users WHERE id = $1', - parameters: [userId], - allShards: true, - aggregationStrategy: 'first', - }); - return results[0] || null; - } - - /** - * Query all users across shards with custom filter - */ - async findActiveUsersAcrossShards(): Promise { - return this.queryCoordinator.executeCrossShardQuery({ - query: - "SELECT * FROM users WHERE status = 'active' AND created_at > NOW() - INTERVAL '30 days'", - allShards: true, - aggregationStrategy: 'merge', - }); - } -} diff --git a/src/common/database/sharding/hash/shard.hash.ts b/src/common/database/sharding/hash/shard.hash.ts deleted file mode 100644 index c713ce7b..00000000 --- a/src/common/database/sharding/hash/shard.hash.ts +++ /dev/null @@ -1,209 +0,0 @@ -import crc32 from 'crc-32'; -import { createHash } from 'crypto'; - -/** - * Hash utility for shard routing - * Provides consistent hashing with virtual nodes for even data distribution - */ -export class ShardHash { - private ring: Map = new Map(); - private sortedKeys: number[] = []; - private shardWeights: Map = new Map(); - - constructor( - private shards: string[], - private weights: Map, - private virtualNodesPerShard: number = 150, - ) { - this.buildRing(); - } - - /** - * Build the consistent hashing ring - */ - private buildRing(): void { - for (const shard of this.shards) { - const weight = this.weights.get(shard) || 1; - const virtualNodes = this.virtualNodesPerShard * weight; - - for (let i = 0; i < virtualNodes; i++) { - const virtualKey = `${shard}#${i}`; - const hash = this.hash(virtualKey); - this.ring.set(hash, shard); - this.sortedKeys.push(hash); - } - } - - this.sortedKeys.sort((a, b) => a - b); - } - - /** - * Generate hash for a key using configured algorithm - */ - private hash(key: string, algorithm: string = 'murmur3'): number { - switch (algorithm) { - case 'crc32': - return Math.abs(crc32.str(key)); - case 'md5': - return parseInt(createHash('md5').update(key).digest('hex').substring(0, 8), 16); - case 'sha256': - return parseInt(createHash('sha256').update(key).digest('hex').substring(0, 8), 16); - case 'murmur3': - return Math.abs(this.murmurHash(key)); - default: - return Math.abs(this.murmurHash(key)); - } - } - - /** - * Simple MurmurHash3 implementation (browser-safe, no external dependencies) - */ - private murmurHash(key: string): number { - const c1 = 0xcc9e2d51; - const c2 = 0x1b873593; - const r1 = 15; - const r2 = 13; - const m = 5; - const n = 0xe6546b64; - - let hash = 0; - const bytes = new TextEncoder().encode(key); - const blocks = Math.floor(bytes.length / 4); - - for (let i = 0; i < blocks; i++) { - let k = - (bytes[i * 4] & 0xff) | - ((bytes[i * 4 + 1] & 0xff) << 8) | - ((bytes[i * 4 + 2] & 0xff) << 16) | - ((bytes[i * 4 + 3] & 0xff) << 24); - - k = Math.imul(k, c1); - k = (k << r1) | (k >>> (32 - r1)); - k = Math.imul(k, c2); - - hash ^= k; - hash = (hash << r2) | (hash >>> (32 - r2)); - hash = Math.imul(hash, m) + n; - } - - let k1 = 0; - const tail = bytes.length % 4; - const tailStart = blocks * 4; - - // MurmurHash3 finalization mix - handle remaining bytes - // The following switch intentionally omits break statements for fallthrough. - // This is the correct MurmurHash3 algorithm implementation. - switch (tail) { - // @ts-expect-error: noFallthroughCasesInSwitch - intentional for MurmurHash3 - case 3: - k1 ^= (bytes[tailStart + 2] & 0xff) << 16; - // @ts-expect-error: noFallthroughCasesInSwitch - intentional for MurmurHash3 - case 2: - k1 ^= (bytes[tailStart + 1] & 0xff) << 8; - // @ts-expect-error: noFallthroughCasesInSwitch - intentional for MurmurHash3 - case 1: - k1 ^= bytes[tailStart] & 0xff; - k1 = Math.imul(k1, c1); - k1 = (k1 << r1) | (k1 >>> (32 - r1)); - k1 = Math.imul(k1, c2); - hash ^= k1; - } - - hash ^= bytes.length; - hash ^= hash >>> 16; - hash = Math.imul(hash, 0x85ebca6b); - hash ^= hash >>> 13; - hash = Math.imul(hash, 0xc2b2ae35); - hash ^= hash >>> 16; - - return hash; - } - - /** - * Get the shard for a given key using consistent hashing - */ - getShard(key: string): string { - if (this.ring.size === 0) { - throw new Error('Hash ring is empty'); - } - - const hash = this.hash(key); - const index = this.findShardIndex(hash); - return this.ring.get(this.sortedKeys[index])!; - } - - /** - * Find the appropriate shard index using binary search - */ - private findShardIndex(hash: number): number { - let start = 0; - let end = this.sortedKeys.length - 1; - - while (start <= end) { - const mid = Math.floor((start + end) / 2); - - if (this.sortedKeys[mid] === hash) { - return mid; - } - - if (this.sortedKeys[mid] < hash) { - start = mid + 1; - } else { - end = mid - 1; - } - } - - // Wrap around to the first shard if we're past the end - return start % this.sortedKeys.length; - } - - /** - * Get all possible shards for a key (for replication) - */ - getShards(key: string, count: number = 1): string[] { - if (count > this.shards.length) { - count = this.shards.length; - } - - const hash = this.hash(key); - const startIndex = this.findShardIndex(hash); - const result: string[] = []; - const seen = new Set(); - - for (let i = 0; i < this.sortedKeys.length && result.length < count; i++) { - const index = (startIndex + i) % this.sortedKeys.length; - const shard = this.ring.get(this.sortedKeys[index])!; - - if (!seen.has(shard)) { - seen.add(shard); - result.push(shard); - } - } - - return result; - } - - /** - * Get the shard distribution statistics - */ - getDistribution(): Map { - const distribution = new Map(); - - for (const shard of this.shards) { - distribution.set(shard, 0); - } - - for (const [, shard] of this.ring) { - distribution.set(shard, (distribution.get(shard) || 0) + 1); - } - - return distribution; - } - - /** - * Get all shards in the ring - */ - getAllShards(): string[] { - return [...new Set(this.ring.values())]; - } -} diff --git a/src/common/database/sharding/index.ts b/src/common/database/sharding/index.ts deleted file mode 100644 index 98d1ebdb..00000000 --- a/src/common/database/sharding/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Database Sharding Module - Exports - * - * Comprehensive database sharding implementation including: - * - Shard routing with consistent hashing - * - Data distribution across multiple shards - * - Cross-shard query coordination - * - Shard-aware connection management - * - Transaction management across shards - * - Shard management and monitoring utilities - */ - -export { ShardingModule } from './sharding.module'; - -// Configuration -export { shardConfig } from './config/shard.config'; -export type { ShardConfig, ShardGroupConfig, ShardingConfig } from './config/shard.config'; - -// Hash Ring -export { ShardHash } from './hash/shard.hash'; - -// Router -export { ShardRouter } from './router/shard.router'; - -// Data Source Manager -export { ShardDataSourceManager } from './datasource/shard-datasource.manager'; - -// Query Runner -export { ShardAwareQueryRunner } from './runner/shard-aware-query-runner'; - -// Query Coordinator -export { CrossShardQueryCoordinator } from './coordinator/cross-shard-query-coordinator'; - -// Transaction Service -export { ShardTransactionService } from './shard-transaction.service'; - -// Management Service -export { ShardManagementService } from './services/shard-management.service'; - -// Decorators -export { ShardAware, ShardKey } from './decorators/shard-aware.decorator'; -export { extractShardKey } from './decorators/shard-aware.decorator'; - -// Repository Base -export { ShardAwareRepository } from './repository/shard-aware-repository'; - -// Constants -export * from './constants/shard.constants'; diff --git a/src/common/database/sharding/repository/shard-aware-repository.ts b/src/common/database/sharding/repository/shard-aware-repository.ts deleted file mode 100644 index 8cecc2bc..00000000 --- a/src/common/database/sharding/repository/shard-aware-repository.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ShardRouter } from '../router/shard.router'; -import { ShardDataSourceManager } from '../datasource/shard-datasource.manager'; -import { CrossShardQueryCoordinator } from '../coordinator/cross-shard-query-coordinator'; - -/** - * Shard-aware repository base class - * Provides sharded data access operations - */ -@Injectable() -export abstract class ShardAwareRepository { - protected readonly logger = new Logger(this.constructor.name); - - constructor( - protected shardRouter: ShardRouter, - protected dataSourceManager: ShardDataSourceManager, - protected queryCoordinator: CrossShardQueryCoordinator, - protected tableName: string, - ) {} - - /** - * Find by shard key (routes to specific shard) - */ - async findOneByShardKey(shardKey: string, shardGroup: string = 'primary'): Promise { - const shardId = this.shardRouter.route(shardKey, shardGroup); - const results = await this.dataSourceManager.query( - shardId, - `SELECT * FROM ${this.tableName} WHERE shard_key = $1 LIMIT 1`, - [shardKey], - ); - return results[0] || null; - } - - /** - * Find by ID across all shards (expensive - use sparingly) - */ - async findByIdCrossShard(id: string): Promise { - const results = await this.queryCoordinator.executeCrossShardQuery({ - query: `SELECT * FROM ${this.tableName} WHERE id = $1`, - parameters: [id], - allShards: true, - aggregationStrategy: 'first', - }); - return results[0] || null; - } - - /** - * Find all on specific shard - */ - async findAllOnShard(shardId: string): Promise { - return this.dataSourceManager.query(shardId, `SELECT * FROM ${this.tableName}`); - } - - /** - * Find all across all shards - */ - async findAllCrossShard(): Promise { - return this.queryCoordinator.executeCrossShardQuery({ - query: `SELECT * FROM ${this.tableName}`, - allShards: true, - aggregationStrategy: 'merge', - }); - } - - /** - * Insert on specific shard - */ - async insertOnShard( - shardKey: string, - data: Partial, - shardGroup: string = 'primary', - ): Promise { - const shardId = this.shardRouter.route(shardKey, shardGroup); - const columns = Object.keys(data).join(', '); - const values = Object.values(data); - const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); - - const results = await this.dataSourceManager.query( - shardId, - `INSERT INTO ${this.tableName} (${columns}) VALUES (${placeholders}) RETURNING *`, - values, - ); - return results[0]; - } - - /** - * Update on specific shard - */ - async updateOnShard( - shardKey: string, - data: Partial, - shardGroup: string = 'primary', - ): Promise { - const shardId = this.shardRouter.route(shardKey, shardGroup); - const setClause = Object.keys(data) - .map((key, i) => `${key} = $${i + 1}`) - .join(', '); - const values = Object.values(data); - - const results = await this.dataSourceManager.query( - shardId, - `UPDATE ${this.tableName} SET ${setClause} WHERE shard_key = $${values.length + 1} RETURNING *`, - [...values, shardKey], - ); - return results[0] || null; - } - - /** - * Delete on specific shard - */ - async deleteOnShard(shardKey: string, shardGroup: string = 'primary'): Promise { - const shardId = this.shardRouter.route(shardKey, shardGroup); - const result = await this.dataSourceManager.query( - shardId, - `DELETE FROM ${this.tableName} WHERE shard_key = $1`, - [shardKey], - ); - return result.length > 0; - } - - /** - * Count on specific shard - */ - async countOnShard(shardId: string): Promise { - const results = await this.dataSourceManager.query<{ count: string }>( - shardId, - `SELECT COUNT(*) as count FROM ${this.tableName}`, - ); - return parseInt(results[0]?.count || '0', 10); - } - - /** - * Count across all shards - */ - async countCrossShard(): Promise { - const results = await this.queryCoordinator.executeCrossShardQuery<{ count: string }>({ - query: `SELECT COUNT(*) as count FROM ${this.tableName}`, - allShards: true, - aggregationStrategy: 'aggregate', - }); - return parseInt(results[0]?.count || '0', 10); - } - - /** - * Execute custom query on specific shard - */ - async query(shardId: string, query: string, parameters?: any[]): Promise { - return this.dataSourceManager.query(shardId, query, parameters); - } - - /** - * Execute custom cross-shard query - */ - async queryCrossShard(query: string, parameters?: any[]): Promise { - return this.queryCoordinator.executeCrossShardQuery({ - query, - parameters, - allShards: true, - aggregationStrategy: 'merge', - }); - } -} diff --git a/src/common/database/sharding/router/shard.router.ts b/src/common/database/sharding/router/shard.router.ts deleted file mode 100644 index 6c3fa5cb..00000000 --- a/src/common/database/sharding/router/shard.router.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { ShardConfig, ShardGroupConfig, ShardingConfig } from '../config/shard.config'; -import { ShardHash } from '../hash/shard.hash'; -import { Logger } from '@nestjs/common'; - -/** - * Shard Router - * Routes database operations to the correct shard based on shard key - */ -export class ShardRouter { - private readonly logger = new Logger(ShardRouter.name); - private hashRing!: ShardHash; - private shardConfigs: Map = new Map(); - private shardGroupConfigs: Map = new Map(); - private explicitMappings: Map = new Map(); - - constructor(private config: ShardingConfig) { - this.initialize(config); - } - - /** - * Initialize the router with configuration - */ - private initialize(config: ShardingConfig): void { - this.shardConfigs = new Map(Object.entries(config.shards)); - this.shardGroupConfigs = new Map(Object.entries(config.shardGroups)); - this.explicitMappings = new Map(Object.entries(config.shardMappings)); - - const shards = Object.keys(config.shards); - const weights = new Map(); - - for (const [shardId, shard] of this.shardConfigs) { - weights.set(shardId, shard.weight); - } - - this.hashRing = new ShardHash(shards, weights, config.virtualNodesPerShard); - - this.logger.log(`Shard router initialized with ${shards.length} shards`); - } - - /** - * Route a key to a shard - */ - route(key: string, group: string = 'primary'): string { - // Check explicit mappings first - const explicitShard = this.explicitMappings.get(key); - if (explicitShard) { - this.logger.debug(`Explicit mapping: ${key} -> ${explicitShard}`); - if (this.isShardActive(explicitShard)) { - return explicitShard; - } - - if (this.config.fallbackOnShardFailure) { - this.logger.warn(`Shard ${explicitShard} is not active, using default`); - return this.config.defaultShard; - } - throw new Error(`Shard ${explicitShard} is not active and fallback is disabled`); - } - - // Use consistent hashing for distribution - const shardGroup = this.shardGroupConfigs.get(group); - if (!shardGroup) { - throw new Error(`Shard group ${group} not found`); - } - - let primaryShard: string; - - switch (shardGroup.strategy) { - case 'hash': - primaryShard = this.hashRing.getShard(key); - break; - case 'range': - primaryShard = this.routeByRange(key, shardGroup); - break; - case 'list': - primaryShard = this.routeByList(key, shardGroup); - break; - case 'composite': - primaryShard = this.routeByComposite(key, shardGroup); - break; - default: - primaryShard = this.hashRing.getShard(key); - } - - // Verify shard is active - if (!this.isShardActive(primaryShard)) { - if (this.config.fallbackOnShardFailure) { - this.logger.warn(`Primary shard ${primaryShard} is not active, using default`); - return this.config.defaultShard; - } - throw new Error(`Shard ${primaryShard} is not active and fallback is disabled`); - } - - this.logger.debug(`Routed key ${key} to shard ${primaryShard}`); - return primaryShard; - } - - /** - * Get all shards for a key (for replication or fault tolerance) - */ - routeReplicas(key: string, group: string = 'primary'): string[] { - const shardGroup = this.shardGroupConfigs.get(group); - if (!shardGroup) { - throw new Error(`Shard group ${group} not found`); - } - - if (!shardGroup.replication) { - return [this.route(key, group)]; - } - - const replicas = this.hashRing.getShards(key, 3); // Get 3 replicas - return replicas.filter((shard) => this.isShardActive(shard)); - } - - /** - * Route by range strategy - */ - private routeByRange(key: string, _group: ShardGroupConfig): string { - // For numeric keys, distribute by range - const numericKey = parseInt(key, 10); - if (isNaN(numericKey)) { - return this.hashRing.getShard(key); - } - - // Use hash as fallback for range-based routing - return this.hashRing.getShard(key); - } - - /** - * Route by list strategy - */ - private routeByList(key: string, _group: ShardGroupConfig): string { - // Use predefined mappings for list strategy - return this.hashRing.getShard(key); - } - - /** - * Route by composite strategy - */ - private routeByComposite(key: string, _group: ShardGroupConfig): string { - // Combine multiple strategies - return this.hashRing.getShard(key); - } - - /** - * Check if a shard is active - */ - isShardActive(shardId: string): boolean { - const shard = this.shardConfigs.get(shardId); - return shard?.status === 'active'; - } - - /** - * Get shard configuration - */ - getShardConfig(shardId: string): ShardConfig | undefined { - return this.shardConfigs.get(shardId); - } - - /** - * Get all active shards - */ - getActiveShards(group: string = 'primary'): string[] { - const shardGroup = this.shardGroupConfigs.get(group); - if (!shardGroup) { - return []; - } - - return shardGroup.shards.filter((shardId) => this.isShardActive(shardId)); - } - - /** - * Get all available shards - */ - getAllShards(): string[] { - return Array.from(this.shardConfigs.keys()); - } - - /** - * Get shard distribution statistics - */ - getDistribution(): Map { - return this.hashRing.getDistribution(); - } - - /** - * Add explicit mapping for a key to shard - */ - addMapping(key: string, shardId: string): void { - if (!this.isShardActive(shardId)) { - throw new Error(`Cannot map to inactive shard ${shardId}`); - } - this.explicitMappings.set(key, shardId); - this.logger.log(`Added mapping: ${key} -> ${shardId}`); - } - - /** - * Remove explicit mapping - */ - removeMapping(key: string): void { - this.explicitMappings.delete(key); - } - - /** - * Update shard status - */ - updateShardStatus(shardId: string, status: 'active' | 'inactive' | 'maintenance'): void { - const shard = this.shardConfigs.get(shardId); - if (shard) { - shard.status = status; - this.logger.log(`Updated shard ${shardId} status to ${status}`); - } - } - - /** - * Rebuild hash ring (useful after configuration changes) - */ - rebuild(): void { - this.initialize(this.config); - } -} diff --git a/src/common/database/sharding/services/shard-management.service.ts b/src/common/database/sharding/services/shard-management.service.ts deleted file mode 100644 index 5be5a3a6..00000000 --- a/src/common/database/sharding/services/shard-management.service.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ShardRouter } from '../router/shard.router'; -import { ShardDataSourceManager } from '../datasource/shard-datasource.manager'; -import { CrossShardQueryCoordinator } from '../coordinator/cross-shard-query-coordinator'; - -/** - * Shard Management Service - * Provides utilities for shard management, rebalancing, and monitoring - */ -@Injectable() -export class ShardManagementService { - private readonly logger = new Logger(ShardManagementService.name); - - constructor( - private shardRouter: ShardRouter, - private dataSourceManager: ShardDataSourceManager, - private queryCoordinator: CrossShardQueryCoordinator, - ) {} - - /** - * Add a new shard to the cluster - */ - async addShard( - _shardId: string, - _config: { - name: string; - host: string; - port: number; - database: string; - username: string; - password: string; - weight?: number; - }, - ): Promise { - // Note: In production, this would require dynamic configuration updates - // and potentially rebalancing existing data - throw new Error( - 'Shard addition requires configuration restart. Use migration utilities instead.', - ); - } - - /** - * Rebalance data across shards - */ - async rebalanceShards( - sourceShard: string, - targetShard: string, - table: string, - keyField: string, - filter?: string, - ): Promise<{ rowsMoved: number; duration: number }> { - const startTime = Date.now(); - - this.logger.log(`Starting rebalance from ${sourceShard} to ${targetShard} for table ${table}`); - - // Get data to move - const data = await this.dataSourceManager.query( - sourceShard, - `SELECT * FROM ${table} ${filter ? `WHERE ${filter}` : ''}`, - ); - - if (data.length === 0) { - this.logger.log('No data to rebalance'); - return { rowsMoved: 0, duration: 0 }; - } - - // Insert into target shard - const keys = Object.keys(data[0]); - const columns = keys.join(', '); - - for (const row of data) { - const values = keys.map((key) => `'${row[key]}'`).join(', '); - await this.dataSourceManager.query( - targetShard, - `INSERT INTO ${table} (${columns}) VALUES (${values})`, - ); - } - - // Delete from source shard - await this.dataSourceManager.query( - sourceShard, - `DELETE FROM ${table} ${filter ? `WHERE ${filter}` : ''}`, - ); - - const duration = Date.now() - startTime; - this.logger.log(`Rebalanced ${data.length} rows in ${duration}ms`); - - return { rowsMoved: data.length, duration }; - } - - /** - * Split a shard into two - */ - async splitShard( - sourceShard: string, - newShard1: string, - newShard2: string, - _keyField: string, - ): Promise { - this.logger.log(`Splitting shard ${sourceShard} into ${newShard1} and ${newShard2}`); - - // Note: This requires copying data and updating the hash ring - throw new Error('Shard splitting requires full reconfiguration'); - } - - /** - * Merge two shards - */ - async mergeShards(shard1: string, shard2: string, _keyField: string): Promise { - this.logger.log(`Merging shards ${shard1} and ${shard2}`); - - // Note: This requires copying data and updating the hash ring - throw new Error('Shard merging requires full reconfiguration'); - } - - /** - * Get shard statistics - */ - async getShardStats(): Promise<{ - [shardId: string]: { - rowCount: Record; - size: string; - health: any; - }; - }> { - const shardIds = this.shardRouter.getAllShards(); - const stats: any = {}; - - for (const shardId of shardIds) { - try { - // Get table counts - const tableStats = await this.dataSourceManager.query( - shardId, - "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'", - ); - - const rowCounts: Record = {}; - - for (const table of tableStats) { - const count = await this.dataSourceManager.query( - shardId, - `SELECT COUNT(*) as count FROM ${table.table_name}`, - ); - rowCounts[table.table_name] = parseInt(count[0].count, 10) || 0; - } - - // Get database size - const size = await this.dataSourceManager.query( - shardId, - 'SELECT pg_size_pretty(pg_database_size(current_database())) as size', - ); - - // Get health - const health = await this.dataSourceManager.getShardHealth(shardId); - - stats[shardId] = { - rowCount: rowCounts, - size: size[0]?.size || 'unknown', - health, - }; - } catch (error) { - this.logger.error(`Failed to get stats for shard ${shardId}:`, error); - stats[shardId] = { - error: error.message, - health: await this.dataSourceManager.getShardHealth(shardId), - }; - } - } - - return stats; - } - - /** - * Get cluster health - */ - async getClusterHealth() { - return this.queryCoordinator.getClusterHealth(); - } - - /** - * Get shard distribution - */ - getDistribution() { - return this.shardRouter.getDistribution(); - } - - /** - * Migrate data between shards - */ - async migrateData( - sourceShard: string, - targetShard: string, - tables: string[], - ): Promise<{ migratedRows: number; duration: number }> { - const startTime = Date.now(); - let totalRows = 0; - - for (const table of tables) { - const { rowsMoved } = await this.rebalanceShards(sourceShard, targetShard, table, 'id'); - totalRows += rowsMoved; - } - - const duration = Date.now() - startTime; - - return { migratedRows: totalRows, duration }; - } - - /** - * Update shard status - */ - updateShardStatus(shardId: string, status: 'active' | 'inactive' | 'maintenance'): void { - this.shardRouter.updateShardStatus(shardId, status); - this.logger.log(`Updated shard ${shardId} status to ${status}`); - } - - /** - * Add explicit shard mapping - */ - addShardMapping(key: string, shardId: string): void { - this.shardRouter.addMapping(key, shardId); - } - - /** - * Remove shard mapping - */ - removeShardMapping(key: string): void { - this.shardRouter.removeMapping(key); - } -} diff --git a/src/common/database/sharding/shard-transaction.service.ts b/src/common/database/sharding/shard-transaction.service.ts deleted file mode 100644 index c1ed5cca..00000000 --- a/src/common/database/sharding/shard-transaction.service.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ShardRouter } from './router/shard.router'; -import { CrossShardQueryCoordinator } from './coordinator/cross-shard-query-coordinator'; -import { ShardDataSourceManager } from './datasource/shard-datasource.manager'; -import { ITransactionMetrics } from '../transaction.service'; - -/** - * Shard-Aware Transaction Service - * Extends transaction management with shard routing and cross-shard coordination - */ -@Injectable() -export class ShardTransactionService { - private readonly logger = new Logger(ShardTransactionService.name); - private readonly activeTransactions = new Map(); - - constructor( - private shardRouter: ShardRouter, - private queryCoordinator: CrossShardQueryCoordinator, - private dataSourceManager: ShardDataSourceManager, - ) {} - - /** - * Execute transaction on a specific shard (determined by shard key) - */ - async runOnShard( - shardKey: string, - operation: (manager: any) => Promise, - shardGroup: string = 'primary', - ): Promise { - const shardId = this.shardRouter.route(shardKey, shardGroup); - const transactionId = this.generateTransactionId(); - const startTime = new Date(); - - const metrics: ITransactionMetrics = { - transactionId, - startTime, - status: 'STARTED', - operations: [`shard:${shardId}`], - }; - this.activeTransactions.set(transactionId, metrics); - - try { - this.logger.debug(`Starting shard transaction ${transactionId} on ${shardId}`); - - const result = await this.dataSourceManager.runOnShard(shardId, operation); - - const endTime = new Date(); - metrics.endTime = endTime; - metrics.duration = endTime.getTime() - startTime.getTime(); - metrics.status = 'COMMITTED'; - - this.logger.log( - `Shard transaction ${transactionId} committed on ${shardId} in ${metrics.duration}ms`, - ); - - return result; - } catch (error) { - const endTime = new Date(); - metrics.endTime = endTime; - metrics.duration = endTime.getTime() - startTime.getTime(); - metrics.status = 'ROLLED_BACK'; - metrics.error = error instanceof Error ? error.message : String(error); - - this.logger.error(`Shard transaction ${transactionId} rolled back on ${shardId}:`, error); - throw error; - } finally { - this.logger.debug(`Shard transaction ${transactionId} resources released`); - } - } - - /** - * Execute cross-shard transaction - */ - async runCrossShard( - operations: Array<{ - shardKey: string; - query: string; - parameters?: any[]; - }>, - ): Promise { - const transactionId = this.generateTransactionId(); - const startTime = new Date(); - - const metrics: ITransactionMetrics = { - transactionId, - startTime, - status: 'STARTED', - operations: ['cross-shard'], - }; - this.activeTransactions.set(transactionId, metrics); - - try { - this.logger.debug( - `Starting cross-shard transaction ${transactionId} with ${operations.length} operations`, - ); - - const result = await this.queryCoordinator.executeCrossShardTransaction(operations); - - const endTime = new Date(); - metrics.endTime = endTime; - metrics.duration = endTime.getTime() - startTime.getTime(); - metrics.status = 'COMMITTED'; - - this.logger.log( - `Cross-shard transaction ${transactionId} committed in ${metrics.duration}ms`, - ); - - return result as T; - } catch (error) { - const endTime = new Date(); - metrics.endTime = endTime; - metrics.duration = endTime.getTime() - startTime.getTime(); - metrics.status = 'ROLLED_BACK'; - metrics.error = error instanceof Error ? error.message : String(error); - - this.logger.error(`Cross-shard transaction ${transactionId} rolled back:`, error); - throw error; - } finally { - this.logger.debug(`Cross-shard transaction ${transactionId} resources released`); - } - } - - /** - * Execute query across multiple shards - */ - async crossShardQuery( - query: string, - options?: { - shardKey?: string; - shardGroup?: string; - allShards?: boolean; - aggregationStrategy?: 'merge' | 'union' | 'aggregate' | 'first'; - parameters?: any[]; - }, - ): Promise { - return this.queryCoordinator.executeCrossShardQuery({ - query, - ...options, - }); - } - - /** - * Execute cross-shard aggregation - */ - async crossShardAggregate( - aggregationQueries: Array<{ - shardId?: string; - query: string; - parameters?: any[]; - mergeKey?: string; - }>, - ): Promise { - return this.queryCoordinator.executeCrossShardAggregation(aggregationQueries); - } - - /** - * Execute parallel operations on multiple shards - */ - async parallelShardOperations( - operations: Array<{ - shardId: string; - operation: (manager: any) => Promise; - }>, - ): Promise> { - const results = new Map(); - const errors: Error[] = []; - - const executionPromises = operations.map(async ({ shardId, operation }) => { - try { - const result = await this.dataSourceManager.runOnShard(shardId, operation); - results.set(shardId, result); - } catch (error) { - errors.push(new Error(`Shard ${shardId}: ${error.message}`)); - } - }); - - await Promise.all(executionPromises); - - if (errors.length > 0 && results.size === 0) { - throw new Error(`All shard operations failed: ${errors.map((e) => e.message).join(', ')}`); - } - - if (errors.length > 0) { - this.logger.warn( - `Partial failure: ${errors.length} shards failed, ${results.size} succeeded`, - ); - } - - return results; - } - - /** - * Get active transactions - */ - getActiveTransactions(): ITransactionMetrics[] { - return Array.from(this.activeTransactions.values()); - } - - /** - * Get transaction metrics - */ - getTransactionMetrics(): ITransactionMetrics[] { - return Array.from(this.activeTransactions.values()); - } - - /** - * Clear completed transactions - */ - clearCompletedTransactions(): void { - const now = new Date(); - for (const [id, metrics] of this.activeTransactions.entries()) { - if (metrics.endTime && now.getTime() - metrics.endTime.getTime() > 300000) { - this.activeTransactions.delete(id); - } - } - } - - /** - * Generate transaction ID - */ - private generateTransactionId(): string { - return `stx_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; - } -} diff --git a/src/common/database/sharding/sharding.module.ts b/src/common/database/sharding/sharding.module.ts deleted file mode 100644 index 4510f86e..00000000 --- a/src/common/database/sharding/sharding.module.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Global, Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { shardConfig } from './config/shard.config'; -import { ShardRouter } from './router/shard.router'; -import { ShardDataSourceManager } from './datasource/shard-datasource.manager'; -import { CrossShardQueryCoordinator } from './coordinator/cross-shard-query-coordinator'; -import { ShardAwareQueryRunner } from './runner/shard-aware-query-runner'; -import { ShardHash } from './hash/shard.hash'; - -/** - * Database Sharding Module - * - * Implements database sharding with: - * - Shard routing using consistent hashing - * - Data distribution across multiple shards - * - Cross-shard query coordination - * - Shard-aware connection management - */ -@Global() -@Module({ - imports: [ConfigModule.forFeature(shardConfig)], - providers: [ - { - provide: ShardHash, - useFactory: (config: any) => { - const shards = Object.keys(config.shards); - const weights = new Map(); - for (const [shardId, shard] of Object.entries(config.shards)) { - weights.set(shardId, (shard as any).weight); - } - return new ShardHash(shards, weights, config.virtualNodesPerShard); - }, - inject: ['sharding'], - }, - { - provide: ShardRouter, - useFactory: (config: any) => { - return new ShardRouter(config); - }, - inject: ['sharding'], - }, - { - provide: ShardDataSourceManager, - useFactory: (config: any, _shardRouter: ShardRouter) => { - const shardConfigs = new Map(); - for (const [shardId, shard] of Object.entries(config.shards)) { - shardConfigs.set(shardId, shard); - } - return new ShardDataSourceManager(shardConfigs); - }, - inject: ['sharding', ShardRouter], - }, - { - provide: CrossShardQueryCoordinator, - useFactory: (_shardRouter: ShardRouter, dataSourceManager: ShardDataSourceManager) => { - return new CrossShardQueryCoordinator(_shardRouter, dataSourceManager); - }, - inject: [ShardRouter, ShardDataSourceManager], - }, - { - provide: ShardAwareQueryRunner, - useFactory: (dataSourceManager: ShardDataSourceManager) => { - return new ShardAwareQueryRunner(dataSourceManager); - }, - inject: [ShardDataSourceManager], - }, - ], - exports: [ - ShardHash, - ShardRouter, - ShardDataSourceManager, - CrossShardQueryCoordinator, - ShardAwareQueryRunner, - ], -}) -export class ShardingModule {} diff --git a/src/common/database/transaction.service.ts b/src/common/database/transaction.service.ts deleted file mode 100644 index 09759555..00000000 --- a/src/common/database/transaction.service.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { DataSource, EntityManager, QueryRunner } from 'typeorm'; -/** - * Transaction monitoring interface - */ -export interface ITransactionMetrics { - transactionId: string; - startTime: Date; - endTime?: Date; - duration?: number; - status: 'STARTED' | 'COMMITTED' | 'ROLLED_BACK'; - operations: string[]; - error?: string; -} -/** - * Transaction Service - * Provides robust transaction management for critical operations - */ -@Injectable() -export class TransactionService { - private readonly logger = new Logger(TransactionService.name); - private readonly activeTransactions = new Map(); - - constructor(private readonly dataSource: DataSource) {} - - /** - * Execute operations within a transaction - * Automatically handles commit and rollback - */ - async runInTransaction(operation: (manager: EntityManager) => Promise): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - const transactionId = this.generateTransactionId(); - const startTime = new Date(); - const metrics: ITransactionMetrics = { - transactionId, - startTime, - status: 'STARTED', - operations: [], - }; - this.activeTransactions.set(transactionId, metrics); - - try { - this.logger.debug(`Transaction ${transactionId} started`); - this.logger.log(`Transaction ${transactionId}: Starting operation execution`); - - const result = await operation(queryRunner.manager); - - await queryRunner.commitTransaction(); - const endTime = new Date(); - const duration = endTime.getTime() - startTime.getTime(); - - metrics.endTime = endTime; - metrics.duration = duration; - metrics.status = 'COMMITTED'; - - this.logger.log(`Transaction ${transactionId} committed successfully in ${duration}ms`); - return result; - } catch (error) { - await queryRunner.rollbackTransaction(); - const endTime = new Date(); - const duration = endTime.getTime() - startTime.getTime(); - - metrics.endTime = endTime; - metrics.duration = duration; - metrics.status = 'ROLLED_BACK'; - metrics.error = error instanceof Error ? error.message : String(error); - - this.logger.error(`Transaction ${transactionId} rolled back after ${duration}ms:`, error); - throw error; - } finally { - await queryRunner.release(); - this.logger.debug(`Transaction ${transactionId} resources released`); - } - } - - /** - * Execute operations with manual transaction control - * Useful for complex scenarios requiring custom logic - */ - async withTransaction(callback: (queryRunner: QueryRunner) => Promise): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - - try { - return await callback(queryRunner); - } finally { - await queryRunner.release(); - } - } - - /** - * Execute operations with isolation level - */ - async runWithIsolationLevel( - isolationLevel: 'READ UNCOMMITTED' | 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE', - operation: (manager: EntityManager) => Promise, - ): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - - try { - await queryRunner.query(`SET TRANSACTION ISOLATION LEVEL ${isolationLevel}`); - await queryRunner.startTransaction(); - - this.logger.debug(`Transaction started with isolation level: ${isolationLevel}`); - - const result = await operation(queryRunner.manager); - - await queryRunner.commitTransaction(); - this.logger.debug('Transaction committed successfully'); - - return result; - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error('Transaction rolled back due to error:', error); - throw error; - } finally { - await queryRunner.release(); - } - } - - /** - * Execute operations with retry logic - */ - async runWithRetry( - operation: (manager: EntityManager) => Promise, - maxRetries: number = 3, - retryDelay: number = 1000, - ): Promise { - let lastError: Error | undefined; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - return await this.runInTransaction(operation); - } catch (error) { - lastError = error as Error; - - // Check if error is retryable (deadlock, serialization failure, etc.) - if (this.isRetryableError(error) && attempt < maxRetries) { - this.logger.warn( - `Transaction failed (attempt ${attempt}/${maxRetries}), retrying in ${retryDelay}ms...`, - ); - await this.delay(retryDelay); - retryDelay *= 2; // Exponential backoff - } else { - throw error; - } - } - /** - * Execute operations with manual transaction control - * Useful for complex scenarios requiring custom logic - */ - async withTransaction(callback: (queryRunner: QueryRunner) => Promise): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - try { - return await callback(queryRunner); - } - finally { - await queryRunner.release(); - } - } - } - - /** - * Check if error is retryable - */ - private isRetryableError(error: unknown): boolean { - const retryableErrors = [ - 'deadlock', - 'serialization failure', - 'could not serialize', - 'lock timeout', - 'connection', - ]; - - const errorMessage = (error instanceof Error ? error.message : String(error)).toLowerCase(); - return retryableErrors.some((msg) => errorMessage.includes(msg)); - } - - /** - * Delay helper for retry logic - */ - private delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - /** - * Get current transaction manager if in transaction - */ - getCurrentManager(): EntityManager { - return this.dataSource.manager; - } - - /** - * Generate unique transaction ID - */ - private generateTransactionId(): string { - return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; - } - - /** - * Get transaction metrics - */ - getTransactionMetrics(): ITransactionMetrics[] { - return Array.from(this.activeTransactions.values()); - } - - /** - * Get active transactions count - */ - getActiveTransactionCount(): number { - return this.activeTransactions.size; - } - - /** - * Clear completed transactions - */ - clearCompletedTransactions(): void { - const now = new Date(); - for (const [id, metrics] of this.activeTransactions.entries()) { - if (metrics.endTime && now.getTime() - metrics.endTime.getTime() > 300000) { - // 5 minutes - this.activeTransactions.delete(id); - } - } -} diff --git a/src/common/database/transactional.decorator.ts b/src/common/database/transactional.decorator.ts deleted file mode 100644 index ce65f81e..00000000 --- a/src/common/database/transactional.decorator.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SetMetadata } from '@nestjs/common'; -import { TransactionService } from './transaction.service'; -export const TRANSACTIONAL_KEY = 'transactional'; - -export interface ITransactionalOptions { - isolationLevel?: 'READ UNCOMMITTED' | 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE'; - retry?: boolean; - maxRetries?: number; - retryDelay?: number; - timeout?: number; -} -/** - * Transactional decorator - * Wraps methods in database transactions with retry logic and error handling - */ -export const Transactional = (options: ITransactionalOptions = {}) => - function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor { - const originalMethod = descriptor.value; - descriptor.value = async function (...args: unknown[]) { - const transactionService: TransactionService = this.transactionService; - if (!transactionService) { - throw new Error(`TransactionService not injected in ${target.constructor.name}. ` + - 'Please inject it via constructor.'); - } - const operationName = `${target.constructor.name}.${propertyKey}`; - try { - return await transactionService.runWithRetry(async (_manager) => { - return await originalMethod.apply(this, args); - }, options.maxRetries ?? 3, options.retryDelay ?? 1000); - } - catch (error) { - console.error(`Transaction failed in ${operationName}:`, error); - throw error; - } - }; - // Attach metadata (useful for interceptors or future enhancements) - SetMetadata(TRANSACTIONAL_KEY, { - method: propertyKey, - options, - }); - return descriptor; -}; diff --git a/src/common/database/transactional.interceptor.ts b/src/common/database/transactional.interceptor.ts deleted file mode 100644 index a565aeb4..00000000 --- a/src/common/database/transactional.interceptor.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { Observable } from 'rxjs'; -import { TransactionService } from './transaction.service'; -import { TRANSACTIONAL_KEY, ITransactionalOptions } from './transactional.decorator'; - -/** - * Interceptor to automatically wrap methods in transactions - */ -@Injectable() -export class TransactionalInterceptor implements NestInterceptor { - private readonly logger = new Logger(TransactionalInterceptor.name); - - constructor( - private readonly reflector: Reflector, - private readonly transactionService: TransactionService, - ) {} - - /** - * Executes intercept. - * @param context The context. - * @param next The next. - * @returns The resulting observable. - */ - async intercept(context: ExecutionContext, next: CallHandler): Promise> { - const options = this.reflector.get( - TRANSACTIONAL_KEY, - context.getHandler(), - ); - - if (!options) { - return next.handle(); - } -} diff --git a/src/common/dto/pagination.dto.spec.ts b/src/common/dto/pagination.dto.spec.ts deleted file mode 100644 index 2a3df5e0..00000000 --- a/src/common/dto/pagination.dto.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import 'reflect-metadata'; -import { validateSync } from 'class-validator'; -import { PaginationQueryDto, CursorPaginationQueryDto } from './pagination.dto'; -import { APP_CONSTANTS } from '../constants/app.constants'; -describe('Pagination DTO validation', () => { - const { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } = APP_CONSTANTS; - it('uses the default page size when no limit is provided', () => { - const dto = new PaginationQueryDto(); - const errors = validateSync(dto); - expect(errors).toHaveLength(0); - expect(dto.limit).toBe(DEFAULT_PAGE_SIZE); - }); - it('accepts a limit equal to the maximum page size', () => { - const dto = new PaginationQueryDto(); - dto.limit = MAX_PAGE_SIZE; - const errors = validateSync(dto); - expect(errors).toHaveLength(0); - }); - it('rejects a limit greater than the maximum page size', () => { - const dto = new PaginationQueryDto(); - dto.limit = MAX_PAGE_SIZE + 1; - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].constraints).toHaveProperty('max'); - }); - it('validates cursor pagination limit against the same maximum', () => { - const dto = new CursorPaginationQueryDto(); - dto.limit = MAX_PAGE_SIZE + 1; - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].constraints).toHaveProperty('max'); - }); -}); diff --git a/src/common/examples/timeout-example.controller.ts b/src/common/examples/timeout-example.controller.ts deleted file mode 100644 index 47d0476b..00000000 --- a/src/common/examples/timeout-example.controller.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Controller, Get, Post, Body } from '@nestjs/common'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; -import { Timeout } from '../interceptors/timeout.interceptor'; - -/** - * Exposes timeout Example endpoints. - */ -@ApiTags('Timeout Examples') -@Controller('examples') -export class TimeoutExampleController { - /** - * Returns quick Response. - * @returns The operation result. - */ - @Get('quick') - @ApiOperation({ summary: 'Quick endpoint with custom timeout' }) - @Timeout(5000) // 5 seconds timeout - getQuickResponse(): { message: string } { - // This endpoint will timeout after 5 seconds - return { message: 'Quick response' }; - } - - /** - * Returns slow Response. - * @returns The operation result. - */ - @Get('slow') - @ApiOperation({ summary: 'Slow endpoint with longer timeout' }) - @Timeout(120000) // 2 minutes timeout - async getSlowResponse(): Promise<{ message: string }> { - // Simulate a slow operation - await new Promise((resolve) => setTimeout(resolve, 10000)); // 10 second delay - return { message: 'Slow response completed' }; - } - - /** - * Processes data. - * @param data The data to process. - * @returns The operation result. - */ - @Post('process') - @ApiOperation({ summary: 'Processing endpoint with custom timeout' }) - @Timeout(60000) // 1 minute timeout - async processData(@Body() _data: any): Promise<{ result: string }> { - // Simulate data processing - await new Promise((resolve) => setTimeout(resolve, 30000)); // 30 second processing - return { result: 'Data processed successfully' }; - } - - /** - * Returns default Timeout. - * @returns The operation result. - */ - @Get('default') - @ApiOperation({ summary: 'Endpoint using default timeout' }) - getDefaultTimeout(): { message: string; timeout: string } { - // This endpoint will use the default timeout from configuration - return { - message: 'Using default timeout', - timeout: 'Configured by REQUEST_TIMEOUT env var or default 30s', - }; - } -} diff --git a/src/common/examples/transaction-management.example.ts b/src/common/examples/transaction-management.example.ts deleted file mode 100644 index 45f1373b..00000000 --- a/src/common/examples/transaction-management.example.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Repository } from 'typeorm'; -import { TransactionService } from '../database/transaction.service'; -import { TransactionHelperService } from '../database/transaction-helper.service'; -// Mock entities for example -interface IUser { - id: string; - email: string; - password: string; - firstName: string; - lastName: string; - isEmailVerified: boolean; - emailVerificationToken?: string; - emailVerificationExpires?: Date; - lastLoginAt?: Date | null; - status?: string; - profileCompleted?: boolean; -} - -interface IPayment { - id: string; - userId: string; - amount: number; - status: string; - description: string; - createdAt: Date; - completedAt?: Date; -} -interface Invoice { - id: string; - userId: string; - paymentId: string; - amount: number; - status: string; - description: string; - createdAt: Date; - dueDate: Date; - paidAt?: Date; -} -/** - * Example service demonstrating transaction management - * Shows atomic operations for complex business logic - */ -@Injectable() -export class TransactionExampleService { - private readonly logger = new Logger(TransactionExampleService.name); - - constructor( - private readonly userRepository: Repository, - private readonly transactionService: TransactionService, - private readonly transactionHelper: TransactionHelperService, - private readonly paymentRepository: Repository, - private readonly invoiceRepository: Repository, - ) {} - - /** - * Example: Atomic user registration with email verification - * All operations succeed or fail together - */ - async registerUserWithVerification(userData: { - email: string; - password: string; - firstName: string; - lastName: string; - }): Promise<{ userId: string; verificationToken: string }> { - return await this.transactionService.runInTransaction(async (_manager) => { - // Create user - const user = await this.userRepository.save({ - email: userData.email, - password: userData.password, - firstName: userData.firstName, - lastName: userData.lastName, - isEmailVerified: false, - lastLoginAt: null, - }); - - // Generate verification token - const verificationToken = this.generateVerificationToken(); - const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); - - // Save verification token - await this.userRepository.update(user.id, { - emailVerificationToken: verificationToken, - emailVerificationExpires: expiresAt, - } as any); - - this.logger.log( - `User registered with ID: ${user.id}, verification token: ${verificationToken}`, - ); - - return { - userId: user.id, - verificationToken, - }; - }); - } - - /** - * Example: Payment with invoice creation - * Demonstrates atomic payment processing - */ - async processPaymentWithInvoice(paymentData: { - userId: string; - amount: number; - description: string; - }): Promise<{ paymentId: string; invoiceId: string }> { - return await this.transactionService.runWithRetry(async (_manager) => { - // Create payment record - const payment = await this.paymentRepository.save({ - userId: paymentData.userId, - amount: paymentData.amount, - status: 'PENDING' as any, - description: paymentData.description, - createdAt: new Date(), - }); - - // Create invoice - const invoice = await this.invoiceRepository.save({ - userId: paymentData.userId, - paymentId: payment.id, - amount: paymentData.amount, - status: 'PENDING' as any, - description: paymentData.description, - createdAt: new Date(), - dueDate: new Date(Date.now() + 30 * 24 * 60 * 1000), // 30 days - }); - - // Simulate payment processing - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // Update payment status - await this.paymentRepository.update(payment.id, { - status: 'COMPLETED' as any, - completedAt: new Date(), - } as any); - - // Update invoice status - await this.invoiceRepository.update(invoice.id, { - status: 'PAID' as any, - paidAt: new Date(), - } as any); - - this.logger.log(`Payment processed: ${payment.id}, Invoice: ${invoice.id}`); - - return { - paymentId: payment.id, - invoiceId: invoice.id, - }; - }); - } - - /** - * Example: Complex operation with conditional rollback - */ - async processWithConditionalRollback(userId: string, data: any): Promise { - return await this.transactionHelper.executeWithRollback([ - { - operation: async (_manager) => { - // Step 1: Update user - await this.userRepository.update(userId, { - lastLoginAt: new Date(), - status: 'ACTIVE', - } as any); - return { step: 'user_updated', userId }; - }, - rollback: async (_manager) => { - // Rollback user status - await this.userRepository.update(userId, { - lastLoginAt: null, - status: 'INACTIVE', - } as any); - this.logger.warn(`Rolled back user status for ${userId}`); - }, - }, - { - operation: async (_manager) => { - // Step 2: Create related record - const record = await this.someRecordRepository.save({ - userId, - data, - createdAt: new Date(), - }); - return { step: 'record_created', userId: record.id }; - }, - rollback: async (_manager) => { - // Rollback record creation - this.logger.warn('Rolled back record creation'); - }, - condition: () => Math.random() > 0.5, // 50% chance of success - }, - ]); - } - - /** - * Example: Savepoint usage for nested operations - */ - async complexNestedOperation(userId: string): Promise { - return await this.transactionService.runInTransaction(async (manager) => { - // Create savepoint for first operation - await this.transactionHelper.createSavepoint(manager, 'user_update'); - - try { - // Update user - await this.userRepository.update(userId, { - lastLoginAt: new Date(), - profileCompleted: true, - } as any); - - // Create savepoint for second operation - await this.transactionHelper.createSavepoint(manager, 'profile_setup'); - - // Second operation that might fail - if (Math.random() > 0.3) { - throw new Error('Profile setup failed'); - } - - this.logger.log('Profile setup completed successfully'); - } catch (error) { - // Rollback to first savepoint - await this.transactionHelper.rollbackToSavepoint(manager, 'user_update'); - this.logger.error('Profile setup failed, rolled back to user_update'); - throw error; - } - }); - } - - private generateVerificationToken(): string { - return Math.random().toString(36).substring(2, 15); - } - - // Mock repository for example - private get someRecordRepository(): Repository { - return this.userRepository as any; - } -} diff --git a/src/common/export/export.controller.ts b/src/common/export/export.controller.ts deleted file mode 100644 index 1a5a9034..00000000 --- a/src/common/export/export.controller.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - Controller, - Get, - Post, - Param, - Query, - Res, - UseGuards, - BadRequestException, - Logger, -} from '@nestjs/common'; -import { Response } from 'express'; -import { ExportService, ExportFormat } from '../export/export.service'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; -import { CurrentUser } from '../../auth/decorators/current-user.decorator'; - -@Controller('export') -@UseGuards(JwtAuthGuard) -export class ExportController { - private readonly logger = new Logger(ExportController.name); - - constructor(private readonly exportService: ExportService) {} - - /** - * Request user data export - */ - @Post('user-data') - async requestUserDataExport( - @CurrentUser() user: any, - @Query('format') format: ExportFormat = 'json', - ) { - const validFormats: ExportFormat[] = ['json', 'pdf', 'csv']; - if (!validFormats.includes(format)) { - throw new BadRequestException( - `Invalid format. Supported formats: ${validFormats.join(', ')}`, - ); - } - - this.logger.log(`User ${user.id} requested data export in ${format} format`); - return this.exportService.requestUserDataExport(user.id, format); - } - - /** - * Get user's export history - */ - @Get('history') - async getExportHistory(@CurrentUser() user: any) { - return this.exportService.getUserExportHistory(user.id); - } - - /** - * Download completed export file - */ - @Get('download/:exportId') - async downloadExportFile( - @CurrentUser() user: any, - @Param('exportId') exportId: string, - @Res() res: Response, - ) { - const exportFile = await this.exportService.getCompletedExportFile(user.id, exportId); - - res.setHeader('Content-Type', exportFile.mimeType); - res.setHeader('Content-Disposition', `attachment; filename="${exportFile.fileName}"`); - res.send(exportFile.content); - } - - /** - * Get available export formats - */ - @Get('formats') - getAvailableFormats() { - return { - formats: [ - { - name: 'json', - mimeType: 'application/json', - description: 'JSON format - structured data, machine-readable', - }, - { - name: 'csv', - mimeType: 'text/csv', - description: 'CSV format - spreadsheet compatible', - }, - { - name: 'pdf', - mimeType: 'application/pdf', - description: 'PDF format - readable document', - }, - ], - }; - } -} diff --git a/src/common/export/export.service.ts b/src/common/export/export.service.ts deleted file mode 100644 index 43535c63..00000000 --- a/src/common/export/export.service.ts +++ /dev/null @@ -1,567 +0,0 @@ -import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { InjectQueue, Process, Processor } from '@nestjs/bull'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, Repository, UpdateDateColumn, } from 'typeorm'; -import { Job, Queue } from 'bull'; -import { User } from '../../users/entities/user.entity'; -import { Enrollment } from '../../courses/entities/enrollment.entity'; -import { TIME } from '../constants/time.constants'; - -export type ExportFormat = 'json' | 'pdf' | 'csv'; - -export enum UserExportStatus { - PENDING = 'pending', - IN_PROGRESS = 'in_progress', - COMPLETED = 'completed', - FAILED = 'failed' -} - -/** - * Provides user Export History operations. - */ -@Entity('user_export_history') -@Index(['userId', 'createdAt']) -export class UserExportHistory { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'user_id' }) - @Index() - userId: string; - - @Column({ type: 'varchar' }) - format: ExportFormat; - - @Column({ - type: 'enum', - enum: UserExportStatus, - default: UserExportStatus.PENDING, - }) - @Index() - status: UserExportStatus; - - @Column({ name: 'file_name', nullable: true }) - fileName?: string; - - @Column({ name: 'mime_type', nullable: true }) - mimeType?: string; - - @Column({ name: 'file_content', type: 'text', nullable: true }) - fileContent?: string; - - @Column({ name: 'error_message', type: 'text', nullable: true }) - errorMessage?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - @Column({ name: 'completed_at', type: 'timestamp', nullable: true }) - completedAt?: Date; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; -} - -interface IExportJobData { - exportId: string; - userId: string; - format: ExportFormat; -} - -interface IPreparedExportData { - user: { - id: string; - @Column({ name: 'user_id' }) - @Index() - userId: string; - @Column({ type: 'varchar' }) - format: ExportFormat; - @Column({ - type: 'enum', - enum: UserExportStatus, - default: UserExportStatus.PENDING, - }) - @Index() - status: UserExportStatus; - @Column({ name: 'file_name', nullable: true }) - fileName?: string; - @Column({ name: 'mime_type', nullable: true }) - mimeType?: string; - @Column({ name: 'file_content', type: 'text', nullable: true }) - fileContent?: string; - @Column({ name: 'error_message', type: 'text', nullable: true }) - errorMessage?: string; - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - @Column({ name: 'completed_at', type: 'timestamp', nullable: true }) - completedAt?: Date; - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; -} - -/** - * Provides export operations. - */ -@Injectable() -export class ExportService { - private readonly logger = new Logger(ExportService.name); - private static readonly QUEUE_NAME = 'user-data-export'; - private static readonly JOB_NAME = 'generate-user-data-export'; - constructor( - @InjectRepository(User) - private readonly userRepository: Repository, - @InjectRepository(Enrollment) - private readonly enrollmentRepository: Repository, - @InjectRepository(UserExportHistory) - private readonly exportHistoryRepository: Repository, - @InjectQueue(ExportService.QUEUE_NAME) - private readonly exportQueue: Queue, - ) {} - - /** - * Executes request User Data Export. - * @param userId The user identifier. - * @param format The format. - * @returns The operation result. - */ - async requestUserDataExport(userId: string, format: ExportFormat = 'json') { - this.ensureValidFormat(format); - - const user = await this.userRepository.findOne({ where: { id: userId } }); - if (!user) { - throw new NotFoundException('User not found'); - } - - const exportRecord = this.exportHistoryRepository.create({ - userId, - format, - status: UserExportStatus.PENDING, - metadata: { - requestedAt: new Date().toISOString(), - }, - }); - - const savedRecord = await this.exportHistoryRepository.save(exportRecord); - - await this.exportQueue.add( - ExportService.JOB_NAME, - { - exportId: savedRecord.id, - userId, - format, - } as IExportJobData, - { - attempts: 3, - backoff: { - type: 'exponential', - delay: TIME.TWO_SECONDS_MS, - }, - removeOnComplete: 50, - removeOnFail: 50, - }, - ); - - return { - exportId: savedRecord.id, - status: savedRecord.status, - message: 'Export request accepted and queued for background processing', - }; - } - - /** - * Retrieves user Export History. - * @param userId The user identifier. - * @returns The operation result. - */ - async getUserExportHistory(userId: string) { - const history = await this.exportHistoryRepository.find({ - where: { userId }, - order: { createdAt: 'DESC' }, - }); - - return history.map((item) => ({ - id: item.id, - format: item.format, - status: item.status, - fileName: item.fileName, - mimeType: item.mimeType, - errorMessage: item.errorMessage, - metadata: item.metadata, - completedAt: item.completedAt, - createdAt: item.createdAt, - updatedAt: item.updatedAt, - })); - } - - /** - * Retrieves completed Export File. - * @param userId The user identifier. - * @param exportId The export identifier. - * @returns The resulting promise<{ - file name: string; - mime type: string; - content: buffer; - }>. - */ - async getCompletedExportFile( - userId: string, - exportId: string, - ): Promise<{ - fileName: string; - mimeType: string; - content: Buffer; - }> { - const record = await this.exportHistoryRepository.findOne({ - where: { id: exportId, userId }, - }); - - if (!record) { - throw new NotFoundException('Export record not found'); - } - async getCompletedExportFile(userId: string, exportId: string): Promise<{ - fileName: string; - mimeType: string; - content: Buffer; - }> { - const record = await this.exportHistoryRepository.findOne({ - where: { id: exportId, userId }, - }); - if (!record) { - throw new NotFoundException('Export record not found'); - } - if (record.status !== UserExportStatus.COMPLETED || !record.fileContent) { - throw new BadRequestException('Export is not ready yet'); - } - return { - fileName: record.fileName || `user-export-${record.id}.${record.format}`, - mimeType: record.mimeType || 'application/octet-stream', - content: Buffer.from(record.fileContent, 'base64'), - }; - } - - return { - fileName: record.fileName || `user-export-${record.id}.${record.format}`, - mimeType: record.mimeType || 'application/octet-stream', - content: Buffer.from(record.fileContent, 'base64'), - }; - } - - async processExportJob(jobData: IExportJobData): Promise { - const { exportId, userId, format } = jobData; - - const exportRecord = await this.exportHistoryRepository.findOne({ where: { id: exportId } }); - if (!exportRecord) { - this.logger.warn(`Export record not found: ${exportId}`); - return; - } - - try { - await this.exportHistoryRepository.update(exportId, { - status: UserExportStatus.IN_PROGRESS, - metadata: { - ...(exportRecord.metadata || {}), - startedAt: new Date().toISOString(), - }, - }); - - const exportData = await this.prepareExportData(userId); - const preparedFile = - format === 'pdf' - ? this.generatePdfExport(exportData) - : format === 'csv' - ? this.generateCsvExport(exportData) - : this.generateJsonExport(exportData); - - await this.exportHistoryRepository.update(exportId, { - status: UserExportStatus.COMPLETED, - fileName: preparedFile.fileName, - mimeType: preparedFile.mimeType, - fileContent: preparedFile.content.toString('base64'), - completedAt: new Date(), - metadata: { - ...(exportRecord.metadata || {}), - completedAt: new Date().toISOString(), - payloadBytes: preparedFile.content.length, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown export error'; - this.logger.error(`Export processing failed for ${exportId}`, message); - - await this.exportHistoryRepository.update(exportId, { - status: UserExportStatus.FAILED, - errorMessage: message, - metadata: { - ...(exportRecord.metadata || {}), - failedAt: new Date().toISOString(), - }, - }); - - throw error; - } - } - - private ensureValidFormat(format: string): asserts format is ExportFormat { - if (format !== 'json' && format !== 'pdf' && format !== 'csv') { - throw new BadRequestException('Unsupported export format. Supported formats: json, pdf, csv'); - } - } - - private async prepareExportData(userId: string): Promise { - const user = await this.userRepository.findOne({ where: { id: userId } }); - if (!user) { - throw new NotFoundException('User not found'); - } - - const enrollments = await this.enrollmentRepository.find({ - where: { userId }, - relations: ['course'], - order: { enrolledAt: 'DESC' }, - }); - - const completedCourses = enrollments.filter((item) => item.progress >= 100).length; - const totalProgress = enrollments.reduce((sum, item) => sum + Number(item.progress || 0), 0); - const averageProgress = enrollments.length > 0 ? totalProgress / enrollments.length : 0; - - return { - user: { - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - role: user.role, - status: user.status, - tenantId: user.tenantId, - isEmailVerified: user.isEmailVerified, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - lastLoginAt: user.lastLoginAt, - }, - courseProgress: enrollments.map((enrollment) => ({ - enrollmentId: enrollment.id, - courseId: enrollment.courseId, - courseTitle: enrollment.course?.title || 'Unknown course', - progress: Number(enrollment.progress || 0), - status: enrollment.status, - enrolledAt: enrollment.enrolledAt, - lastAccessedAt: enrollment.lastAccessedAt, - })), - exportMeta: { - generatedAt: new Date().toISOString(), - totalEnrollments: enrollments.length, - completedCourses, - averageProgress: Number(averageProgress.toFixed(2)), - }, - }; - } - - private generateJsonExport(data: IPreparedExportData): { - fileName: string; - mimeType: string; - content: Buffer; - } { - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - return { - fileName: `user-data-export-${data.user.id}-${timestamp}.json`, - mimeType: 'application/json', - content: Buffer.from(JSON.stringify(data, null, 2), 'utf8'), - }; - } - - private generateCsvExport(data: IPreparedExportData): { - fileName: string; - mimeType: string; - content: Buffer; - } { - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - - // CSV headers - const headers = [ - 'User ID', - 'Email', - 'First Name', - 'Last Name', - 'Role', - 'Status', - 'Email Verified', - 'Created At', - 'Course Title', - 'Progress (%)', - 'Enrollment Status', - 'Enrolled At', - ]; - - // Build CSV rows - const rows: string[][] = []; - - if (data.courseProgress.length === 0) { - // User with no enrollments - rows.push([ - data.user.id, - data.user.email, - data.user.firstName, - data.user.lastName, - data.user.role, - data.user.status, - String(data.user.isEmailVerified), - data.user.createdAt.toISOString(), - 'No enrollments', - '0', - 'N/A', - 'N/A', - ]); - } else { - // One row per enrollment - data.courseProgress.forEach((enrollment) => { - rows.push([ - data.user.id, - data.user.email, - data.user.firstName, - data.user.lastName, - data.user.role, - data.user.status, - String(data.user.isEmailVerified), - data.user.createdAt.toISOString(), - enrollment.courseTitle, - String(enrollment.progress), - enrollment.status, - enrollment.enrolledAt.toISOString(), - ]); - }); - } - - // Escape CSV fields - const escapeCsvField = (field: string): string => { - if (field.includes(',') || field.includes('"') || field.includes('\n')) { - return `"${field.replace(/"/g, '""')}"`; - } - return field; - }; - - // Build CSV content - const csvContent = [ - headers.join(','), - ...rows.map((row) => row.map(escapeCsvField).join(',')), - ].join('\n'); - - return { - fileName: `user-data-export-${data.user.id}-${timestamp}.csv`, - mimeType: 'text/csv', - content: Buffer.from(csvContent, 'utf8'), - }; - } - - private generatePdfExport(data: IPreparedExportData): { - fileName: string; - mimeType: string; - content: Buffer; - } { - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const lines = [ - 'TeachLink User Data Export', - '', - `Generated: ${data.exportMeta.generatedAt}`, - '', - `User: ${data.user.firstName} ${data.user.lastName} (${data.user.email})`, - `Role: ${data.user.role}`, - `Status: ${data.user.status}`, - `Email Verified: ${data.user.isEmailVerified ? 'Yes' : 'No'}`, - '', - `Total Enrollments: ${data.exportMeta.totalEnrollments}`, - `Completed Courses: ${data.exportMeta.completedCourses}`, - `Average Progress: ${data.exportMeta.averageProgress}%`, - '', - 'Course Progress', - ...data.courseProgress.map( - (item, index) => - `${index + 1}. ${item.courseTitle} | Progress: ${item.progress}% | Status: ${item.status}`, - ), - ]; - - return { - fileName: `user-data-export-${data.user.id}-${timestamp}.pdf`, - mimeType: 'application/pdf', - content: this.buildSimplePdf(lines), - }; - } - - // Lightweight PDF writer to avoid additional dependencies for a simple report export. - private buildSimplePdf(lines: string[]): Buffer { - const escapePdfText = (value: string): string => - value.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); - - const bodyLines: string[] = ['BT', '/F1 11 Tf', '50 780 Td', '14 TL']; - lines.forEach((line, index) => { - const command = index === 0 ? 'Tj' : 'T*'; - bodyLines.push(`(${escapePdfText(line)}) ${command}`); - }); - bodyLines.push('ET'); - - const stream = bodyLines.join('\n'); - - const object1 = '1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n'; - const object2 = '2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n'; - const object3 = - '3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n'; - const object4 = '4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n'; - const object5 = `5 0 obj\n<< /Length ${Buffer.byteLength(stream, 'utf8')} >>\nstream\n${stream}\nendstream\nendobj\n`; - - const header = '%PDF-1.4\n'; - - const offset1 = header.length; - const offset2 = offset1 + object1.length; - const offset3 = offset2 + object2.length; - const offset4 = offset3 + object3.length; - const offset5 = offset4 + object4.length; - - const body = object1 + object2 + object3 + object4 + object5; - const xrefOffset = header.length + body.length; - - const xref = [ - 'xref', - '0 6', - '0000000000 65535 f ', - `${offset1.toString().padStart(10, '0')} 00000 n `, - `${offset2.toString().padStart(10, '0')} 00000 n `, - `${offset3.toString().padStart(10, '0')} 00000 n `, - `${offset4.toString().padStart(10, '0')} 00000 n `, - `${offset5.toString().padStart(10, '0')} 00000 n `, - 'trailer', - '<< /Size 6 /Root 1 0 R >>', - 'startxref', - `${xrefOffset}`, - '%%EOF', - ].join('\n'); - - return Buffer.from(header + body + xref, 'utf8'); - } -} - -/** - * Provides user Data Export operations. - */ -@Processor('user-data-export') -export class UserDataExportProcessor { - private readonly logger = new Logger(UserDataExportProcessor.name); - - constructor(private readonly exportService: ExportService) {} - - /** - * Handles generate User Data Export. - * @param job The job. - */ - @Process('generate-user-data-export') - async handleGenerateUserDataExport(job: Job): Promise { - this.logger.log(`Processing user export job: ${job.id}`); - await job.progress(20); - - await this.exportService.processExportJob(job.data); - - await job.progress(100); - } -} diff --git a/src/common/guards/roles.guard.spec.ts b/src/common/guards/roles.guard.spec.ts deleted file mode 100644 index ea680589..00000000 --- a/src/common/guards/roles.guard.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { RolesGuard } from '../guards/roles.guard'; -import { Roles } from '../decorators/roles.decorator'; -describe('RolesGuard', () => { - let guard: RolesGuard; - let reflector: unknown; - beforeEach(() => { - reflector = { - getAllAndOverride: jest.fn(), - }; - guard = new RolesGuard(reflector); - }); - it('should allow access if no roles are required', () => { - reflector.getAllAndOverride.mockReturnValue(undefined); - const context = { - getHandler: () => { }, - getClass: () => { }, - switchToHttp: () => ({ getRequest: () => ({ user: { roles: ['admin'] } }) }), - }; - expect(guard.canActivate(context as unknown)).toBe(true); - }); - it('should deny access if user has no roles', () => { - reflector.getAllAndOverride.mockReturnValue(['admin']); - const context = { - getHandler: () => { }, - getClass: () => { }, - switchToHttp: () => ({ getRequest: () => ({ user: {} }) }), - }; - expect(guard.canActivate(context as unknown)).toBe(false); - }); - it('should allow access if user has required role', () => { - reflector.getAllAndOverride.mockReturnValue(['admin']); - const context = { - getHandler: () => { }, - getClass: () => { }, - switchToHttp: () => ({ getRequest: () => ({ user: { roles: ['admin', 'moderator'] } }) }), - }; - expect(guard.canActivate(context as unknown)).toBe(true); - }); - it('should deny access if user does not have required role', () => { - reflector.getAllAndOverride.mockReturnValue(['admin']); - const context = { - getHandler: () => { }, - getClass: () => { }, - switchToHttp: () => ({ getRequest: () => ({ user: { roles: ['user'] } }) }), - }; - expect(guard.canActivate(context as unknown)).toBe(false); - }); - it('should support multiple roles per endpoint', () => { - reflector.getAllAndOverride.mockReturnValue(['admin', 'moderator']); - const context = { - getHandler: () => { }, - getClass: () => { }, - switchToHttp: () => ({ getRequest: () => ({ user: { roles: ['moderator'] } }) }), - }; - expect(guard.canActivate(context as unknown)).toBe(true); - }); -}); diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts deleted file mode 100644 index df26afdb..00000000 --- a/src/common/guards/roles.guard.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { ROLES_KEY } from '../decorators/roles.decorator'; - -/** - * Protects roles execution paths. - */ -@Injectable() -export class RolesGuard implements CanActivate { - constructor(private reflector: Reflector) {} - - /** - * Executes can Activate. - * @param context The context. - * @returns Whether the operation succeeded. - */ - canActivate(context: ExecutionContext): boolean { - const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ - context.getHandler(), - context.getClass(), - ]); - if (!requiredRoles || requiredRoles.length === 0) { - return true; - } -} diff --git a/src/common/guards/ws-throttler.guard.ts b/src/common/guards/ws-throttler.guard.ts deleted file mode 100644 index eb3806f5..00000000 --- a/src/common/guards/ws-throttler.guard.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable, ExecutionContext, Logger } from '@nestjs/common'; -import { ThrottlerGuard, ThrottlerLimitDetail } from '@nestjs/throttler'; -import { WsException } from '@nestjs/websockets'; - -/** - * Protects ws Throttler execution paths. - */ -@Injectable() -export class WsThrottlerGuard extends ThrottlerGuard { - private readonly wsLogger = new Logger(WsThrottlerGuard.name); - protected async getTracker(req: Record): Promise { - const user = req.user; - const ip = req.conn?.remoteAddress || req.request?.connection?.remoteAddress || 'unknown'; - return user?.sub || user?.id || ip; - } - protected async throwThrottlingException(context: ExecutionContext, _throttlerLimitDetail: ThrottlerLimitDetail): Promise { - const client = context.switchToWs().getClient(); - const tracker = await this.getTracker(client); - this.wsLogger.warn(`WebSocket rate limit exceeded for ${tracker}`); - throw new WsException('Rate limit exceeded'); - } -} diff --git a/src/common/interceptors/api-deprecation.interceptor.ts b/src/common/interceptors/api-deprecation.interceptor.ts deleted file mode 100644 index e4407c7c..00000000 --- a/src/common/interceptors/api-deprecation.interceptor.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common'; -import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { - API_VERSION_HEADER, - DEFAULT_API_VERSION, - SUPPORTED_API_VERSIONS, - normalizeRequestedApiVersion, - isVersionNeutralPath, -} from '../interceptors/api-version.interceptor'; - -export interface IDeprecationConfig { - deprecatedVersions: string[]; - sunsetDate?: Date; - migrationGuide?: string; -} - -export const DEPRECATION_CONFIG: IDeprecationConfig = { - deprecatedVersions: [], // Add versions here when they become deprecated - sunsetDate: undefined, - migrationGuide: 'https://docs.teachlink.com/api-versioning', -}; - -@Injectable() -export class ApiDeprecationInterceptor implements NestInterceptor { - private readonly logger = new Logger(ApiDeprecationInterceptor.name); - - intercept(context: ExecutionContext, next: CallHandler): Observable { - const http = context.switchToHttp(); - const request = http.getRequest(); - const response = http.getResponse(); - - const path = request.path || request.url || '/'; - - // Skip version-neutral paths - if (isVersionNeutralPath(path)) { - return next.handle(); - } - - // Get requested API version - const requestedVersion = - request.apiVersion || - normalizeRequestedApiVersion(request.headers?.[API_VERSION_HEADER.toLowerCase()]) || - DEFAULT_API_VERSION; - - // Check if version is deprecated - if (DEPRECATION_CONFIG.deprecatedVersions.includes(requestedVersion)) { - const deprecationMessage = this.buildDeprecationNotice(requestedVersion); - - // Set deprecation headers - response.setHeader('Deprecation', 'true'); - response.setHeader('Sunset', DEPRECATION_CONFIG.sunsetDate?.toISOString() || ''); - response.setHeader('Link', `<${DEPRECATION_CONFIG.migrationGuide}>; rel="deprecation"`); - response.setHeader( - 'Warning', - `299 - "API version ${requestedVersion} is deprecated. ${deprecationMessage}"`, - ); - - this.logger.warn(`Deprecated API version ${requestedVersion} accessed from ${request.ip}`); - } - - // Add API version to response headers - response.setHeader(API_VERSION_HEADER, requestedVersion); - response.setHeader('X-Supported-Versions', SUPPORTED_API_VERSIONS.join(', ')); - - return next.handle().pipe( - tap(() => { - // Additional post-processing if needed - }), - ); - } - - private buildDeprecationNotice(version: string): string { - const notices: Record = { - '1': `API version ${version} is deprecated. Please migrate to version ${DEFAULT_API_VERSION} or later.`, - }; - - return notices[version] || `API version ${version} is deprecated.`; - } -} diff --git a/src/common/interceptors/api-version.interceptor.spec.ts b/src/common/interceptors/api-version.interceptor.spec.ts deleted file mode 100644 index 79c7acf0..00000000 --- a/src/common/interceptors/api-version.interceptor.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { isVersionNeutralPath, normalizeRequestedApiVersion, parseSupportedApiVersions, } from './api-version.interceptor'; -describe('api version helpers', () => { - it('normalizes supported version formats', () => { - expect(normalizeRequestedApiVersion('1')).toBe('1'); - expect(normalizeRequestedApiVersion('v1')).toBe('1'); - expect(normalizeRequestedApiVersion('1.0')).toBe('1'); - expect(normalizeRequestedApiVersion('v1.0')).toBe('1'); - }); - it('rejects invalid version formats', () => { - expect(normalizeRequestedApiVersion('latest')).toBeNull(); - expect(normalizeRequestedApiVersion('v1.2')).toBeNull(); - expect(normalizeRequestedApiVersion(undefined)).toBeNull(); - }); - it('parses configured supported versions', () => { - expect(parseSupportedApiVersions('1, v1, 2')).toEqual(['1', '2']); - }); - it('detects version-neutral routes', () => { - expect(isVersionNeutralPath('/')).toBe(true); - expect(isVersionNeutralPath('/health')).toBe(true); - expect(isVersionNeutralPath('/metrics/scheduled-tasks/dashboard')).toBe(true); - expect(isVersionNeutralPath('/webhooks/stripe')).toBe(true); - expect(isVersionNeutralPath('/users')).toBe(false); - }); -}); diff --git a/src/common/interceptors/api-version.interceptor.ts b/src/common/interceptors/api-version.interceptor.ts deleted file mode 100644 index 66477cbb..00000000 --- a/src/common/interceptors/api-version.interceptor.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { - CallHandler, - createParamDecorator, - ExecutionContext, - Injectable, - Logger, - NestInterceptor, -} from '@nestjs/common'; -import { Observable, tap } from 'rxjs'; - -export const API_VERSION_HEADER = process.env.API_VERSION_HEADER_NAME?.trim() || 'X-API-Version'; -export const API_VERSION_HEADER_KEY = API_VERSION_HEADER.toLowerCase(); -const VERSION_NEUTRAL_PATH_PREFIXES = ['/api', '/health', '/metrics', '/webhooks']; -const VERSION_NEUTRAL_EXACT_PATHS = ['/', '/api-json', '/favicon.ico']; - -export interface IVersionedRequest { - apiVersion?: string; - path?: string; - url?: string; - headers?: Record; -} -export function normalizeRequestedApiVersion(version?: string | string[]): string | null { - if (!version) { - return null; - } - const raw = Array.isArray(version) ? version[0] : version; - const trimmed = raw.trim(); - const match = trimmed.match(/^v?(\d+)(?:\.0+)?$/i); - if (!match) { - return null; - } - return match[1]; -} - -/** - * Normalizes a configured API version string. - * @param version The configured version value. - * @returns The normalized major version. - */ -export function normalizeConfiguredVersion(version: string): string { - const normalized = normalizeRequestedApiVersion(version); - return normalized || '1'; -} -export const DEFAULT_API_VERSION = normalizeConfiguredVersion(process.env.API_DEFAULT_VERSION?.trim() || '1'); -export function parseSupportedApiVersions(raw = process.env.API_SUPPORTED_VERSIONS): string[] { - const configured = raw?.trim() ? raw : DEFAULT_API_VERSION; - const versions = configured - .split(',') - .map((version) => normalizeRequestedApiVersion(version)) - .filter((version): version is string => Boolean(version)); - if (!versions.length) { - return [DEFAULT_API_VERSION]; - } - return Array.from(new Set(versions)); -} -export const SUPPORTED_API_VERSIONS = parseSupportedApiVersions(process.env.API_SUPPORTED_VERSIONS); -export function isVersionNeutralPath(pathOrUrl: string): boolean { - const path = (pathOrUrl || '/').split('?')[0]; - if (VERSION_NEUTRAL_EXACT_PATHS.includes(path)) { - return true; - } - return VERSION_NEUTRAL_PATH_PREFIXES.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)); -} -@Injectable() -export class ApiVersionInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { - const http = context.switchToHttp(); - const request = http.getRequest(); - const response = http.getResponse<{ setHeader: (name: string, value: string) => void }>(); - - const path = request.path || request.url || '/'; - - const resolvedVersion = - request.apiVersion || - normalizeRequestedApiVersion(request.headers?.[API_VERSION_HEADER_KEY]) || - DEFAULT_API_VERSION; - - request.apiVersion = resolvedVersion; - - if (!isVersionNeutralPath(path)) { - response.setHeader(API_VERSION_HEADER, resolvedVersion); - } -} -export function GetApiVersion(): ParameterDecorator { - return (_target: object, _propertyKey: string | symbol, _parameterIndex: number): void => { - // Intentionally a marker decorator for future injection. - }; -} diff --git a/src/common/interceptors/cache.interceptor.ts b/src/common/interceptors/cache.interceptor.ts deleted file mode 100644 index eebd7b0a..00000000 --- a/src/common/interceptors/cache.interceptor.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; -import { Cache } from 'cache-manager'; -import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; - -/** - * Intercepts cache request handling. - */ -@Injectable() -export class CacheInterceptor implements NestInterceptor { - constructor(private cacheManager: Cache) {} - - /** - * Executes intercept. - * @param context The context. - * @param next The next. - * @returns The resulting observable. - */ - async intercept(context: ExecutionContext, next: CallHandler): Promise> { - const req = context.switchToHttp().getRequest(); - const res = context.switchToHttp().getResponse(); - const key = req.originalUrl; - - // Only cache GET requests - if (req.method !== 'GET') { - // Invalidate cache for mutations - if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { - await this.cacheManager.del(key); - } - return next.handle(); - } -} diff --git a/src/common/interceptors/global-exception.filter.spec.ts b/src/common/interceptors/global-exception.filter.spec.ts deleted file mode 100644 index ba1dd5d7..00000000 --- a/src/common/interceptors/global-exception.filter.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { GlobalExceptionFilter } from './global-exception.filter'; -import { HttpStatus } from '@nestjs/common'; -import { MulterError } from 'multer'; -import { runWithCorrelationId } from '../utils/correlation.utils'; -describe('GlobalExceptionFilter', () => { - it('adds correlation ID to error response and header', () => { - const filter = new GlobalExceptionFilter(); - const req: unknown = { method: 'GET', url: '/test' }; - const responseHeaders: Record = {}; - const res: unknown = { - status: (code: number) => { - res.statusCode = code; - return res; - }, - json: (body: unknown) => { - res.body = body; - return res; - }, - setHeader: (name: string, value: string) => { - responseHeaders[name.toLowerCase()] = value; - }, - getHeader: (name: string) => responseHeaders[name.toLowerCase()], - }; - runWithCorrelationId(() => { - filter.catch(new Error('Test error'), { - switchToHttp: () => ({ getRequest: () => req, getResponse: () => res }), - } as unknown); - }, 'cid-123'); - const body = res.body; - expect(res.statusCode).toBe(HttpStatus.INTERNAL_SERVER_ERROR); - expect(body.correlationId).toBe('cid-123'); - expect(body.message).toBe('Test error'); - }); - it('maps Multer file size errors to payload too large', () => { - const filter = new GlobalExceptionFilter(); - const req: unknown = { method: 'POST', url: '/media/upload' }; - const res: unknown = { - status: (code: number) => { - res.statusCode = code; - return res; - }, - json: (body: unknown) => { - res.body = body; - return res; - }, - setHeader: jest.fn(), - }; - filter.catch(new MulterError('LIMIT_FILE_SIZE', 'file'), { - switchToHttp: () => ({ getRequest: () => req, getResponse: () => res }), - } as unknown); - expect(res.statusCode).toBe(HttpStatus.PAYLOAD_TOO_LARGE); - expect(res.body.message).toBe('Uploaded file exceeds the maximum allowed size.'); - }); -}); diff --git a/src/common/interceptors/global-exception.filter.ts b/src/common/interceptors/global-exception.filter.ts deleted file mode 100644 index 548d5117..00000000 --- a/src/common/interceptors/global-exception.filter.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger, } from '@nestjs/common'; -import { Request, Response } from 'express'; -import { MulterError } from 'multer'; -import { QueryFailedError, EntityNotFoundError, OptimisticLockVersionMismatchError } from 'typeorm'; -import { IApiError, IValidationErrorDetail } from '../../interfaces/api-error.interface'; -import { CORRELATION_ID_HEADER, getCorrelationId } from '../utils/correlation.utils'; - -/** - * Provides global Exception Filter behavior. - */ -@Catch() -export class GlobalExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(GlobalExceptionFilter.name); - private readonly isProduction = process.env.NODE_ENV === 'production'; - - /** - * Executes catch. - * @param exception The exception. - * @param host The host. - */ - catch(exception: unknown, host: ArgumentsHost): void { - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); - - const { statusCode, message, error, details, stack } = this.resolveException(exception); - - const correlationId = getCorrelationId(); - - const errorResponse: IApiError = { - statusCode, - message, - error, - timestamp: new Date().toISOString(), - path: request.url, - correlationId, - ...(details?.length && { details }), - ...(!this.isProduction && stack && { stack }), - }; - - if (correlationId) { - response.setHeader(CORRELATION_ID_HEADER, correlationId); - } - - this.logger.error( - `[${request.method}] ${request.url} → ${statusCode} ${error}: ${ - Array.isArray(message) ? message.join(', ') : message - }`, - !this.isProduction ? stack : undefined, - GlobalExceptionFilter.name, - ); - - response.status(statusCode).json(errorResponse); - } - - // ─── Resolution helpers ──────────────────────────────────────────────────── - - private resolveException(exception: unknown): { - statusCode: number; - message: string | string[]; - error: string; - details?: IValidationErrorDetail[]; - stack?: string; - } { - // 1. NestJS HttpException (includes class-validator BadRequestException) - if (exception instanceof HttpException) { - return this.fromHttpException(exception); - } - - // 2. TypeORM – query/constraint failures - if (exception instanceof QueryFailedError) { - return this.fromQueryFailedError(exception); - } - - // 3. Multer upload failures - if (exception instanceof MulterError) { - return this.fromMulterError(exception); - } - - // 4. TypeORM – entity not found - if (exception instanceof EntityNotFoundError) { - return { - statusCode: HttpStatus.NOT_FOUND, - message: 'The requested resource was not found.', - error: 'Not Found', - stack: (exception as Error).stack, - }; - } - - // 4b. TypeORM - Optimistic Locking Conflict - if (exception instanceof OptimisticLockVersionMismatchError) { - return { - statusCode: HttpStatus.CONFLICT, - message: 'The resource was modified by another request. Please refresh and try again.', - error: 'Conflict', - stack: (exception as Error).stack, - }; - } - - // 5. Generic / unexpected Error - if (exception instanceof Error) { - return { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, - message: this.isProduction - ? 'An unexpected error occurred. Please try again later.' - : exception.message, - error: 'Internal Server Error', - stack: exception.stack, - }; - } - - // 6. Non-Error throw (strings, objects, etc.) - return { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, - message: 'An unexpected error occurred.', - error: 'Internal Server Error', - }; - } - - private fromHttpException(exception: HttpException): { - statusCode: number; - message: string | string[]; - error: string; - details?: IValidationErrorDetail[]; - stack?: string; - } { - const statusCode = exception.getStatus(); - const exceptionResponse = exception.getResponse(); - const stack = exception.stack; - - // class-validator wraps errors as { message: string[], error: string } - if (typeof exceptionResponse === 'object' && exceptionResponse !== null) { - const res = exceptionResponse as Record; - - const rawMessages = res['message']; - const messages: string[] = Array.isArray(rawMessages) - ? (rawMessages as string[]) - : typeof rawMessages === 'string' - ? [rawMessages] - : [exception.message]; - - // Parse class-validator constraint objects when present - const details = this.extractValidationDetails(rawMessages); - - return { - statusCode, - message: messages.length === 1 ? messages[0] : messages, - error: (res['error'] as string) ?? exception.message, - ...(details.length && { details }), - stack, - }; - } - - return { - statusCode, - message: typeof exceptionResponse === 'string' ? exceptionResponse : exception.message, - error: exception.message, - stack, - }; - } - - private fromQueryFailedError(exception: QueryFailedError): { - statusCode: number; - message: string; - error: string; - stack?: string; - } { - const driverError = (exception as QueryFailedError & { code?: string }).code; - - // PostgreSQL unique-violation - if (driverError === '23505') { - return { - statusCode: HttpStatus.CONFLICT, - message: 'A record with the provided value already exists.', - error: 'Conflict', - stack: exception.stack, - }; - } - - // PostgreSQL foreign-key violation - if (driverError === '23503') { - return { - statusCode: HttpStatus.UNPROCESSABLE_ENTITY, - message: 'Referenced resource does not exist.', - error: 'Unprocessable Entity', - stack: exception.stack, - }; - } - - // Generic DB error – never expose query details in production - return { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, - message: this.isProduction ? 'A database error occurred.' : exception.message, - error: 'Database Error', - stack: exception.stack, - }; - } - - private fromMulterError(exception: MulterError): { - statusCode: number; - message: string; - error: string; - stack?: string; - } { - switch (exception.code) { - case 'LIMIT_FILE_SIZE': - return { - statusCode: HttpStatus.PAYLOAD_TOO_LARGE, - message: 'Uploaded file exceeds the maximum allowed size.', - error: 'Payload Too Large', - stack: exception.stack, - }; - if (correlationId) { - response.setHeader(CORRELATION_ID_HEADER, correlationId); - } - this.logger.error(`[${request.method}] ${request.url} → ${statusCode} ${error}: ${Array.isArray(message) ? message.join(', ') : message}`, !this.isProduction ? stack : undefined, GlobalExceptionFilter.name); - response.status(statusCode).json(errorResponse); - } - // ─── Resolution helpers ──────────────────────────────────────────────────── - private resolveException(exception: unknown): { - statusCode: number; - message: string | string[]; - error: string; - details?: ValidationErrorDetail[]; - stack?: string; - } { - // 1. NestJS HttpException (includes class-validator BadRequestException) - if (exception instanceof HttpException) { - return this.fromHttpException(exception); - } - // 2. TypeORM – query/constraint failures - if (exception instanceof QueryFailedError) { - return this.fromQueryFailedError(exception); - } - // 3. Multer upload failures - if (exception instanceof MulterError) { - return this.fromMulterError(exception); - } - // 4. TypeORM – entity not found - if (exception instanceof EntityNotFoundError) { - return { - statusCode: HttpStatus.NOT_FOUND, - message: 'The requested resource was not found.', - error: 'Not Found', - stack: (exception as Error).stack, - }; - } - // 5. Generic / unexpected Error - if (exception instanceof Error) { - return { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, - message: this.isProduction - ? 'An unexpected error occurred. Please try again later.' - : exception.message, - error: 'Internal Server Error', - stack: exception.stack, - }; - } - // 6. Non-Error throw (strings, objects, etc.) - return { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, - message: 'An unexpected error occurred.', - error: 'Internal Server Error', - }; - } - } - - /** - * Converts class-validator nested error objects into structured details when - * the raw message array contains constraint objects rather than plain strings. - */ - private extractValidationDetails(raw: unknown): IValidationErrorDetail[] { - if (!Array.isArray(raw)) return []; - - return raw.reduce((acc, item) => { - if ( - typeof item === 'object' && - item !== null && - 'property' in item && - 'constraints' in item - ) { - acc.push({ - property: (item as { property: string }).property, - constraints: (item as { constraints: Record }).constraints, - }); - } - return acc; - }, []); - } -} diff --git a/src/common/interceptors/logging.interceptor.spec.ts b/src/common/interceptors/logging.interceptor.spec.ts deleted file mode 100644 index 06be7e2e..00000000 --- a/src/common/interceptors/logging.interceptor.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { LoggingInterceptor } from './logging.interceptor'; -import { LogShipperService } from '../services/log-shipper.service'; -import { of, firstValueFrom } from 'rxjs'; -describe('LoggingInterceptor', () => { - it('attaches and propagates correlation ID header', async () => { - const mockShipper = { ship: jest.fn() } as unknown as LogShipperService; - const interceptor = new LoggingInterceptor(mockShipper); - - const req: any = { method: 'GET', url: '/spam', headers: {} }; - const headers: Record = {}; - const res: any = { - statusCode: 200, - setHeader: (name: string, value: string) => { - headers[name.toLowerCase()] = value; - }, - getHeader: (name: string) => headers[name.toLowerCase()], - }; - - const context: any = { - getType: () => 'http', - switchToHttp: () => ({ getRequest: () => req, getResponse: () => res }), - }; - - const next: any = { - handle: () => of({ success: true }), - }; - - await firstValueFrom(interceptor.intercept(context, next)); - - const correlationId = res.getHeader('x-request-id'); - expect(typeof correlationId).toBe('string'); - expect(correlationId).toMatch(/^cid-/); - }); -}); diff --git a/src/common/interceptors/logging.interceptor.ts b/src/common/interceptors/logging.interceptor.ts deleted file mode 100644 index 69cee5aa..00000000 --- a/src/common/interceptors/logging.interceptor.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common'; -import { Observable, throwError } from 'rxjs'; -import { tap, catchError } from 'rxjs/operators'; -import { Request, Response } from 'express'; -import { - CORRELATION_ID_HEADER, - generateCorrelationId, - getCorrelationId, -} from '../utils/correlation.utils'; -import { LogShipperService } from '../services/log-shipper.service'; - -/** Standard log-level labels used in the JSON envelope. */ -export type LogLevel = 'info' | 'warn' | 'error'; - -/** - * Standard log envelope emitted for every HTTP request/response. - * - * All fields are present in every log entry so consumers can build - * consistent Elasticsearch mappings and Kibana dashboards without - * per-environment special-casing. - */ -export interface IRequestLog { - '@timestamp': string; - service: string; - environment: string; - level: LogLevel; - event: string; - correlationId: string; - method: string; - url: string; - route: string; - ip: string; - userAgent: string; - userId?: string | number; - userRole?: string; -} - -export interface IResponseLog extends IRequestLog { - statusCode: number; - responseTimeMs: number; - contentLength?: number; -} -/** - * #154 / #360 – LoggingInterceptor - * - * Logs every HTTP request with a standardized JSON envelope: - * - @timestamp, service, environment, level, event - * - correlationId (propagated via AsyncLocalStorage / x-request-id header) - * - method, URL, resolved route, IP, user-agent - * - authenticated user ID + role (when present on request.user) - * - response status code and wall-clock response time - * - * JSON format is used in all environments for consistent parsing by - * log shippers and aggregators (#360). - */ -@Injectable() -export class LoggingInterceptor implements NestInterceptor { - private readonly logger = new Logger(LoggingInterceptor.name); - private readonly service = process.env.npm_package_name ?? 'teachlink-api'; - private readonly environment = process.env.NODE_ENV ?? 'development'; - - constructor(private readonly logShipper: LogShipperService) {} - - /** - * Executes intercept. - * @param context The context. - * @param next The next. - * @returns The resulting observable. - */ - intercept(context: ExecutionContext, next: CallHandler): Observable { - // Only intercept HTTP contexts (skip WebSockets, microservices, etc.) - if (context.getType() !== 'http') { - return next.handle(); - } - // ─── Private helpers ─────────────────────────────────────────────────────── - private logIncoming(log: RequestLog): void { - const message = `→ ${log.method} ${log.url}`; - if (this.isProd) { - this.logger.log(JSON.stringify({ event: 'request.incoming', ...log })); - } - else { - this.logger.log(`${message} | id=${log.requestId} ip=${log.ip}${log.userId ? ` user=${log.userId}` : ''}`); - } - } - - const startTime = Date.now(); - const correlationId = getCorrelationId() || generateCorrelationId(); - - const response = httpCtx.getResponse(); - response?.setHeader(CORRELATION_ID_HEADER, correlationId); - - const baseLog: IRequestLog = { - '@timestamp': new Date().toISOString(), - service: this.service, - environment: this.environment, - level: 'info', - event: 'request.incoming', - correlationId, - method: request.method ?? 'UNKNOWN', - url: request.url, - route: request.route?.path ?? request.url, - ip: this.resolveClientIp(request), - userAgent: (request.headers['user-agent'] as string) ?? 'unknown', - ...(request.user?.id !== undefined && { userId: request.user.id as string | number }), - ...(request.user?.role !== undefined && { userRole: request.user.role as string }), - }; - - this.emit('log', baseLog); - - return next.handle().pipe( - tap(() => { - const res = httpCtx.getResponse(); - const outgoing: IResponseLog = { - ...baseLog, - event: 'request.completed', - statusCode: res.statusCode, - responseTimeMs: Date.now() - startTime, - contentLength: this.getContentLength(res), - }; - outgoing.level = this.resolveLevel(res.statusCode); - this.emit( - outgoing.level === 'error' ? 'error' : outgoing.level === 'warn' ? 'warn' : 'log', - outgoing, - ); - }), - catchError((error: unknown) => { - const status = - typeof error === 'object' && error !== null && 'status' in error - ? (error as { status: number }).status - : 500; - - const outgoing: IResponseLog = { - ...baseLog, - event: 'request.completed', - level: this.resolveLevel(status), - statusCode: status, - responseTimeMs: Date.now() - startTime, - }; - this.emit( - outgoing.level === 'error' ? 'error' : outgoing.level === 'warn' ? 'warn' : 'log', - outgoing, - ); - - return throwError(() => error); - }), - ); - } - - // ─── Private helpers ─────────────────────────────────────────────────────── - - /** Emit a structured JSON log entry and ship it to the external aggregator. */ - private emit(nestLevel: 'log' | 'warn' | 'error', entry: IRequestLog | IResponseLog): void { - this.logger[nestLevel](JSON.stringify(entry)); - this.logShipper.ship(entry); - } - - private resolveLevel(statusCode: number): LogLevel { - if (statusCode >= 500) return 'error'; - if (statusCode >= 400) return 'warn'; - return 'info'; - } - - private resolveClientIp(request: Request): string { - const forwarded = request.headers['x-forwarded-for']; - if (typeof forwarded === 'string') { - return forwarded.split(',')[0].trim(); - } -} diff --git a/src/common/interceptors/monitoring.interceptor.ts b/src/common/interceptors/monitoring.interceptor.ts deleted file mode 100644 index a750ad36..00000000 --- a/src/common/interceptors/monitoring.interceptor.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; -import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { MetricsCollectionService } from '../../monitoring/metrics/metrics-collection.service'; - -/** - * Intercepts monitoring request handling. - */ -@Injectable() -export class MonitoringInterceptor implements NestInterceptor { - constructor(private readonly metricsService: MetricsCollectionService) {} - - /** - * Executes intercept. - * @param context The context. - * @param next The next. - * @returns The resulting observable. - */ - intercept(context: ExecutionContext, next: CallHandler): Observable { - const now = Date.now(); - const httpContext = context.switchToHttp(); - const request = httpContext.getRequest(); - - // Some requests might not be HTTP (e.g. WebSocket or Microservice), check if request exists - if (!request) { - return next.handle(); - } -} diff --git a/src/common/interceptors/response-transform.interceptor.ts b/src/common/interceptors/response-transform.interceptor.ts deleted file mode 100644 index 46ee4e55..00000000 --- a/src/common/interceptors/response-transform.interceptor.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { Response } from 'express'; -export interface ApiResponse { - success: boolean; - message?: string; - data: T; - metadata?: Record; -} - -/** - * Intercepts response Transform request handling. - */ -@Injectable() -export class ResponseTransformInterceptor implements NestInterceptor> { - /** - * Executes intercept. - * @param context The context. - * @param next The next. - * @returns The resulting observable>. - */ - intercept(context: ExecutionContext, next: CallHandler): Observable> { - const ctx = context.switchToHttp(); - const response = ctx.getResponse(); - - // Exclude file/stream responses (Content-Type or underlying stream) - const contentType = response.getHeader('Content-Type'); - if ( - response.headersSent || - (contentType && - (contentType.toString().includes('octet-stream') || - contentType.toString().includes('application/pdf') || - contentType.toString().startsWith('image/') || - contentType.toString().startsWith('audio/') || - contentType.toString().startsWith('video/'))) - ) { - // Return as Observable> by casting, since we skip transformation - return next.handle() as unknown as Observable>; - } - - return next.handle().pipe( - map((data: any) => { - // Allow controllers to return { data, message, metadata } for custom messages/metadata - let message: string | undefined; - let metadata: Record | undefined; - let responseData = data; - if (data && typeof data === 'object' && !Array.isArray(data)) { - if ('data' in data && typeof data.data !== 'undefined') { - responseData = data.data; - message = data.message; - metadata = data.metadata; - } - } - return next.handle().pipe(map((data: unknown) => { - // Allow controllers to return { data, message, metadata } for custom messages/metadata - let message: string | undefined; - let metadata: Record | undefined; - let responseData = data; - if (data && typeof data === 'object' && !Array.isArray(data)) { - if ('data' in data && typeof data.data !== 'undefined') { - responseData = data.data; - message = data.message; - metadata = data.metadata; - } - } - return { - success: true, - message, - data: responseData, - metadata, - }; - })); - } -} diff --git a/src/common/interceptors/timeout.interceptor.ts b/src/common/interceptors/timeout.interceptor.ts deleted file mode 100644 index 2ecbe7fc..00000000 --- a/src/common/interceptors/timeout.interceptor.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler, BadGatewayException, Logger, Inject, } from '@nestjs/common'; -import { Observable, TimeoutError } from 'rxjs'; -import { timeout, catchError } from 'rxjs/operators'; -import { TimeoutConfigService } from '../timeout/timeout-config.service'; -export const DEFAULT_TIMEOUT = parseInt(process.env.REQUEST_TIMEOUT || '30000', 10); // 30 seconds default - -/** - * Executes timeout. - * @param ms The ms. - * @returns The resulting method decorator. - */ -export function Timeout(ms?: number): MethodDecorator { - return (target, propertyKey, descriptor) => { - Reflect.defineMetadata('timeout', ms, descriptor.value ?? target); - }; -} - -/** - * Intercepts timeout request handling. - */ -@Injectable() -export class TimeoutInterceptor implements NestInterceptor { - private readonly logger = new Logger(TimeoutInterceptor.name); - - constructor(@Inject(TimeoutConfigService) private timeoutConfig: TimeoutConfigService) {} - - /** - * Executes intercept. - * @param context The context. - * @param next The next. - * @returns The resulting observable. - */ - intercept(context: ExecutionContext, next: CallHandler): Observable { - const handler = context.getHandler(); - const customTimeout = Reflect.getMetadata('timeout', handler); - - const request = context.switchToHttp().getRequest(); - const method = request.method; - const url = request.url; - - // Determine timeout value with priority: decorator > config service > default - let timeoutValue: number; - if (customTimeout) { - timeoutValue = customTimeout; - this.logger.debug(`Using decorator timeout of ${timeoutValue}ms for ${method} ${url}`); - } else { - timeoutValue = this.timeoutConfig.getTimeoutForRequest(method, url); - this.logger.debug(`Using config timeout of ${timeoutValue}ms for ${method} ${url}`); - } - - return next.handle().pipe( - timeout(timeoutValue), - catchError((err) => { - if (err instanceof TimeoutError) { - this.logger.warn(`Request timeout: ${method} ${url} after ${timeoutValue}ms`); - throw new BadGatewayException({ - statusCode: 504, - message: `Request timed out after ${timeoutValue}ms`, - error: 'Timeout', - timestamp: new Date().toISOString(), - path: url, - method, - }); - } - else { - timeoutValue = this.timeoutConfig.getTimeoutForRequest(method, url); - this.logger.debug(`Using config timeout of ${timeoutValue}ms for ${method} ${url}`); - } - return next.handle().pipe(timeout(timeoutValue), catchError((err) => { - if (err instanceof TimeoutError) { - this.logger.warn(`Request timeout: ${method} ${url} after ${timeoutValue}ms`); - throw new BadGatewayException({ - statusCode: 504, - message: `Request timed out after ${timeoutValue}ms`, - error: 'Timeout', - timestamp: new Date().toISOString(), - path: url, - method, - }); - } - throw err; - })); - } -} diff --git a/src/common/lazy-loading/index.ts b/src/common/lazy-loading/index.ts deleted file mode 100644 index 03b3b621..00000000 --- a/src/common/lazy-loading/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './lazy-module-loader.service'; -export * from './lazy-loading.module'; -export * from './startup-logger.service'; diff --git a/src/common/lazy-loading/lazy-loading.module.ts b/src/common/lazy-loading/lazy-loading.module.ts deleted file mode 100644 index 5e54421b..00000000 --- a/src/common/lazy-loading/lazy-loading.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Module, Global, DynamicModule } from '@nestjs/common'; -import { LazyModuleLoader } from './lazy-module-loader.service'; - -/** - * Registers the lazy Loading module. - */ -@Global() -@Module({ - providers: [LazyModuleLoader], - exports: [LazyModuleLoader], -}) -export class LazyLoadingModule { - /** - * Creates the root application module. - * @returns The resulting dynamic module. - */ - static forRoot(): DynamicModule { - return { - module: LazyLoadingModule, - providers: [LazyModuleLoader], - exports: [LazyModuleLoader], - global: true, - }; - } -} diff --git a/src/common/lazy-loading/lazy-module-loader.service.ts b/src/common/lazy-loading/lazy-module-loader.service.ts deleted file mode 100644 index 0ff6d38f..00000000 --- a/src/common/lazy-loading/lazy-module-loader.service.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { Injectable, Logger, DynamicModule } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; - -export interface ILazyModuleOptions { - moduleName: string; - featureFlag?: string; - dependencies?: string[]; -} - -export interface IModuleLoadResult { - moduleName: string; - loadedAt: Date; - loadTimeMs: number; - success: boolean; - error?: Error; -} - -/** - * Provides lazy Module Loader operations. - */ -@Injectable() -export class LazyModuleLoader { - private readonly logger = new Logger(LazyModuleLoader.name); - private loadedModules = new Map>(); - private moduleLoadResults = new Map(); - private moduleRegistry = new Map Promise>(); - - constructor(private readonly moduleRef: ModuleRef) {} - - /** - * Register a lazy-loadable module - */ - register(moduleName: string, factory: () => Promise): void { - if (this.moduleRegistry.has(moduleName)) { - this.logger.warn(`Module ${moduleName} is already registered`); - return; - } - /** - * Load a module on-demand - */ - async load(moduleName: string): Promise { - // Check if already loaded - if (this.loadedModules.has(moduleName)) { - this.logger.debug(`Module ${moduleName} is already loaded`); - const loadedModule = this.loadedModules.get(moduleName); - if (loadedModule) { - return loadedModule; - } - } - // Check if registered - const factory = this.moduleRegistry.get(moduleName); - if (!factory) { - this.logger.error(`Module ${moduleName} is not registered for lazy loading`); - return null; - } - // Load the module - const startTime = Date.now(); - this.logger.log(`Loading module: ${moduleName}...`); - const loadPromise = this.loadModuleInternal(moduleName, factory, startTime); - this.loadedModules.set(moduleName, loadPromise); - return loadPromise; - } - /** - * Load multiple modules - */ - async loadMany(moduleNames: string[]): Promise { - const results = await Promise.all(moduleNames.map((name) => this.load(name))); - return results.filter((m): m is DynamicModule => m !== null); - } - - // Load the module - const startTime = Date.now(); - this.logger.log(`Loading module: ${moduleName}...`); - - const loadPromise = this.loadModuleInternal(moduleName, factory, startTime); - this.loadedModules.set(moduleName, loadPromise); - - return loadPromise; - } - - /** - * Load multiple modules - */ - async loadMany(moduleNames: string[]): Promise { - const results = await Promise.all(moduleNames.map((name) => this.load(name))); - return results.filter((m): m is DynamicModule => m !== null); - } - - /** - * Check if a module is loaded - */ - isLoaded(moduleName: string): boolean { - return this.loadedModules.has(moduleName); - } - - /** - * Check if a module is registered - */ - isRegistered(moduleName: string): boolean { - return this.moduleRegistry.has(moduleName); - } - - /** - * Get list of loaded module names - */ - getLoadedModules(): string[] { - return Array.from(this.loadedModules.keys()); - } - - /** - * Get list of registered module names - */ - getRegisteredModules(): string[] { - return Array.from(this.moduleRegistry.keys()); - } - - /** - * Get load result for a module - */ - getLoadResult(moduleName: string): IModuleLoadResult | undefined { - return this.moduleLoadResults.get(moduleName); - } - - /** - * Get all load results - */ - getAllLoadResults(): IModuleLoadResult[] { - return Array.from(this.moduleLoadResults.values()); - } - - /** - * Get total load time for all loaded modules - */ - getTotalLoadTime(): number { - let total = 0; - for (const result of this.moduleLoadResults.values()) { - if (result.success) { - total += result.loadTimeMs; - } - } - /** - * Check if a module is registered - */ - isRegistered(moduleName: string): boolean { - return this.moduleRegistry.has(moduleName); - } - - this.loadedModules.delete(moduleName); - this.moduleLoadResults.delete(moduleName); - this.logger.log(`Unloaded module: ${moduleName}`); - return true; - } - - /** - * Preload modules that are likely to be needed - */ - async preload(moduleNames: string[]): Promise { - this.logger.log(`Preloading ${moduleNames.length} modules...`); - const startTime = Date.now(); - - await this.loadMany(moduleNames); - - const duration = Date.now() - startTime; - this.logger.log(`Preloaded ${moduleNames.length} modules in ${duration}ms`); - } - - /** - * Generate a report of all module loading activity - */ - generateReport(): { - registered: number; - loaded: number; - totalLoadTime: number; - averageLoadTime: number; - modules: IModuleLoadResult[]; - } { - const results = this.getAllLoadResults(); - const successfulLoads = results.filter((r) => r.success); - const totalLoadTime = this.getTotalLoadTime(); - - return { - registered: this.moduleRegistry.size, - loaded: this.loadedModules.size, - totalLoadTime, - averageLoadTime: successfulLoads.length > 0 ? totalLoadTime / successfulLoads.length : 0, - modules: results, - }; - } - - private async loadModuleInternal( - moduleName: string, - factory: () => Promise, - startTime: number, - ): Promise { - try { - const module = await factory(); - const loadTimeMs = Date.now() - startTime; - - const result: IModuleLoadResult = { - moduleName, - loadedAt: new Date(), - loadTimeMs, - success: true, - }; - - this.moduleLoadResults.set(moduleName, result); - this.logger.log(`Module ${moduleName} loaded in ${loadTimeMs}ms`); - - return module; - } catch (error) { - const loadTimeMs = Date.now() - startTime; - - const result: IModuleLoadResult = { - moduleName, - loadedAt: new Date(), - loadTimeMs, - success: false, - error: error as Error, - }; - - this.moduleLoadResults.set(moduleName, result); - this.logger.error( - `Failed to load module ${moduleName} after ${loadTimeMs}ms`, - (error as Error).message, - ); - - throw error; - } -} diff --git a/src/common/lazy-loading/startup-logger.service.ts b/src/common/lazy-loading/startup-logger.service.ts deleted file mode 100644 index 41d159ab..00000000 --- a/src/common/lazy-loading/startup-logger.service.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { Injectable, Logger, OnModuleInit, OnApplicationBootstrap } from '@nestjs/common'; - -export interface IStartupMetrics { - bootstrapStartTime: number; - bootstrapEndTime: number; - totalStartupTimeMs: number; - moduleInitTimeMs: number; - modulesLoaded: string[]; - modulesSkipped: string[]; - memoryUsage: NodeJS.MemoryUsage; -} - -export interface IModuleLoadMetric { - moduleName: string; - startTime: number; - endTime: number; - durationMs: number; - dependencies: string[]; -} - -/** - * Provides startup Logger operations. - */ -@Injectable() -export class StartupLogger implements OnModuleInit, OnApplicationBootstrap { - private readonly logger = new Logger(StartupLogger.name); - private bootstrapStartTime: number = 0; - private moduleInitStartTime: number = 0; - private moduleMetrics: Map = new Map(); - private modulesLoaded: string[] = []; - private modulesSkipped: string[] = []; - - constructor() { - this.bootstrapStartTime = Date.now(); - this.moduleInitStartTime = this.bootstrapStartTime; - } - - /** - * Executes on Module Init. - * @returns The operation result. - */ - onModuleInit() { - this.moduleInitStartTime = Date.now(); - this.logger.log('Module initialization started'); - } - - /** - * Executes on Application Bootstrap. - * @returns The operation result. - */ - onApplicationBootstrap() { - const metrics = this.generateMetrics(); - this.logStartupReport(metrics); - } - - /** - * Record a module being loaded - */ - recordModuleLoaded(moduleName: string, startTime: number, dependencies: string[] = []): void { - const endTime = Date.now(); - const durationMs = endTime - startTime; - - this.moduleMetrics.set(moduleName, { - moduleName, - startTime, - endTime, - durationMs, - dependencies, - }); - - this.modulesLoaded.push(moduleName); - this.logger.debug(`Module ${moduleName} loaded in ${durationMs}ms`); - } - - /** - * Record a module being skipped (feature flag disabled) - */ - recordModuleSkipped(moduleName: string, reason: string): void { - this.modulesSkipped.push(moduleName); - this.logger.debug(`Module ${moduleName} skipped: ${reason}`); - } - - /** - * Get all recorded metrics - */ - getMetrics(): IStartupMetrics { - return this.generateMetrics(); - } - - /** - * Get module load metrics - */ - getModuleMetrics(): IModuleLoadMetric[] { - return Array.from(this.moduleMetrics.values()); - } - - /** - * Get slowest loading modules - */ - getSlowestModules(limit: number = 5): IModuleLoadMetric[] { - return this.getModuleMetrics() - .sort((a, b) => b.durationMs - a.durationMs) - .slice(0, limit); - } - - /** - * Get total startup time - */ - getTotalStartupTime(): number { - return Date.now() - this.bootstrapStartTime; - } - - /** - * Generate a complete startup report - */ - private generateMetrics(): IStartupMetrics { - const now = Date.now(); - - return { - bootstrapStartTime: this.bootstrapStartTime, - bootstrapEndTime: now, - totalStartupTimeMs: now - this.bootstrapStartTime, - moduleInitTimeMs: now - this.moduleInitStartTime, - modulesLoaded: this.modulesLoaded, - modulesSkipped: this.modulesSkipped, - memoryUsage: process.memoryUsage(), - }; - } - - /** - * Log startup report - */ - private logStartupReport(metrics: IStartupMetrics): void { - const totalModules = metrics.modulesLoaded.length + metrics.modulesSkipped.length; - const loadedCount = metrics.modulesLoaded.length; - const skippedCount = metrics.modulesSkipped.length; - - this.logger.log('========================================'); - this.logger.log(' STARTUP REPORT'); - this.logger.log('========================================'); - this.logger.log(`Total Startup Time: ${metrics.totalStartupTimeMs}ms`); - this.logger.log(`Module Init Time: ${metrics.moduleInitTimeMs}ms`); - this.logger.log(`Modules Loaded: ${loadedCount}/${totalModules}`); - this.logger.log(`Modules Skipped: ${skippedCount}/${totalModules}`); - - if (skippedCount > 0) { - this.logger.log(`Skipped Modules: ${metrics.modulesSkipped.join(', ')}`); - } - onModuleInit() { - this.moduleInitStartTime = Date.now(); - this.logger.log('Module initialization started'); - } - onApplicationBootstrap() { - const metrics = this.generateMetrics(); - this.logStartupReport(metrics); - } - /** - * Record a module being loaded - */ - recordModuleLoaded(moduleName: string, startTime: number, dependencies: string[] = []): void { - const endTime = Date.now(); - const durationMs = endTime - startTime; - this.moduleMetrics.set(moduleName, { - moduleName, - startTime, - endTime, - durationMs, - dependencies, - }); - this.modulesLoaded.push(moduleName); - this.logger.debug(`Module ${moduleName} loaded in ${durationMs}ms`); - } - /** - * Record a module being skipped (feature flag disabled) - */ - recordModuleSkipped(moduleName: string, reason: string): void { - this.modulesSkipped.push(moduleName); - this.logger.debug(`Module ${moduleName} skipped: ${reason}`); - } - /** - * Get all recorded metrics - */ - getMetrics(): StartupMetrics { - return this.generateMetrics(); - } - /** - * Get module load metrics - */ - getModuleMetrics(): ModuleLoadMetric[] { - return Array.from(this.moduleMetrics.values()); - } - /** - * Get slowest loading modules - */ - getSlowestModules(limit: number = 5): ModuleLoadMetric[] { - return this.getModuleMetrics() - .sort((a, b) => b.durationMs - a.durationMs) - .slice(0, limit); - } - /** - * Get total startup time - */ - getTotalStartupTime(): number { - return Date.now() - this.bootstrapStartTime; - } - /** - * Generate a complete startup report - */ - private generateMetrics(): StartupMetrics { - const now = Date.now(); - return { - bootstrapStartTime: this.bootstrapStartTime, - bootstrapEndTime: now, - totalStartupTimeMs: now - this.bootstrapStartTime, - moduleInitTimeMs: now - this.moduleInitStartTime, - modulesLoaded: this.modulesLoaded, - modulesSkipped: this.modulesSkipped, - memoryUsage: process.memoryUsage(), - }; - } - /** - * Log startup report - */ - private logStartupReport(metrics: StartupMetrics): void { - const totalModules = metrics.modulesLoaded.length + metrics.modulesSkipped.length; - const loadedCount = metrics.modulesLoaded.length; - const skippedCount = metrics.modulesSkipped.length; - this.logger.log('========================================'); - this.logger.log(' STARTUP REPORT'); - this.logger.log('========================================'); - this.logger.log(`Total Startup Time: ${metrics.totalStartupTimeMs}ms`); - this.logger.log(`Module Init Time: ${metrics.moduleInitTimeMs}ms`); - this.logger.log(`Modules Loaded: ${loadedCount}/${totalModules}`); - this.logger.log(`Modules Skipped: ${skippedCount}/${totalModules}`); - if (skippedCount > 0) { - this.logger.log(`Skipped Modules: ${metrics.modulesSkipped.join(', ')}`); - } - // Memory usage - const memoryMB = Math.round(metrics.memoryUsage.heapUsed / 1024 / 1024); - this.logger.log(`Memory Usage: ${memoryMB}MB`); - // Slowest modules - const slowest = this.getSlowestModules(3); - if (slowest.length > 0) { - this.logger.log('Slowest Modules:'); - slowest.forEach((m) => { - this.logger.log(` - ${m.moduleName}: ${m.durationMs}ms`); - }); - } - this.logger.log('========================================'); - // Performance warning if startup is slow - if (metrics.totalStartupTimeMs > 10000) { - this.logger.warn('Startup time exceeds 10 seconds - consider optimizing module loading'); - } - } - /** - * Generate JSON report for external monitoring - */ - generateJSONReport(): Record { - const metrics = this.generateMetrics(); - return { - timestamp: new Date().toISOString(), - startup: { - totalTimeMs: metrics.totalStartupTimeMs, - moduleInitTimeMs: metrics.moduleInitTimeMs, - }, - modules: { - total: metrics.modulesLoaded.length + metrics.modulesSkipped.length, - loaded: metrics.modulesLoaded.length, - skipped: metrics.modulesSkipped.length, - loadedList: metrics.modulesLoaded, - skippedList: metrics.modulesSkipped, - }, - memory: { - heapUsed: Math.round(metrics.memoryUsage.heapUsed / 1024 / 1024), - heapTotal: Math.round(metrics.memoryUsage.heapTotal / 1024 / 1024), - rss: Math.round(metrics.memoryUsage.rss / 1024 / 1024), - external: Math.round(metrics.memoryUsage.external / 1024 / 1024), - }, - moduleMetrics: this.getModuleMetrics().map((m) => ({ - name: m.moduleName, - loadTimeMs: m.durationMs, - dependencies: m.dependencies, - })), - }; - } -} diff --git a/src/common/middleware/csrf.middleware.ts b/src/common/middleware/csrf.middleware.ts deleted file mode 100644 index 4d387c63..00000000 --- a/src/common/middleware/csrf.middleware.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { ConfigService } from '@nestjs/config'; -import { CsrfService } from '../csrf/csrf.service'; - -/** - * Applies csrf middleware behavior. - */ -@Injectable() -export class CsrfMiddleware implements NestMiddleware { - constructor( - private csrfService: CsrfService, - private configService: ConfigService, - ) {} - - /** - * Executes use. - * @param req The req. - * @param res The res. - * @param next The next. - */ - use(req: Request, res: Response, next: NextFunction): void { - // Skip CSRF for GET, HEAD, OPTIONS requests - if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { - this.generateCsrfToken(req, res); - return next(); - } - private generateCsrfToken(req: Request, res: Response): void { - const sessionId = this.getSessionId(req); - const existingToken = this.csrfService.getToken(sessionId); - if (existingToken) { - res.setHeader('X-CSRF-Token', existingToken); - (req as unknown).csrfToken = existingToken; - return; - } - // Generate new token - const token = this.csrfService.generateToken(sessionId); - res.setHeader('X-CSRF-Token', token); - (req as unknown).csrfToken = token; - } - private validateCsrfToken(req: Request): void { - const sessionId = this.getSessionId(req); - const tokenFromHeader = req.headers['x-csrf-token'] as string; - const tokenFromBody = req.body?._csrf; - const submittedToken = tokenFromHeader || tokenFromBody; - if (!submittedToken || !this.csrfService.validateToken(sessionId, submittedToken)) { - throw new UnauthorizedException('Invalid CSRF token'); - } - } - private getSessionId(req: Request): string { - // Try to get session ID from session - if ((req as unknown).session?.id) { - return (req as unknown).session.id; - } - // Fallback to IP address (less secure, but better than nothing) - return req.ip || req.connection.remoteAddress || 'unknown'; - } -} diff --git a/src/common/modules/api-versioning.module.ts b/src/common/modules/api-versioning.module.ts deleted file mode 100644 index c4d3ed55..00000000 --- a/src/common/modules/api-versioning.module.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { BadRequestException, Injectable, MiddlewareConsumer, Module, NestMiddleware, NestModule, NotAcceptableException, RequestMethod, } from '@nestjs/common'; -import { APP_INTERCEPTOR } from '@nestjs/core'; -import type { NextFunction, Request, Response } from 'express'; -import { - ApiVersionInterceptor, - API_VERSION_HEADER, - API_VERSION_HEADER_KEY, - DEFAULT_API_VERSION, - isVersionNeutralPath, - normalizeRequestedApiVersion, - SUPPORTED_API_VERSIONS, - IVersionedRequest, -} from '../interceptors/api-version.interceptor'; - -export const API_VERSIONING_DOCUMENTATION = [ - 'TeachLink uses header-based API versioning.', - `Send ${API_VERSION_HEADER}: ${DEFAULT_API_VERSION} on versioned endpoints.`, - `Supported versions: ${SUPPORTED_API_VERSIONS.join(', ')}.`, - 'Health, metrics, root, and webhook endpoints are version-neutral.', -].join(' '); - -/** - * Registers the api Version Validation module. - */ -@Injectable() -export class ApiVersionValidationMiddleware implements NestMiddleware { - /** - * Executes use. - * @param req The req. - * @param res The res. - * @param next The next. - */ - use( - req: Request & IVersionedRequest & { headers: Record }, - res: Response, - next: NextFunction, - ): void { - const path = req.path || req.url || '/'; - - if (isVersionNeutralPath(path)) { - req.apiVersion = DEFAULT_API_VERSION; - next(); - return; - } -} - -/** - * Registers the api Versioning module. - */ -@Module({ - providers: [ - ApiVersionValidationMiddleware, - ApiVersionInterceptor, - { - provide: APP_INTERCEPTOR, - useClass: ApiVersionInterceptor, - }, - ], - exports: [ApiVersionValidationMiddleware, ApiVersionInterceptor], -}) -export class ApiVersioningModule implements NestModule { - /** - * Executes configure. - * @param consumer The consumer. - */ - configure(consumer: MiddlewareConsumer): void { - consumer.apply(ApiVersionValidationMiddleware).forRoutes({ - path: '*', - method: RequestMethod.ALL, - }); - } -} diff --git a/src/common/services/idempotency.service.spec.ts b/src/common/services/idempotency.service.spec.ts deleted file mode 100644 index e778d367..00000000 --- a/src/common/services/idempotency.service.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigService } from '@nestjs/config'; -import { IdempotencyService } from './idempotency.service'; - -describe('IdempotencyService', () => { - let service: IdempotencyService; - let configService: ConfigService; - - const mockConfigService = { - get: jest.fn((key: string, defaultValue?: any) => { - const config = { - REDIS_HOST: 'localhost', - REDIS_PORT: 6379, - REDIS_PASSWORD: undefined, - IDEMPOTENCY_TTL_SECONDS: 86400, - }; - return config[key] || defaultValue; - }), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - IdempotencyService, - { - provide: ConfigService, - useValue: mockConfigService, - }, - ], - }).compile(); - - service = module.get(IdempotencyService); - configService = module.get(ConfigService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('generateKey', () => { - it('should generate a unique key based on user, endpoint, and payload', () => { - const userId = 'user123'; - const endpoint = '/payments/create-intent'; - const payload = { amount: 100, courseId: 'course456' }; - - const key1 = service.generateKey(userId, endpoint, payload); - const key2 = service.generateKey(userId, endpoint, payload); - - expect(key1).toBe(key2); - expect(typeof key1).toBe('string'); - expect(key1.length).toBe(64); // SHA-256 hash length - }); - - it('should generate different keys for different payloads', () => { - const userId = 'user123'; - const endpoint = '/payments/create-intent'; - const payload1 = { amount: 100 }; - const payload2 = { amount: 200 }; - - const key1 = service.generateKey(userId, endpoint, payload1); - const key2 = service.generateKey(userId, endpoint, payload2); - - expect(key1).not.toBe(key2); - }); - - it('should generate different keys for different users', () => { - const endpoint = '/payments/create-intent'; - const payload = { amount: 100 }; - - const key1 = service.generateKey('user1', endpoint, payload); - const key2 = service.generateKey('user2', endpoint, payload); - - expect(key1).not.toBe(key2); - }); - }); - - describe('cleanup', () => { - it('should not throw error when cleaning up', async () => { - // This would require mocking Redis - // For now, we just ensure the method exists - expect(service.cleanup).toBeDefined(); - }); - }); -}); diff --git a/src/common/timeout/timeout-config.service.ts b/src/common/timeout/timeout-config.service.ts deleted file mode 100644 index 1963df27..00000000 --- a/src/common/timeout/timeout-config.service.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -export interface ITimeoutConfig { - default: number; - endpoints: Record; - methods: Record; -} - -/** - * Provides timeout Config operations. - */ -@Injectable() -export class TimeoutConfigService { - private readonly config: ITimeoutConfig; - - constructor(private configService: ConfigService) { - this.config = this.loadConfig(); - } - - private loadConfig(): ITimeoutConfig { - return { - default: parseInt(process.env.REQUEST_TIMEOUT || '30000', 10), // 30 seconds - endpoints: { - // API endpoints with custom timeouts - '/auth/login': 10000, // 10 seconds for login - '/auth/register': 15000, // 15 seconds for registration - '/payments/create-payment-intent': 20000, // 20 seconds for payment processing - '/media/upload': 120000, // 2 minutes for file upload - '/backup/create': 300000, // 5 minutes for backup creation - '/search': 15000, // 15 seconds for search - '/email-marketing/campaigns/send': 60000, // 1 minute for campaign sending - }, - methods: { - // HTTP method-specific timeouts - GET: 30000, // 30 seconds for GET requests - POST: 60000, // 1 minute for POST requests - PUT: 45000, // 45 seconds for PUT requests - DELETE: 30000, // 30 seconds for DELETE requests - PATCH: 45000, // 45 seconds for PATCH requests - }, - }; - } - - /** - * Retrieves default Timeout. - * @returns The calculated numeric value. - */ - getDefaultTimeout(): number { - return this.config.default; - } - - /** - * Retrieves endpoint Timeout. - * @param path The path. - * @returns The operation result. - */ - getEndpointTimeout(path: string): number | null { - // Check for exact path match - if (this.config.endpoints[path]) { - return this.config.endpoints[path]; - } - private loadConfig(): TimeoutConfig { - return { - default: parseInt(process.env.REQUEST_TIMEOUT || '30000', 10), // 30 seconds - endpoints: { - // API endpoints with custom timeouts - '/auth/login': 10000, // 10 seconds for login - '/auth/register': 15000, // 15 seconds for registration - '/payments/create-payment-intent': 20000, // 20 seconds for payment processing - '/media/upload': 120000, // 2 minutes for file upload - '/backup/create': 300000, // 5 minutes for backup creation - '/search': 15000, // 15 seconds for search - '/email-marketing/campaigns/send': 60000, // 1 minute for campaign sending - }, - methods: { - // HTTP method-specific timeouts - GET: 30000, // 30 seconds for GET requests - POST: 60000, // 1 minute for POST requests - PUT: 45000, // 45 seconds for PUT requests - DELETE: 30000, // 30 seconds for DELETE requests - PATCH: 45000, // 45 seconds for PATCH requests - }, - }; - } - - return null; - } - - /** - * Retrieves method Timeout. - * @param method The method. - * @returns The operation result. - */ - getMethodTimeout(method: string): number | null { - return this.config.methods[method.toUpperCase()] || null; - } - - /** - * Retrieves timeout For Request. - * @param method The method. - * @param path The path. - * @returns The calculated numeric value. - */ - getTimeoutForRequest(method: string, path: string): number { - // Priority: endpoint > method > default - const endpointTimeout = this.getEndpointTimeout(path); - if (endpointTimeout) { - return endpointTimeout; - } - getEndpointTimeout(path: string): number | null { - // Check for exact path match - if (this.config.endpoints[path]) { - return this.config.endpoints[path]; - } - // Check for pattern matches - for (const [pattern, timeout] of Object.entries(this.config.endpoints)) { - if (this.matchesPattern(path, pattern)) { - return timeout; - } - } - return null; - } - getMethodTimeout(method: string): number | null { - return this.config.methods[method.toUpperCase()] || null; - } - getTimeoutForRequest(method: string, path: string): number { - // Priority: endpoint > method > default - const endpointTimeout = this.getEndpointTimeout(path); - if (endpointTimeout) { - return endpointTimeout; - } - const methodTimeout = this.getMethodTimeout(method); - if (methodTimeout) { - return methodTimeout; - } - return this.getDefaultTimeout(); - } - private matchesPattern(path: string, pattern: string): boolean { - // Simple pattern matching - can be enhanced with regex - if (pattern.includes('*')) { - const regexPattern = pattern.replace(/\*/g, '.*'); - return new RegExp(`^${regexPattern}$`).test(path); - } - return false; - } - updateEndpointTimeout(path: string, timeout: number): void { - this.config.endpoints[path] = timeout; - } - updateMethodTimeout(method: string, timeout: number): void { - this.config.methods[method.toUpperCase()] = timeout; - } - updateDefaultTimeout(timeout: number): void { - this.config.default = timeout; - } - getConfig(): TimeoutConfig { - return { ...this.config }; - } - return false; - } - - /** - * Updates endpoint Timeout. - * @param path The path. - * @param timeout The timeout. - */ - updateEndpointTimeout(path: string, timeout: number): void { - this.config.endpoints[path] = timeout; - } - - /** - * Updates method Timeout. - * @param method The method. - * @param timeout The timeout. - */ - updateMethodTimeout(method: string, timeout: number): void { - this.config.methods[method.toUpperCase()] = timeout; - } - - /** - * Updates default Timeout. - * @param timeout The timeout. - */ - updateDefaultTimeout(timeout: number): void { - this.config.default = timeout; - } - - getConfig(): ITimeoutConfig { - return { ...this.config }; - } -} diff --git a/src/common/timeout/timeout.controller.ts b/src/common/timeout/timeout.controller.ts deleted file mode 100644 index 26d0321b..00000000 --- a/src/common/timeout/timeout.controller.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Controller, Get, Put, UseGuards, Body, HttpCode, HttpStatus } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; -import { TimeoutConfigService, ITimeoutConfig } from './timeout-config.service'; - -/** - * Exposes timeout endpoints. - */ -@ApiTags('Timeout Configuration') -@ApiBearerAuth() -@Controller('timeout') -@UseGuards(JwtAuthGuard) -export class TimeoutController { - constructor(private readonly timeoutConfig: TimeoutConfigService) {} - - /** - * Returns config. - * @returns The resulting timeout config. - */ - @Get('config') - @ApiOperation({ summary: 'Get current timeout configuration' }) - @ApiResponse({ status: 200, description: 'Timeout configuration retrieved successfully' }) - getConfig(): ITimeoutConfig { - return this.timeoutConfig.getConfig(); - } - - /** - * Updates default Timeout. - * @param body The body. - * @returns The operation result. - */ - @Put('default') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Update default timeout' }) - @ApiResponse({ status: 200, description: 'Default timeout updated successfully' }) - updateDefaultTimeout(@Body() body: { timeout: number }): { message: string; timeout: number } { - this.timeoutConfig.updateDefaultTimeout(body.timeout); - return { - message: 'Default timeout updated successfully', - timeout: body.timeout, - }; - } - - /** - * Updates endpoint Timeout. - * @param body The body. - * @returns The operation result. - */ - @Put('endpoint') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Update endpoint timeout' }) - @ApiResponse({ status: 200, description: 'Endpoint timeout updated successfully' }) - updateEndpointTimeout(@Body() body: { path: string; timeout: number }): { message: string } { - this.timeoutConfig.updateEndpointTimeout(body.path, body.timeout); - return { - message: `Endpoint timeout updated successfully for ${body.path}`, - }; - } - - /** - * Updates method Timeout. - * @param body The body. - * @returns The operation result. - */ - @Put('method') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Update HTTP method timeout' }) - @ApiResponse({ status: 200, description: 'Method timeout updated successfully' }) - updateMethodTimeout(@Body() body: { method: string; timeout: number }): { message: string } { - this.timeoutConfig.updateMethodTimeout(body.method, body.timeout); - return { - message: `Method timeout updated successfully for ${body.method}`, - }; - } - - /** - * Validates timeout. - * @param body The body. - * @returns The operation result. - */ - @Get('check') - @ApiOperation({ summary: 'Get timeout for a specific request' }) - @ApiResponse({ status: 200, description: 'Timeout calculated successfully' }) - checkTimeout(@Body() body: { method: string; path: string }): { - timeout: number; - source: string; - } { - const timeout = this.timeoutConfig.getTimeoutForRequest(body.method, body.path); - const endpointTimeout = this.timeoutConfig.getEndpointTimeout(body.path); - const methodTimeout = this.timeoutConfig.getMethodTimeout(body.method); - - let source = 'default'; - if (endpointTimeout) { - source = 'endpoint'; - } else if (methodTimeout) { - source = 'method'; - } -} diff --git a/src/common/timeout/timeout.module.ts b/src/common/timeout/timeout.module.ts deleted file mode 100644 index 6101e03d..00000000 --- a/src/common/timeout/timeout.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TimeoutConfigService } from './timeout-config.service'; -import { TimeoutController } from './timeout.controller'; -import { TimeoutExampleController } from '../examples/timeout-example.controller'; - -/** - * Registers the timeout module. - */ -@Module({ - providers: [TimeoutConfigService], - controllers: [TimeoutController, TimeoutExampleController], - exports: [TimeoutConfigService], -}) -export class TimeoutModule { -} diff --git a/src/common/utils/correlation.utils.spec.ts b/src/common/utils/correlation.utils.spec.ts deleted file mode 100644 index 1fa4b219..00000000 --- a/src/common/utils/correlation.utils.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { correlationMiddleware, getCorrelationId, injectCorrelationIdToHeaders, CORRELATION_ID_HEADER, } from './correlation.utils'; -describe('correlation.utils', () => { - it('generates and propagates correlation ID through middleware', (done) => { - const req: unknown = { method: 'GET', url: '/test', headers: {} }; - const headers: Record = {}; - const res: unknown = { - setHeader: (name: string, value: string) => { - headers[name.toLowerCase()] = value; - }, - getHeader: (name: string) => headers[name.toLowerCase()], - }; - correlationMiddleware(req, res, () => { - const id = getCorrelationId(); - expect(typeof id).toBe('string'); - expect(res.getHeader(CORRELATION_ID_HEADER)).toBe(id); - done(); - }); - }); - it('respects incoming x-request-id header', (done) => { - const incomingId = 'test-correlation-id'; - const req: unknown = { method: 'GET', url: '/test', headers: { 'x-request-id': incomingId } }; - const headers: Record = {}; - const res: unknown = { - setHeader: (name: string, value: string) => { - headers[name.toLowerCase()] = value; - }, - getHeader: (name: string) => headers[name.toLowerCase()], - }; - correlationMiddleware(req, res, () => { - expect(getCorrelationId()).toBe(incomingId); - expect(res.getHeader(CORRELATION_ID_HEADER)).toBe(incomingId); - done(); - }); - }); - it('injects correlation header into outgoing request headers', () => { - const custom = injectCorrelationIdToHeaders({ Authorization: 'Bearer token' }, 'cid-1'); - expect(custom[CORRELATION_ID_HEADER]).toBe('cid-1'); - expect(custom.Authorization).toBe('Bearer token'); - }); -}); diff --git a/src/common/utils/pagination.util.spec.ts b/src/common/utils/pagination.util.spec.ts deleted file mode 100644 index 26a46f4a..00000000 --- a/src/common/utils/pagination.util.spec.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; -import { generateCursor, decodeCursor, validateCursor, paginateWithCursor, paginate, } from './pagination.util'; -import { SortOrder, CursorDirection } from '../dto/pagination.dto'; -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- -function buildMockQueryBuilder(items: unknown[]) { - const qb: unknown = { - alias: 'entity', - andWhere: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - addOrderBy: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getCount: jest.fn().mockResolvedValue(items.length), - getMany: jest.fn().mockResolvedValue(items), - }; - return qb; -} -function makeItems(count: number) { - return Array.from({ length: count }, (_, i) => ({ - id: `id-${String(i + 1).padStart(3, '0')}`, - createdAt: new Date(2024, 0, count - i).toISOString(), - title: `Course ${i + 1}`, - })); -} -// --------------------------------------------------------------------------- -// generateCursor -// --------------------------------------------------------------------------- -describe('generateCursor', () => { - it('produces a non-empty base64 string', () => { - const entity = { id: 'abc-123', createdAt: '2024-06-01T00:00:00.000Z' }; - const cursor = generateCursor(entity, 'createdAt'); - expect(typeof cursor).toBe('string'); - expect(cursor.length).toBeGreaterThan(0); - }); - it('encodes id and sortValue', () => { - const entity = { id: 'uuid-x', createdAt: '2024-06-01T00:00:00.000Z' }; - const cursor = generateCursor(entity, 'createdAt'); - const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString('utf8')); - expect(decoded.id).toBe('uuid-x'); - expect(decoded.sortValue).toBe('2024-06-01T00:00:00.000Z'); - }); - it('generates different cursors for different entities', () => { - const e1 = { id: 'id-1', createdAt: '2024-01-01' }; - const e2 = { id: 'id-2', createdAt: '2024-01-02' }; - expect(generateCursor(e1, 'createdAt')).not.toBe(generateCursor(e2, 'createdAt')); - }); - it('generates different cursors for same createdAt but different ids', () => { - const ts = '2024-01-01T00:00:00.000Z'; - const e1 = { id: 'id-1', createdAt: ts }; - const e2 = { id: 'id-2', createdAt: ts }; - expect(generateCursor(e1, 'createdAt')).not.toBe(generateCursor(e2, 'createdAt')); - }); -}); -// --------------------------------------------------------------------------- -// decodeCursor -// --------------------------------------------------------------------------- -describe('decodeCursor', () => { - it('round-trips correctly with generateCursor', () => { - const entity = { id: 'test-id', createdAt: '2024-03-15T12:00:00.000Z' }; - const cursor = generateCursor(entity, 'createdAt'); - const decoded = decodeCursor(cursor); - expect(decoded.id).toBe('test-id'); - expect(decoded.sortValue).toBe('2024-03-15T12:00:00.000Z'); - }); - it('throws BadRequestException for non-base64 input', () => { - expect(() => decodeCursor('!!!not-base64!!!')).toThrow(BadRequestException); - }); - it('throws BadRequestException when id field is missing', () => { - const bad = Buffer.from(JSON.stringify({ sortValue: '2024-01-01' })).toString('base64'); - expect(() => decodeCursor(bad)).toThrow(BadRequestException); - }); - it('throws BadRequestException when sortValue field is missing', () => { - const bad = Buffer.from(JSON.stringify({ id: 'some-id' })).toString('base64'); - expect(() => decodeCursor(bad)).toThrow(BadRequestException); - }); - it('throws BadRequestException for valid base64 but invalid JSON', () => { - const bad = Buffer.from('not json at all').toString('base64'); - expect(() => decodeCursor(bad)).toThrow(BadRequestException); - }); -}); -// --------------------------------------------------------------------------- -// validateCursor -// --------------------------------------------------------------------------- -describe('validateCursor', () => { - it('returns true for a cursor produced by generateCursor', () => { - const cursor = generateCursor({ id: 'id-1', createdAt: '2024-01-01' }, 'createdAt'); - expect(validateCursor(cursor)).toBe(true); - }); - it('returns false for a random string', () => { - expect(validateCursor('garbage')).toBe(false); - }); - it('returns false for empty string', () => { - expect(validateCursor('')).toBe(false); - }); -}); -// --------------------------------------------------------------------------- -// paginateWithCursor — forward pagination -// --------------------------------------------------------------------------- -describe('paginateWithCursor — forward (default)', () => { - it('returns all items and no cursors when fewer items than limit', async () => { - const items = makeItems(3); - const qb = buildMockQueryBuilder(items); - const result = await paginateWithCursor(qb, { limit: 10 }); - expect(result.data).toHaveLength(3); - expect(result.meta.hasNextPage).toBe(false); - expect(result.meta.hasPrevPage).toBe(false); - expect(result.meta.nextCursor).toBeNull(); - expect(result.meta.prevCursor).toBeNull(); - expect(result.meta.limit).toBe(10); - }); - it('returns nextCursor and hasNextPage=true when limit+1 items returned', async () => { - const items = makeItems(11); // limit=10 → 11th item signals more - const qb = buildMockQueryBuilder(items); - const result = await paginateWithCursor(qb, { limit: 10 }); - expect(result.data).toHaveLength(10); - expect(result.meta.hasNextPage).toBe(true); - expect(result.meta.nextCursor).not.toBeNull(); - expect(result.meta.prevCursor).toBeNull(); // no cursor provided → first page - }); - it('sets hasPrevPage=true and prevCursor when cursor is provided', async () => { - const items = makeItems(5); - const cursor = generateCursor(items[0], 'createdAt'); - const qb = buildMockQueryBuilder(items); - const result = await paginateWithCursor(qb, { cursor, limit: 10 }); - expect(result.meta.hasPrevPage).toBe(true); - expect(result.meta.prevCursor).not.toBeNull(); - }); - it('applies WHERE condition when cursor is provided', async () => { - const items = makeItems(3); - const cursor = generateCursor(items[0], 'createdAt'); - const qb = buildMockQueryBuilder(items); - await paginateWithCursor(qb, { cursor, limit: 10 }); - expect(qb.andWhere).toHaveBeenCalledTimes(1); - const [whereStr] = qb.andWhere.mock.calls[0]; - expect(whereStr).toContain('entity.createdAt'); - expect(whereStr).toContain('entity.id'); - }); - it('does NOT call andWhere when no cursor is provided', async () => { - const qb = buildMockQueryBuilder(makeItems(3)); - await paginateWithCursor(qb, { limit: 10 }); - expect(qb.andWhere).not.toHaveBeenCalled(); - }); - it('uses DESC operator (<) for forward DESC pagination', async () => { - const items = makeItems(3); - const cursor = generateCursor(items[0], 'createdAt'); - const qb = buildMockQueryBuilder(items); - await paginateWithCursor(qb, { cursor, limit: 5, order: SortOrder.DESC }); - const [whereStr] = qb.andWhere.mock.calls[0]; - expect(whereStr).toContain('<'); - }); - it('uses ASC operator (>) for forward ASC pagination', async () => { - const items = makeItems(3); - const cursor = generateCursor(items[0], 'createdAt'); - const qb = buildMockQueryBuilder(items); - await paginateWithCursor(qb, { cursor, limit: 5, order: SortOrder.ASC }); - const [whereStr] = qb.andWhere.mock.calls[0]; - expect(whereStr).toContain('>'); - }); - it('applies orderBy with the specified sortBy field', async () => { - const qb = buildMockQueryBuilder(makeItems(2)); - await paginateWithCursor(qb, { sortBy: 'title', order: SortOrder.ASC }); - expect(qb.orderBy).toHaveBeenCalledWith('entity.title', SortOrder.ASC); - }); - it('uses default limit of 10 when not specified', async () => { - const qb = buildMockQueryBuilder(makeItems(0)); - const result = await paginateWithCursor(qb, {}); - expect(result.meta.limit).toBe(10); - // take() called with limit+1 - expect(qb.take).toHaveBeenCalledWith(11); - }); -}); -// --------------------------------------------------------------------------- -// paginateWithCursor — backward pagination -// --------------------------------------------------------------------------- -describe('paginateWithCursor — backward', () => { - it('reverses results to natural order', async () => { - // Mock returns items in reversed sort order (ASC after inversion) - const items = [ - { id: 'id-001', createdAt: '2024-01-01' }, - { id: 'id-002', createdAt: '2024-01-02' }, - { id: 'id-003', createdAt: '2024-01-03' }, - ]; - const cursor = generateCursor({ id: 'id-004', createdAt: '2024-01-04' }, 'createdAt'); - const qb = buildMockQueryBuilder(items); - const result = await paginateWithCursor(qb, { - cursor, - limit: 10, - direction: CursorDirection.BACKWARD, - }); - // Reversed: [id-003, id-002, id-001] - expect(result.data[0].id).toBe('id-003'); - expect(result.data[2].id).toBe('id-001'); - }); - it('sets hasNextPage=true when cursor is provided on backward navigation', async () => { - const items = makeItems(3); - const cursor = generateCursor(items[0], 'createdAt'); - const qb = buildMockQueryBuilder(items); - const result = await paginateWithCursor(qb, { - cursor, - limit: 10, - direction: CursorDirection.BACKWARD, - }); - expect(result.meta.hasNextPage).toBe(true); - expect(result.meta.nextCursor).not.toBeNull(); - }); - it('sets hasPrevPage=true and prevCursor when limit+1 items returned', async () => { - const items = makeItems(11); - const cursor = generateCursor(items[0], 'createdAt'); - const qb = buildMockQueryBuilder(items); - const result = await paginateWithCursor(qb, { - cursor, - limit: 10, - direction: CursorDirection.BACKWARD, - }); - expect(result.data).toHaveLength(10); - expect(result.meta.hasPrevPage).toBe(true); - expect(result.meta.prevCursor).not.toBeNull(); - }); - it('inverts sort order for backward direction (DESC → ASC)', async () => { - const cursor = generateCursor({ id: 'id-x', createdAt: '2024-01-10' }, 'createdAt'); - const qb = buildMockQueryBuilder([]); - await paginateWithCursor(qb, { - cursor, - limit: 5, - order: SortOrder.DESC, - direction: CursorDirection.BACKWARD, - }); - expect(qb.orderBy).toHaveBeenCalledWith('entity.createdAt', SortOrder.ASC); - }); - it('uses > operator for backward DESC pagination', async () => { - const items = makeItems(2); - const cursor = generateCursor(items[0], 'createdAt'); - const qb = buildMockQueryBuilder(items); - await paginateWithCursor(qb, { - cursor, - limit: 5, - order: SortOrder.DESC, - direction: CursorDirection.BACKWARD, - }); - const [whereStr] = qb.andWhere.mock.calls[0]; - expect(whereStr).toContain('>'); - }); -}); -// --------------------------------------------------------------------------- -// paginate (offset-based — regression) -// --------------------------------------------------------------------------- -describe('paginate (offset-based)', () => { - it('returns correct metadata for a given page', async () => { - const allItems = makeItems(25); - const pageItems = allItems.slice(10, 20); - const qb = buildMockQueryBuilder(pageItems); - qb.getCount.mockResolvedValue(25); - const result = await paginate(qb, { page: 2, limit: 10 }); - expect(result.meta.totalItems).toBe(25); - expect(result.meta.currentPage).toBe(2); - expect(result.meta.totalPages).toBe(3); - expect(result.meta.itemsPerPage).toBe(10); - expect(result.meta.itemCount).toBe(pageItems.length); - }); - it('applies skip and take correctly', async () => { - const qb = buildMockQueryBuilder([]); - qb.getCount.mockResolvedValue(0); - await paginate(qb, { page: 3, limit: 5 }); - expect(qb.skip).toHaveBeenCalledWith(10); - expect(qb.take).toHaveBeenCalledWith(5); - }); - it('defaults to page 1 and limit 10', async () => { - const qb = buildMockQueryBuilder([]); - qb.getCount.mockResolvedValue(0); - const result = await paginate(qb, {}); - expect(result.meta.currentPage).toBe(1); - expect(result.meta.itemsPerPage).toBe(10); - }); -}); diff --git a/src/common/utils/pagination.util.ts b/src/common/utils/pagination.util.ts deleted file mode 100644 index b7cf3985..00000000 --- a/src/common/utils/pagination.util.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; -import { SelectQueryBuilder } from 'typeorm'; -import { PaginationQueryDto, SortOrder, CursorPaginationQueryDto, CursorDirection, } from '../dto/pagination.dto'; -import { APP_CONSTANTS } from '../constants/app.constants'; -const { DEFAULT_PAGE_SIZE } = APP_CONSTANTS; - -export interface IPaginatedResponse { - data: T[]; - meta: { - totalItems: number; - itemCount: number; - itemsPerPage: number; - totalPages: number; - currentPage: number; - }; -} - -export interface ICursorPaginatedResponse { - data: T[]; - meta: { - nextCursor: string | null; - prevCursor: string | null; - hasNextPage: boolean; - hasPrevPage: boolean; - limit: number; - }; -} - -/** - * Paginates query results. - * @param queryBuilder The query value. - * @param options The options. - * @returns The resulting paginated response. - */ -export async function paginate( - queryBuilder: SelectQueryBuilder, - options: PaginationQueryDto, -): Promise> { - const page = options.page || 1; - const limit = options.limit || DEFAULT_PAGE_SIZE; - const skip = (page - 1) * limit; - - // Apply sorting - if (options.sortBy) { - const alias = queryBuilder.alias; - queryBuilder.orderBy(`${alias}.${options.sortBy}`, options.order); - } - - // Clone query to get count without pagination limits - const totalItems = await queryBuilder.getCount(); - - // Apply pagination - const data = await queryBuilder.skip(skip).take(limit).getMany(); - - const totalPages = Math.ceil(totalItems / limit); - - return { - data, - meta: { - totalItems: number; - itemCount: number; - itemsPerPage: number; - totalPages: number; - currentPage: number; - }; -} -export interface CursorPaginatedResponse { - data: T[]; - meta: { - nextCursor: string | null; - prevCursor: string | null; - hasNextPage: boolean; - hasPrevPage: boolean; - limit: number; - }; -} -export async function paginate(queryBuilder: SelectQueryBuilder, options: PaginationQueryDto): Promise> { - const page = options.page || 1; - const limit = options.limit || DEFAULT_PAGE_SIZE; - const skip = (page - 1) * limit; - // Apply sorting - if (options.sortBy) { - const alias = queryBuilder.alias; - queryBuilder.orderBy(`${alias}.${options.sortBy}`, options.order); - } - // Clone query to get count without pagination limits - const totalItems = await queryBuilder.getCount(); - // Apply pagination - const data = await queryBuilder.skip(skip).take(limit).getMany(); - const totalPages = Math.ceil(totalItems / limit); - return { - data, - meta: { - totalItems, - itemCount: data.length, - itemsPerPage: limit, - totalPages, - currentPage: page, - }, - }; -} -/** - * Encodes entity fields into a base64 opaque cursor string. - * The cursor captures the sort field value and the entity id for stable pagination. - */ -export function generateCursor(entity: Record, sortBy: string): string { - const cursorData = { id: entity.id, sortValue: entity[sortBy] }; - return Buffer.from(JSON.stringify(cursorData)).toString('base64'); -} -/** - * Decodes a cursor string back to its constituent fields. - * Throws BadRequestException if the cursor is malformed or missing required fields. - */ -export function decodeCursor(cursor: string): { - id: string; - sortValue: unknown; -} { - try { - const json = Buffer.from(cursor, 'base64').toString('utf8'); - const data = JSON.parse(json); - if (typeof data.id !== 'string' || data.sortValue === undefined) { - throw new BadRequestException('Invalid cursor structure'); - } - return data; - } - catch (error) { - if (error instanceof BadRequestException) - throw error; - throw new BadRequestException('Invalid cursor value'); - } -} -/** - * Returns true if the cursor can be decoded without errors, false otherwise. - */ -export function validateCursor(cursor: string): boolean { - try { - decodeCursor(cursor); - return true; - } - catch { - return false; - } -} -/** - * Cursor-based pagination for TypeORM query builders. - * - * Supports bidirectional navigation: - * - direction=forward (default): fetch items after the cursor (newer → older for DESC) - * - direction=backward: fetch items before the cursor, reversing results to natural order - * - * The cursor encodes {id, sortValue} of a boundary item so pages stay stable even - * as new records are inserted. - */ -export async function paginateWithCursor>( - queryBuilder: SelectQueryBuilder, - options: CursorPaginationQueryDto, -): Promise> { - const limit = options.limit || DEFAULT_PAGE_SIZE; - const sortBy = options.sortBy || 'createdAt'; - const order = options.order || SortOrder.DESC; - const direction = options.direction || CursorDirection.FORWARD; - const alias = queryBuilder.alias; - - const isForward = direction === CursorDirection.FORWARD; - const isDesc = order === SortOrder.DESC; - - if (options.cursor) { - const { id: cursorId, sortValue } = decodeCursor(options.cursor); - - // Determine the comparison operator for WHERE clause: - // Forward + DESC → get items older than cursor → primary field uses '<' - // Forward + ASC → get items newer than cursor → primary field uses '>' - // Backward + DESC → get items newer than cursor → primary field uses '>' - // Backward + ASC → get items older than cursor → primary field uses '<' - const useGreaterThan = (isForward && !isDesc) || (!isForward && isDesc); - const op = useGreaterThan ? '>' : '<'; - - queryBuilder.andWhere( - `(${alias}.${sortBy} ${op} :sortValue` + - ` OR (${alias}.${sortBy} = :sortValue AND ${alias}.id ${op} :cursorId))`, - { sortValue, cursorId }, - ); - } - - // For backward pagination the sort is inverted so we retrieve the nearest items; - // results are reversed after fetch to restore natural reading order. - const effectiveOrder: SortOrder = isForward ? order : isDesc ? SortOrder.ASC : SortOrder.DESC; - const effectiveIdOrder: SortOrder = isForward - ? isDesc - ? SortOrder.DESC - : SortOrder.ASC - : isDesc - ? SortOrder.ASC - : SortOrder.DESC; - - queryBuilder - .orderBy(`${alias}.${sortBy}`, effectiveOrder) - .addOrderBy(`${alias}.id`, effectiveIdOrder); - - // Fetch one extra item to determine whether another page exists in this direction - const rawItems = await queryBuilder.take(limit + 1).getMany(); - const hasMore = rawItems.length > limit; - const pageData = rawItems.slice(0, limit); - - if (!isForward) { - pageData.reverse(); - } - - // nextCursor points to the last item on this page (used to paginate forward) - const nextCursor = - pageData.length > 0 && (isForward ? hasMore : !!options.cursor) - ? generateCursor(pageData[pageData.length - 1], sortBy) - : null; - - // prevCursor points to the first item on this page (used to paginate backward) - const prevCursor = - pageData.length > 0 && (!isForward ? hasMore : !!options.cursor) - ? generateCursor(pageData[0], sortBy) - : null; - - return { - data: pageData, - meta: { - nextCursor, - prevCursor, - hasNextPage: isForward ? hasMore : !!options.cursor, - hasPrevPage: !isForward ? hasMore : !!options.cursor, - limit, - }, - }; -} diff --git a/src/common/utils/sanitization.utils.spec.ts b/src/common/utils/sanitization.utils.spec.ts deleted file mode 100644 index 473baab4..00000000 --- a/src/common/utils/sanitization.utils.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { sanitizeSqlLike, enforceWhitelistedValue } from './sanitization.utils'; -describe('sanitization.utils', () => { - describe('sanitizeSqlLike', () => { - it('trims whitespace and escapes %, _, and \\', () => { - const raw = " test%_\\' OR 1=1 -- "; - const escaped = sanitizeSqlLike(raw); - expect(escaped).toBe("test\\%\\_\\\\' OR 1=1 --"); - }); - it('normalizes control characters to space', () => { - const raw = 'foo\nbar\tbaz\rqux'; - const escaped = sanitizeSqlLike(raw); - expect(escaped).toBe('foo bar baz qux'); - }); - }); - describe('enforceWhitelistedValue', () => { - it('returns value from allowlist', () => { - const value = enforceWhitelistedValue('active', ['active', 'inactive'] as const, 'status'); - expect(value).toBe('active'); - }); - it('throws if value is not allowlisted', () => { - expect(() => enforceWhitelistedValue('hacked' as unknown, ['active', 'inactive'] as const, 'status')).toThrow(/Invalid value for status/); - }); - }); -}); diff --git a/src/common/utils/websocket.utils.ts b/src/common/utils/websocket.utils.ts deleted file mode 100644 index dc54d6c5..00000000 --- a/src/common/utils/websocket.utils.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Socket } from 'socket.io'; -type ConnectionMeta = { - userId: string; - lastSeen: number; - isAlive: boolean; -}; -class WebSocketManager { - private connections = new Map>(); // userId -> sockets - private meta = new Map(); // socketId -> meta - private MAX_CONNECTIONS_PER_USER = 3; - private MAX_GLOBAL_CONNECTIONS = 5000; - private HEARTBEAT_INTERVAL = 30000; // 30s - private TIMEOUT = 60000; // 60s - startHeartbeat(io: unknown) { - setInterval(() => { - io.sockets.sockets.forEach((socket: Socket) => { - const meta = this.meta.get(socket.id); - if (!meta) - return; - if (!meta.isAlive) { - socket.disconnect(true); - this.cleanupSocket(socket); - return; - } - meta.isAlive = false; - socket.emit('ping'); - }); - }, this.HEARTBEAT_INTERVAL); - } - handlePong(socket: Socket) { - const meta = this.meta.get(socket.id); - if (meta) { - meta.isAlive = true; - meta.lastSeen = Date.now(); - } - } - registerConnection(userId: string, socket: Socket) { - if (!this.connections.has(userId)) { - this.connections.set(userId, new Set()); - } - const userConnections = this.connections.get(userId); - if (!userConnections) { - return false; - } - // enforce global connection limits - if (this.meta.size >= this.MAX_GLOBAL_CONNECTIONS) { - socket.emit('error', { message: 'Server is at maximum capacity' }); - socket.disconnect(true); - return false; - } - // enforce max connections - if (userConnections.size >= this.MAX_CONNECTIONS_PER_USER) { - const oldestSocket = [...userConnections][0]; - oldestSocket.disconnect(true); - this.cleanupSocket(oldestSocket); - } - userConnections.add(socket); - this.meta.set(socket.id, { - userId, - lastSeen: Date.now(), - isAlive: true, - }); - return true; - } - cleanupSocket(socket: Socket) { - const meta = this.meta.get(socket.id); - if (!meta) - return; - const userConnections = this.connections.get(meta.userId); - userConnections?.delete(socket); - if (userConnections && userConnections.size === 0) { - this.connections.delete(meta.userId); - } - this.meta.delete(socket.id); - } - getActiveConnections(userId: string): number { - return this.connections.get(userId)?.size || 0; - } - getTotalConnections(): number { - return this.meta.size; - } - forceCleanup() { - this.connections.clear(); - this.meta.clear(); - } -} -export const wsManager = new WebSocketManager(); diff --git a/src/common/validators/is-strong-password.validator.ts b/src/common/validators/is-strong-password.validator.ts deleted file mode 100644 index 73d67a02..00000000 --- a/src/common/validators/is-strong-password.validator.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - IsStrongPassword, - calculatePasswordStrength, - IPasswordStrengthResult, -} from './password.validator'; diff --git a/src/common/validators/password.validator.spec.ts b/src/common/validators/password.validator.spec.ts deleted file mode 100644 index c4ce2af0..00000000 --- a/src/common/validators/password.validator.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { calculatePasswordStrength, PasswordConstraint } from './password.validator'; -describe('Password Validator', () => { - describe('calculatePasswordStrength', () => { - it('recognizes weak passwords', () => { - const result = calculatePasswordStrength('abc'); - expect(result.isValid).toBe(false); - expect(result.level).toBe('weak'); - expect(result.errors).toEqual(expect.arrayContaining([ - 'Password must be at least 8 characters long', - 'Password must contain at least one uppercase letter', - 'Password must contain at least one number', - ])); - }); - it('recognizes strong passwords', () => { - const result = calculatePasswordStrength('StrongPass123!'); - expect(result.isValid).toBe(true); - expect(result.level).toBe('strong'); - expect(result.errors).toEqual([]); - }); - }); - describe('PasswordConstraint', () => { - const constraint = new PasswordConstraint(); - it('validates strong password as valid', () => { - expect(constraint.validate('StrongPass123!')).toBe(true); - }); - it('validates weak password as invalid', () => { - expect(constraint.validate('weak')).toBe(false); - }); - it('returns detailed message for weak password', () => { - const message = constraint.defaultMessage({ value: 'weak' } as unknown); - expect(message).toContain('Password must be at least 8 characters long'); - }); - }); -}); diff --git a/src/common/validators/password.validator.ts b/src/common/validators/password.validator.ts deleted file mode 100644 index 8bae8481..00000000 --- a/src/common/validators/password.validator.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - registerDecorator, - ValidationOptions, - ValidatorConstraint, - ValidatorConstraintInterface, - ValidationArguments, -} from 'class-validator'; - -export interface IPasswordStrengthResult { - isValid: boolean; - errors: string[]; - score: number; - level: 'weak' | 'medium' | 'strong'; -} -export const PASSWORD_REQUIREMENTS = { - minLength: 8, - uppercase: /[A-Z]/, - lowercase: /[a-z]/, - number: /\d/, - special: /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/, -}; - -export function calculatePasswordStrength(password: string): IPasswordStrengthResult { - const errors: string[] = []; - - if (typeof password !== 'string') { - errors.push('Password must be a string'); - return { isValid: false, errors, score: 0, level: 'weak' }; - } - - if (password.length < PASSWORD_REQUIREMENTS.minLength) { - errors.push(`Password must be at least ${PASSWORD_REQUIREMENTS.minLength} characters long`); - } - if (!PASSWORD_REQUIREMENTS.uppercase.test(password)) { - errors.push('Password must contain at least one uppercase letter'); - } - if (!PASSWORD_REQUIREMENTS.lowercase.test(password)) { - errors.push('Password must contain at least one lowercase letter'); - } - if (!PASSWORD_REQUIREMENTS.number.test(password)) { - errors.push('Password must contain at least one number'); - } - if (!PASSWORD_REQUIREMENTS.special.test(password)) { - errors.push('Password must contain at least one special character'); - } - - const score = 5 - errors.length; - const level = score <= 2 ? 'weak' : score === 3 || score === 4 ? 'medium' : 'strong'; - - return { - isValid: errors.length === 0, - errors, - score: Math.max(0, score), - level, - }; -} - -/** - * Provides password Constraint behavior. - */ -@ValidatorConstraint({ name: 'password', async: false }) -export class PasswordConstraint implements ValidatorConstraintInterface { - /** - * Validates validate. - * @param password The password value. - * @returns Whether the operation succeeded. - */ - validate(password: string): boolean { - const result = calculatePasswordStrength(password); - return result.isValid; - } - - /** - * Executes default Message. - * @param args The args. - * @returns The resulting string value. - */ - defaultMessage(args: ValidationArguments): string { - const password = args.value as string; - const result = calculatePasswordStrength(password); - if (result.errors.length === 0) { - return 'Password does not meet strength requirements'; - } -} - -/** - * Executes is Strong Password. - * @param validationOptions The operation options. - * @returns The operation result. - */ -export function IsStrongPassword(validationOptions?: ValidationOptions) { - return function (object: object, propertyName: string) { - registerDecorator({ - target: object.constructor, - propertyName, - options: validationOptions, - constraints: [], - validator: PasswordConstraint, - }); - }; -} diff --git a/src/config/feature-flags.config.spec.ts b/src/config/feature-flags.config.spec.ts deleted file mode 100644 index e24d4cab..00000000 --- a/src/config/feature-flags.config.spec.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { - defaultFeatureFlags, - IFeatureFlagsConfig, - loadFeatureFlags, - getEnabledModules, - getDisabledModules, -} from './feature-flags.config'; - -/** - * Registry test — ensures the feature flags config stays in sync. - * If a flag is accidentally removed, these tests will catch it. - */ - -/** All flags that should exist in the config. Keep this list alphabetically sorted. */ -const EXPECTED_FLAGS: (keyof IFeatureFlagsConfig)[] = [ - 'ENABLE_AB_TESTING', - 'ENABLE_ASSESSMENT', - 'ENABLE_AUTH', - 'ENABLE_BACKUP', - 'ENABLE_CACHING', - 'ENABLE_CDN', - 'ENABLE_COLLABORATION', - 'ENABLE_DATA_WAREHOUSE', - 'ENABLE_EMAIL_MARKETING', - 'ENABLE_FEATURE_FLAGS', - 'ENABLE_GAMIFICATION', - 'ENABLE_GRAPHQL', - 'ENABLE_LEARNING_PATHS', - 'ENABLE_LOCALIZATION', - 'ENABLE_MEDIA_PROCESSING', - 'ENABLE_MIGRATIONS', - 'ENABLE_MODERATION', - 'ENABLE_NOTIFICATIONS', - 'ENABLE_OBSERVABILITY', - 'ENABLE_ORCHESTRATION', - 'ENABLE_PAYMENTS', - 'ENABLE_RATE_LIMITING', - 'ENABLE_SEARCH', - 'ENABLE_SECURITY', - 'ENABLE_SYNC', - 'ENABLE_TENANCY', -]; - -describe('IFeatureFlagsConfig', () => { - describe('defaultFeatureFlags', () => { - it('should define all expected flags', () => { - for (const flag of EXPECTED_FLAGS) { - expect(defaultFeatureFlags).toHaveProperty(flag); - } - }); - - it('should have no unexpected flags', () => { - const actualFlags = Object.keys(defaultFeatureFlags).sort(); - expect(actualFlags).toEqual([...EXPECTED_FLAGS]); - }); - - it.each(EXPECTED_FLAGS)('%s should be a boolean', (flag) => { - expect(typeof defaultFeatureFlags[flag]).toBe('boolean'); - }); - - it.each(EXPECTED_FLAGS)('%s should not be undefined', (flag) => { - expect(defaultFeatureFlags[flag]).not.toBeUndefined(); - }); - }); - - describe('loadFeatureFlags', () => { - const originalEnv = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...originalEnv }; - }); - - afterAll(() => { - process.env = originalEnv; - }); - - it('should return defaults when no env vars are set', () => { - // Clear all ENABLE_ env vars - for (const key of Object.keys(process.env)) { - if (key.startsWith('ENABLE_')) { - delete process.env[key]; - } - } - - const flags = loadFeatureFlags(); - expect(flags).toEqual(defaultFeatureFlags); - }); - - it('should override a flag when its env var is set to "true"', () => { - process.env.ENABLE_AB_TESTING = 'true'; - const flags = loadFeatureFlags(); - expect(flags.ENABLE_AB_TESTING).toBe(true); - }); - - it('should override a flag when its env var is set to "false"', () => { - process.env.ENABLE_AUTH = 'false'; - const flags = loadFeatureFlags(); - expect(flags.ENABLE_AUTH).toBe(false); - }); - - it('should accept "1" as true', () => { - process.env.ENABLE_GRAPHQL = '1'; - const flags = loadFeatureFlags(); - expect(flags.ENABLE_GRAPHQL).toBe(true); - }); - }); - - describe('getEnabledModules', () => { - it('should return all modules when all flags are true', () => { - const allTrue = { ...defaultFeatureFlags }; - for (const key of Object.keys(allTrue) as (keyof IFeatureFlagsConfig)[]) { - allTrue[key] = true; - } - const modules = getEnabledModules(allTrue); - expect(modules.length).toBe(EXPECTED_FLAGS.length); - }); - - it('should return empty array when all flags are false', () => { - const allFalse = { ...defaultFeatureFlags }; - for (const key of Object.keys(allFalse) as (keyof IFeatureFlagsConfig)[]) { - allFalse[key] = false; - } - const modules = getEnabledModules(allFalse); - expect(modules).toEqual([]); - }); - }); - - describe('getDisabledModules', () => { - // NOTE: getDisabledModules has a pre-existing naming convention mismatch. - // It generates names like "AUTHModule" (from key stripping) while - // getEnabledModules uses human-readable names like "AuthModule". - // This means the filter comparison never matches, and all modules are - // always returned as "disabled". This is a known bug tracked separately. - - it('should return an array of module name strings', () => { - const disabled = getDisabledModules(defaultFeatureFlags); - expect(Array.isArray(disabled)).toBe(true); - for (const mod of disabled) { - expect(typeof mod).toBe('string'); - expect(mod.endsWith('Module')).toBe(true); - } - }); - - it('should return more disabled modules when flags are false', () => { - const allFalse = { ...defaultFeatureFlags }; - for (const key of Object.keys(allFalse) as (keyof IFeatureFlagsConfig)[]) { - allFalse[key] = false; - } - const disabledAll = getDisabledModules(allFalse); - expect(disabledAll.length).toBe(EXPECTED_FLAGS.length); - }); - }); -}); diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts deleted file mode 100644 index 0f46eb89..00000000 --- a/src/config/swagger.config.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { INestApplication } from '@nestjs/common'; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import { API_VERSIONING_DOCUMENTATION } from '../common/modules/api-versioning.module'; - -/** - * Sets setup Swagger. - * @param app The app. - */ -export function setupSwagger(app: INestApplication): void { - const config = new DocumentBuilder() - .setTitle('TeachLink API') - .setDescription(`TeachLink backend API documentation. ${API_VERSIONING_DOCUMENTATION}`) - .setVersion('1.0') - .addBearerAuth({ - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - name: 'Authorization', - description: 'Enter JWT token', - in: 'header', - }, 'access-token') - .addTag('Auth', 'Authentication and authorization endpoints') - .addTag('Users', 'User management endpoints') - .addTag('Courses', 'Course management endpoints') - .addTag('Payments', 'Payment processing endpoints') - .addTag('Subscriptions', 'Subscription management endpoints') - .addServer('http://localhost:3000', 'Local Development') - .addServer('https://api.teachlink.com', 'Production') - .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api/docs', app, document, { - swaggerOptions: { - persistAuthorization: true, - tagsSorter: 'alpha', - operationsSorter: 'alpha', - }, - customSiteTitle: 'TeachLink API Docs', - }); -} diff --git a/src/courses/courses.controller.ts b/src/courses/courses.controller.ts deleted file mode 100644 index 34f02dde..00000000 --- a/src/courses/courses.controller.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request, } from '@nestjs/common'; -import { CoursesService } from './courses.service'; -import { CreateCourseDto } from './dto/create-course.dto'; -import { UpdateCourseDto } from './dto/update-course.dto'; -import { CourseSearchDto, CursorCourseSearchDto } from './dto/course-search.dto'; -import { ModulesService } from './modules/modules.service'; -import { CreateModuleDto } from './dto/create-module.dto'; -import { LessonsService } from './lessons/lessons.service'; -import { CreateLessonDto } from './dto/create-lesson.dto'; -import { EnrollmentsService } from './enrollments/enrollments.service'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; - -/** - * Exposes courses endpoints. - */ -@Controller('courses') -export class CoursesController { - constructor( - private readonly coursesService: CoursesService, - private readonly modulesService: ModulesService, - private readonly lessonsService: LessonsService, - private readonly enrollmentsService: EnrollmentsService, - ) {} - - /** - * Creates a new record. - * @param req The req. - * @param createCourseDto The request payload. - * @returns The operation result. - */ - @Post() - @UseGuards(JwtAuthGuard) - create(@Request() req, @Body() createCourseDto: CreateCourseDto) { - // Get user from JWT token - const user = req.user; - if (!user || !user.id) { - throw new Error('User not authenticated'); - } - return this.coursesService.create({ - ...createCourseDto, - instructorId: user.id, - }); - } - - /** - * Returns all. - * @param searchDto The request payload. - * @returns The operation result. - */ - @Get() - findAll(@Query() searchDto: CourseSearchDto) { - return this.coursesService.findAll(searchDto); - } - - /** - * Returns all With Cursor. - * @param searchDto The request payload. - * @returns The operation result. - */ - @Get('cursor') - findAllWithCursor(@Query() searchDto: CursorCourseSearchDto) { - return this.coursesService.findAllWithCursor(searchDto); - } - - /** - * Returns analytics. - * @param _req The req. - * @returns The operation result. - */ - @Get('analytics') - @UseGuards(JwtAuthGuard) - getAnalytics(@Request() _req) { - return this.coursesService.getAnalytics(); - } - - /** - * Returns one. - * @param id The identifier. - * @returns The operation result. - */ - @Get(':id') - findOne(@Param('id') id: string) { - return this.coursesService.findOne(id); - } - - /** - * Updates the requested record. - * @param id The identifier. - * @param updateCourseDto The request payload. - * @returns The operation result. - */ - @Patch(':id') - @UseGuards(JwtAuthGuard) - update(@Param('id') id: string, @Body() updateCourseDto: UpdateCourseDto) { - return this.coursesService.update(id, updateCourseDto); - } - - /** - * Removes the requested record. - * @param id The identifier. - * @returns The operation result. - */ - @Delete(':id') - @UseGuards(JwtAuthGuard) - remove(@Param('id') id: string) { - return this.coursesService.remove(id); - } - - // Modules - /** - * Creates module. - * @param req The req. - * @param courseId The course identifier. - * @param createModuleDto The request payload. - * @returns The operation result. - */ - @Post(':id/modules') - @UseGuards(JwtAuthGuard) - createModule( - @Request() req, - @Param('id') courseId: string, - @Body() createModuleDto: CreateModuleDto, - ) { - createModuleDto.courseId = courseId; - return this.modulesService.create(createModuleDto); - } - - // Lessons - /** - * Creates lesson. - * @param req The req. - * @param moduleId The module identifier. - * @param createLessonDto The request payload. - * @returns The operation result. - */ - @Post('modules/:moduleId/lessons') - @UseGuards(JwtAuthGuard) - createLesson( - @Request() req, - @Param('moduleId') moduleId: string, - @Body() createLessonDto: CreateLessonDto, - ) { - createLessonDto.moduleId = moduleId; - return this.lessonsService.create(createLessonDto); - } - - // Enrollments - /** - * Executes enroll. - * @param req The req. - * @param courseId The course identifier. - * @returns The operation result. - */ - @Post(':id/enroll') - @UseGuards(JwtAuthGuard) - enroll(@Request() req, @Param('id') courseId: string) { - const userId = req.user.id; - if (!userId) { - throw new Error('User not authenticated'); - } -} diff --git a/src/courses/courses.module.ts b/src/courses/courses.module.ts deleted file mode 100644 index ecd729de..00000000 --- a/src/courses/courses.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { CoursesService } from './courses.service'; -import { CoursesController } from './courses.controller'; -import { ModulesService } from './modules/modules.service'; -import { LessonsService } from './lessons/lessons.service'; -import { EnrollmentsService } from './enrollments/enrollments.service'; -import { Course } from './entities/course.entity'; -import { CourseModule as CourseModuleEntity } from './entities/course-module.entity'; -import { Lesson } from './entities/lesson.entity'; -import { Enrollment } from './entities/enrollment.entity'; -import { User } from '../users/entities/user.entity'; -import { SearchModule } from '../search/search.module'; -import { CourseSearchSyncService } from './search-sync/course-search-sync.service'; - -/** - * Registers the courses module. - */ -@Module({ - imports: [ - TypeOrmModule.forFeature([Course, CourseModuleEntity, Lesson, Enrollment, User]), - SearchModule, - ], - controllers: [CoursesController], - providers: [CoursesService, ModulesService, LessonsService, EnrollmentsService, CourseSearchSyncService], - exports: [CoursesService], -}) -export class CoursesModule { -} diff --git a/src/courses/courses.service.spec.ts b/src/courses/courses.service.spec.ts deleted file mode 100644 index 32fe7f08..00000000 --- a/src/courses/courses.service.spec.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { NotFoundException } from '@nestjs/common'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { CoursesService } from './courses.service'; -import { Course } from './entities/course.entity'; -import { CachingService } from '../caching/caching.service'; -import { CacheInvalidationService } from '../caching/invalidation/invalidation.service'; - -const mockRepo = () => ({ - create: jest.fn(), - save: jest.fn(), - findOne: jest.fn(), - find: jest.fn(), - findByIds: jest.fn(), - count: jest.fn(), - softDelete: jest.fn(), - createQueryBuilder: jest.fn(), - manager: { - transaction: jest.fn(), - getRepository: jest.fn(), - }, -}); - -const mockCaching = () => ({ - getOrSet: jest.fn().mockImplementation((_key: string, fn: () => any) => fn()), - invalidate: jest.fn().mockResolvedValue(undefined), -}); - -const mockInvalidation = () => ({ - invalidateByPattern: jest.fn().mockResolvedValue(undefined), - invalidate: jest.fn().mockResolvedValue(undefined), -}); - -describe('CoursesService', () => { - let service: CoursesService; - let repo: ReturnType; - let caching: ReturnType; - let emitter: { emit: jest.Mock }; - - beforeEach(async () => { - repo = mockRepo(); - caching = mockCaching(); - emitter = { emit: jest.fn() }; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CoursesService, - { provide: getRepositoryToken(Course), useValue: repo }, - { provide: CachingService, useValue: caching }, - { provide: CacheInvalidationService, useValue: mockInvalidation() }, - { provide: EventEmitter2, useValue: emitter }, - ], - }).compile(); - - service = module.get(CoursesService); - }); - - afterEach(() => jest.clearAllMocks()); - - // ─── create ────────────────────────────────────────────────────────────── - - describe('create', () => { - it('creates and returns a course', async () => { - const dto = { title: 'NestJS Basics', instructorId: 'inst-1' }; - const entity = { id: 'c1', title: 'NestJS Basics', instructor: { id: 'inst-1' } }; - repo.create.mockReturnValue(entity); - repo.save.mockResolvedValue(entity); - - const result = await service.create(dto); - expect(repo.create).toHaveBeenCalledWith({ - ...dto, - instructor: { id: 'inst-1' }, - }); - expect(repo.save).toHaveBeenCalledWith(entity); - expect(emitter.emit).toHaveBeenCalled(); - expect(result).toEqual(entity); - }); - }); - - // ─── findOne ───────────────────────────────────────────────────────────── - - describe('findOne', () => { - it('returns a course when found', async () => { - const entity = { id: 'c1', title: 'Course 1', modules: [] }; - repo.findOne.mockResolvedValue(entity); - - const result = await service.findOne('c1'); - expect(result).toEqual(entity); - expect(repo.findOne).toHaveBeenCalledWith({ - where: { id: 'c1' }, - relations: ['instructor', 'modules', 'modules.lessons'], - }); - }); - - it('throws NotFoundException when course does not exist', async () => { - repo.findOne.mockResolvedValue(null); - await expect(service.findOne('missing')).rejects.toThrow(NotFoundException); - }); - }); - - // ─── findByIds ─────────────────────────────────────────────────────────── - - describe('findByIds', () => { - it('returns empty array for empty input', async () => { - const result = await service.findByIds([]); - expect(result).toEqual([]); - expect(repo.findByIds).not.toHaveBeenCalled(); - }); - - it('delegates to repository for non-empty ids', async () => { - const courses = [{ id: 'c1' }, { id: 'c2' }]; - repo.findByIds.mockResolvedValue(courses); - const result = await service.findByIds(['c1', 'c2']); - expect(result).toEqual(courses); - }); - }); - - // ─── update ────────────────────────────────────────────────────────────── - - describe('update', () => { - it('throws NotFoundException when course does not exist', async () => { - repo.findOne.mockResolvedValue(null); - await expect(service.update('missing', { title: 'New' })).rejects.toThrow(NotFoundException); - }); - - it('updates and returns the course', async () => { - const entity = { id: 'c1', title: 'Old', modules: [] }; - repo.findOne.mockResolvedValue(entity); - repo.save.mockResolvedValue({ ...entity, title: 'New' }); - - const result = await service.update('c1', { title: 'New' }); - expect(result.title).toBe('New'); - expect(emitter.emit).toHaveBeenCalled(); - }); - }); - - // ─── remove ────────────────────────────────────────────────────────────── - - describe('remove', () => { - it('throws NotFoundException when course does not exist', async () => { - repo.findOne.mockResolvedValue(null); - await expect(service.remove('missing')).rejects.toThrow(NotFoundException); - }); - - it('runs a transaction and emits event', async () => { - const entity = { id: 'c1', modules: [] }; - repo.findOne.mockResolvedValue(entity); - repo.manager.transaction.mockImplementation(async (fn: any) => fn(repo.manager)); - repo.manager.getRepository.mockReturnValue({ softDelete: jest.fn().mockResolvedValue(undefined) }); - - await service.remove('c1'); - expect(repo.manager.transaction).toHaveBeenCalledTimes(1); - expect(emitter.emit).toHaveBeenCalled(); - }); - }); - - // ─── getAnalytics ──────────────────────────────────────────────────────── - - describe('getAnalytics', () => { - it('returns aggregated analytics', async () => { - repo.count - .mockResolvedValueOnce(10) - .mockResolvedValueOnce(6); - - const qb: any = { - leftJoin: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - getRawOne: jest.fn().mockResolvedValue({ totalEnrollments: '42' }), - }; - repo.createQueryBuilder.mockReturnValue(qb); - - const result = await service.getAnalytics(); - expect(result).toEqual({ totalCourses: 10, publishedCourses: 6, totalEnrollments: 42 }); - }); - }); -}); \ No newline at end of file diff --git a/src/courses/courses.service.ts b/src/courses/courses.service.ts deleted file mode 100644 index f6c8c480..00000000 --- a/src/courses/courses.service.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; -import { Course } from './entities/course.entity'; -import { UpdateCourseDto } from './dto/update-course.dto'; -import { - paginate, - paginateWithCursor, - IPaginatedResponse, - ICursorPaginatedResponse, -} from '../common/utils/pagination.util'; -import { CourseSearchDto, CursorCourseSearchDto } from './dto/course-search.dto'; -import { CachingService } from '../caching/caching.service'; -import { CacheInvalidationService } from '../caching/invalidation/invalidation.service'; -import { CACHE_TTL, CACHE_PREFIXES, CACHE_EVENTS } from '../caching/caching.constants'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { sanitizeSqlLike, enforceWhitelistedValue } from '../common/utils/sanitization.utils'; -import { CourseModule } from './entities/course-module.entity'; -import { Lesson } from './entities/lesson.entity'; - -/** - * Provides course operations. - */ -@Injectable() -export class CoursesService { - constructor( - @InjectRepository(Course) - private coursesRepository: Repository, - private readonly cachingService: CachingService, - private readonly invalidationService: CacheInvalidationService, - private readonly eventEmitter: EventEmitter2, - ) {} - - /** - * Creates a new record. - * @param createCourseDto The request payload. - * @returns The resulting course. - */ - async create(createCourseDto: any): Promise { - const course = this.coursesRepository.create({ - ...createCourseDto, - instructor: { id: createCourseDto.instructorId }, - }); - const saved = await this.coursesRepository.save(course); - const result = Array.isArray(saved) ? saved[0] : saved; - this.eventEmitter.emit(CACHE_EVENTS.COURSE_CREATED, { course: result }); - return result; - } - - async findAll(filter?: CourseSearchDto): Promise> { - const cacheKey = `${CACHE_PREFIXES.COURSES_LIST}:${JSON.stringify(filter || {})}`; - - return this.cachingService.getOrSet( - cacheKey, - async () => { - const query = this.coursesRepository.createQueryBuilder('course'); - - query.leftJoinAndSelect('course.instructor', 'instructor'); - - if (filter?.search) { - const safeSearch = sanitizeSqlLike(filter.search); - query.andWhere( - "(course.title ILIKE :search ESCAPE '\\' OR course.description ILIKE :search ESCAPE '\\')", - { search: `%${safeSearch}%` }, - ); - } - - if (filter?.status) { - const allowedStatuses = ['draft', 'published', 'archived'] as const; - const status = enforceWhitelistedValue(filter.status, allowedStatuses, 'status'); - query.andWhere('course.status = :status', { status }); - } - - if (filter?.instructorId) { - query.andWhere('course.instructorId = :instructorId', { - instructorId: filter.instructorId, - }); - } - - query.orderBy('course.createdAt', 'DESC'); - - return await paginate(query, filter); - }, - CACHE_TTL.COURSE_METADATA, - ); - } - - /** - * Retrieves all With Cursor. - * @param filter The filter criteria. - * @returns The resulting cursor paginated response. - */ - async findAllWithCursor( - filter?: CursorCourseSearchDto, - ): Promise> { - const cacheKey = `${CACHE_PREFIXES.COURSES_LIST}:cursor:${JSON.stringify(filter || {})}`; - - return this.cachingService.getOrSet( - cacheKey, - async () => { - const query = this.coursesRepository.createQueryBuilder('course'); - - query.leftJoinAndSelect('course.instructor', 'instructor'); - - if (filter?.search) { - query.andWhere('(course.title ILIKE :search OR course.description ILIKE :search)', { - search: `%${filter.search}%`, - }); - } - - if (filter?.status) { - query.andWhere('course.status = :status', { status: filter.status }); - } - - if (filter?.instructorId) { - query.andWhere('course.instructorId = :instructorId', { - instructorId: filter.instructorId, - }); - } - - if (filter?.minPrice !== undefined) { - query.andWhere('course.price >= :minPrice', { minPrice: filter.minPrice }); - } - - if (filter?.maxPrice !== undefined) { - query.andWhere('course.price <= :maxPrice', { maxPrice: filter.maxPrice }); - } - - return await paginateWithCursor(query, filter ?? {}); - }, - CACHE_TTL.COURSE_METADATA, - ); - } - - /** - * Retrieves records by their identifiers. - * @param ids The identifiers. - * @returns The matching results. - */ - async findByIds(ids: string[]): Promise { - if (ids.length === 0) return []; - return await this.coursesRepository.findByIds(ids); - } - - /** - * Retrieves by Instructor. - * @param instructorId The instructor identifier. - * @returns The matching results. - */ - async findByInstructor(instructorId: string): Promise { - return await this.coursesRepository.find({ - where: { instructor: { id: instructorId } }, - relations: ['instructor'], - }); - } - - /** - * Retrieves by Instructor Ids. - * @param instructorIds The instructor identifiers. - * @returns The matching results. - */ - async findByInstructorIds(instructorIds: string[]): Promise { - if (instructorIds.length === 0) return []; - return await this.coursesRepository - .createQueryBuilder('course') - .leftJoinAndSelect('course.instructor', 'instructor') - .where('instructor.id IN (:...instructorIds)', { instructorIds }) - .getMany(); - } - - /** - * Retrieves the requested record. - * @param id The identifier. - * @returns The resulting course. - */ - async findOne(id: string): Promise { - const cacheKey = `${CACHE_PREFIXES.COURSE}:${id}`; - - return this.cachingService.getOrSet( - cacheKey, - async () => { - const course = await this.coursesRepository.findOne({ - where: { id }, - relations: ['instructor', 'modules', 'modules.lessons'], - }); - if (!course) { - throw new NotFoundException(`Course with ID ${id} not found`); - } - return course; - }, - CACHE_TTL.COURSE_DETAILS, - ); - } - - /** - * Updates the requested record. - * @param id The identifier. - * @param updateCourseDto The request payload. - * @returns The resulting course. - */ - async update(id: string, updateCourseDto: UpdateCourseDto): Promise { - const course = await this.coursesRepository.findOne({ - where: { id }, - relations: ['instructor', 'modules', 'modules.lessons'], - }); - if (!course) { - throw new NotFoundException(`Course with ID ${id} not found`); - } - Object.assign(course, updateCourseDto); - const saved = await this.coursesRepository.save(course); - - // Invalidate cache after update - this.eventEmitter.emit(CACHE_EVENTS.COURSE_UPDATED, { courseId: id }); - - return saved; - } - - /** - * Removes the requested record. - * @param id The identifier. - */ - async remove(id: string): Promise { - const course = await this.coursesRepository.findOne({ - where: { id }, - relations: ['modules'], - }); - if (!course) { - throw new NotFoundException(`Course with ID ${id} not found`); - } - - await this.coursesRepository.manager.transaction(async (manager) => { - const moduleIds = course.modules.map((module) => module.id); - - if (moduleIds.length > 0) { - await manager.getRepository(Lesson).softDelete({ moduleId: In(moduleIds) }); - } - - await manager.getRepository(CourseModule).softDelete({ courseId: id }); - await manager.getRepository(Course).softDelete(id); - }); - - // Invalidate cache after delete - this.eventEmitter.emit(CACHE_EVENTS.COURSE_DELETED, { courseId: id }); - } - - /** - * Retrieves analytics. - * @returns The operation result. - */ - async getAnalytics(): Promise { - const totalCourses = await this.coursesRepository.count(); - const publishedCourses = await this.coursesRepository.count({ - where: { status: 'published' }, - }); - - const { totalEnrollments } = await this.coursesRepository - .createQueryBuilder('course') - .leftJoin('course.enrollments', 'enrollment') - .select('COUNT(enrollment.id)', 'totalEnrollments') - .getRawOne(); - - return { - totalCourses, - publishedCourses, - totalEnrollments: parseInt(totalEnrollments) || 0, - }; - } -} diff --git a/src/courses/enrollments/enrollments.service.ts b/src/courses/enrollments/enrollments.service.ts deleted file mode 100644 index 89156b54..00000000 --- a/src/courses/enrollments/enrollments.service.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Enrollment } from '../entities/enrollment.entity'; -import { Course } from '../entities/course.entity'; -import { User } from '../../users/entities/user.entity'; - -/** - * Provides enrollments operations. - */ -@Injectable() -export class EnrollmentsService { - constructor( - @InjectRepository(Enrollment) - private enrollmentsRepository: Repository, - @InjectRepository(Course) - private coursesRepository: Repository, - @InjectRepository(User) - private usersRepository: Repository, - ) {} - - /** - * Executes enroll. - * @param userId The user identifier. - * @param courseId The course identifier. - * @returns The resulting enrollment. - */ - async enroll(userId: string, courseId: string): Promise { - const existing = await this.enrollmentsRepository.findOne({ - where: { user: { id: userId }, course: { id: courseId } }, - }); - if (existing) { - throw new ConflictException('User is already enrolled in this course'); - } - - const user = await this.usersRepository.findOneBy({ id: userId }); - if (!user) throw new NotFoundException('User not found'); - - const course = await this.coursesRepository.findOneBy({ id: courseId }); - if (!course) throw new NotFoundException('Course not found'); - - const enrollment = this.enrollmentsRepository.create({ - user, - course, - progress: 0, - status: 'active', - }); - return this.enrollmentsRepository.save(enrollment); - } - - /** - * Retrieves user Enrollments. - * @param userId The user identifier. - * @returns The matching results. - */ - async findUserEnrollments(userId: string): Promise { - return this.enrollmentsRepository.find({ - where: { user: { id: userId } }, - relations: ['course'], - }); - } - - /** - * Updates progress. - * @param enrollmentId The enrollment identifier. - * @param progress The progress. - * @returns The resulting enrollment. - */ - async updateProgress(enrollmentId: string, progress: number): Promise { - const enrollment = await this.enrollmentsRepository.findOneBy({ id: enrollmentId }); - if (!enrollment) throw new NotFoundException('Enrollment not found'); - - enrollment.progress = progress; - if (progress >= 100) { - enrollment.status = 'completed'; - } -} diff --git a/src/courses/guards/ws-jwt-auth.guard.ts b/src/courses/guards/ws-jwt-auth.guard.ts deleted file mode 100644 index a6333a5b..00000000 --- a/src/courses/guards/ws-jwt-auth.guard.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import { Socket } from 'socket.io'; - -/** - * Protects ws Jwt Auth execution paths. - */ -@Injectable() -export class WsJwtAuthGuard implements CanActivate { - constructor(private readonly jwtService: JwtService) {} - - /** - * Executes can Activate. - * @param context The context. - * @returns Whether the operation succeeded. - */ - async canActivate(context: ExecutionContext): Promise { - const client: Socket = context.switchToWs().getClient(); - const token = - client.handshake.auth?.token || client.handshake.headers?.authorization?.split(' ')[1]; - - if (!token) { - client.disconnect(true); - return false; - } -} diff --git a/src/courses/search-sync/course-search-sync.service.ts b/src/courses/search-sync/course-search-sync.service.ts deleted file mode 100644 index ecfc5149..00000000 --- a/src/courses/search-sync/course-search-sync.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; -import { IndexingService } from '../../search/indexing/indexing.service'; -import { CACHE_EVENTS } from '../../caching/caching.constants'; - -@Injectable() -export class CourseSearchSyncService { - private readonly logger = new Logger(CourseSearchSyncService.name); - - constructor(private readonly indexingService: IndexingService) {} - - @OnEvent(CACHE_EVENTS.COURSE_CREATED) - async onCourseCreated(payload: { course: Record }): Promise { - try { - await this.indexingService.syncCourse(payload.course); - } catch (err) { - this.logger.warn(`Failed to index new course ${payload.course?.id}: ${err.message}`); - } - } - - @OnEvent(CACHE_EVENTS.COURSE_UPDATED) - async onCourseUpdated(payload: { courseId: string; fields?: Record }): Promise { - try { - if (payload.fields) { - await this.indexingService.updateCourse(payload.courseId, payload.fields); - } - } catch (err) { - this.logger.warn(`Failed to update search index for course ${payload.courseId}: ${err.message}`); - } - } - - @OnEvent(CACHE_EVENTS.COURSE_DELETED) - async onCourseDeleted(payload: { courseId: string }): Promise { - try { - await this.indexingService.removeCourse(payload.courseId); - } catch (err) { - this.logger.warn(`Failed to remove course ${payload.courseId} from search index: ${err.message}`); - } - } -} diff --git a/src/data-warehouse/data-warehouse.controller.ts b/src/data-warehouse/data-warehouse.controller.ts deleted file mode 100644 index 6b3d720b..00000000 --- a/src/data-warehouse/data-warehouse.controller.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { Controller, Get, Post, Body, Param, Query, UseGuards, Request } from '@nestjs/common'; -import { ETLPipelineService } from './etl/etl-pipeline.service'; -import { DimensionalModelingService } from './modeling/dimensional-modeling.service'; -import { DataQualityService } from './quality/data-quality.service'; -import { DataLineageService } from './lineage/data-lineage.service'; -import { IncrementalLoaderService } from './loading/incremental-loader.service'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { RolesGuard } from '../auth/guards/roles.guard'; - -/** - * Exposes data Warehouse endpoints. - */ -@Controller('data-warehouse') -@UseGuards(JwtAuthGuard, RolesGuard) -export class DataWarehouseController { - constructor( - private readonly etlService: ETLPipelineService, - private readonly modelingService: DimensionalModelingService, - private readonly qualityService: DataQualityService, - private readonly lineageService: DataLineageService, - private readonly loaderService: IncrementalLoaderService, - ) {} - - // ETL Pipeline endpoints - /** - * Creates eTLPipeline. - * @param config The config. - * @param _req The req. - * @returns The operation result. - */ - @Post('etl/pipeline') - async createETLPipeline(@Body() config: any, @Request() _req): Promise { - const pipeline = await this.etlService.createPipeline(config); - return { success: true, pipeline }; - } - - /** - * Returns eTLPipeline. - * @param id The identifier. - * @returns The operation result. - */ - @Get('etl/pipeline/:id') - async getETLPipeline(@Param('id') id: string): Promise { - const pipeline = await this.etlService.getJobStatus(id); - return { success: true, pipeline }; - } - - /** - * Returns all ETLPipelines. - * @returns The operation result. - */ - @Get('etl/pipelines') - async getAllETLPipelines(): Promise { - const pipelines = await this.etlService.getAllJobs(); - return { success: true, pipelines }; - } - - // Dimensional Modeling endpoints - /** - * Creates star Schema. - * @param body The body. - * @returns The operation result. - */ - @Post('modeling/star-schema') - async createStarSchema( - @Body() body: { name: string; factTable: any; dimensionTables: any[] }, - ): Promise { - const model = await this.modelingService.createStarSchema( - body.name, - body.factTable, - body.dimensionTables, - ); - return { success: true, model }; - } - - /** - * Creates snowflake Schema. - * @param body The body. - * @returns The operation result. - */ - @Post('modeling/snowflake-schema') - async createSnowflakeSchema( - @Body() body: { name: string; factTable: any; dimensionTables: any[]; subDimensions: any }, - ): Promise { - const model = await this.modelingService.createSnowflakeSchema( - body.name, - body.factTable, - body.dimensionTables, - body.subDimensions, - ); - return { success: true, model }; - } - - /** - * Returns all Models. - * @returns The operation result. - */ - @Get('modeling/models') - async getAllModels(): Promise { - const models = await this.modelingService.getAllModels(); - return { success: true, models }; - } - - /** - * Returns model. - * @param id The identifier. - * @returns The operation result. - */ - @Get('modeling/model/:id') - async getModel(@Param('id') id: string): Promise { - const model = await this.modelingService.getModel(id); - return { success: true, model }; - } - - /** - * Creates query. - * @param queryConfig The configuration values. - * @returns The operation result. - */ - @Post('modeling/query') - async createQuery(@Body() queryConfig: any): Promise { - const query = await this.modelingService.createQuery(queryConfig); - return { success: true, query }; - } - - /** - * Executes execute Query. - * @param id The identifier. - * @param body The body. - * @returns The operation result. - */ - @Post('modeling/query/:id/execute') - async executeQuery(@Param('id') id: string, @Body() body: { parameters?: any }): Promise { - const results = await this.modelingService.executeQuery(id, body.parameters); - return { success: true, results }; - } - - // Data Quality endpoints - /** - * Creates quality Profile. - * @param profileConfig The configuration values. - * @returns The operation result. - */ - @Post('quality/profile') - async createQualityProfile(@Body() profileConfig: any): Promise { - const profile = await this.qualityService.createProfile(profileConfig); - return { success: true, profile }; - } - - /** - * Creates standard Profiles. - * @returns The operation result. - */ - @Post('quality/profiles/standard') - async createStandardProfiles(): Promise { - const profiles = await this.qualityService.createStandardProfiles(); - return { success: true, profiles }; - } - - /** - * Executes run Quality Check. - * @param profileId The profile identifier. - * @param body The body. - * @returns The operation result. - */ - @Post('quality/check/:profileId') - async runQualityCheck( - @Param('profileId') profileId: string, - @Body() body: { data: any[] }, - ): Promise { - const check = await this.qualityService.runQualityChecks(profileId, body.data); - return { success: true, check }; - } - - /** - * Returns quality Checks. - * @param profileId The profile identifier. - * @returns The operation result. - */ - @Get('quality/checks/:profileId') - async getQualityChecks(@Param('profileId') profileId: string): Promise { - const checks = await this.qualityService.getChecksForProfile(profileId); - return { success: true, checks }; - } - - /** - * Returns quality Issues. - * @param profileId The profile identifier. - * @param severity The severity. - * @param resolved The resolved. - * @returns The operation result. - */ - @Get('quality/issues') - async getQualityIssues( - @Query('profileId') profileId?: string, - @Query('severity') severity?: string, - @Query('resolved') resolved?: string, - ): Promise { - const resolvedBool = resolved === 'true' ? true : resolved === 'false' ? false : undefined; - const issues = await this.qualityService.getQualityIssues(profileId, severity, resolvedBool); - return { success: true, issues }; - } - - // Data Lineage endpoints - /** - * Creates lineage Graph. - * @param graphConfig The configuration values. - * @returns The operation result. - */ - @Post('lineage/graph') - async createLineageGraph(@Body() graphConfig: any): Promise { - const graph = await this.lineageService.createGraph(graphConfig); - return { success: true, graph }; - } - - /** - * Creates standard Lineage. - * @returns The operation result. - */ - @Post('lineage/graphs/standard') - async createStandardLineage(): Promise { - const graph = await this.lineageService.createStandardLineage(); - return { success: true, graph }; - } - - /** - * Returns all Lineage Graphs. - * @returns The operation result. - */ - @Get('lineage/graphs') - async getAllLineageGraphs(): Promise { - const graphs = await this.lineageService.getAllGraphs(); - return { success: true, graphs }; - } - - /** - * Executes add Lineage Node. - * @param graphId The graph identifier. - * @param nodeConfig The configuration values. - * @returns The operation result. - */ - @Post('lineage/graph/:id/node') - async addLineageNode(@Param('id') graphId: string, @Body() nodeConfig: any): Promise { - const node = await this.lineageService.addNode(graphId, nodeConfig); - return { success: true, node }; - } - - /** - * Executes add Lineage Edge. - * @param graphId The graph identifier. - * @param edgeConfig The configuration values. - * @returns The operation result. - */ - @Post('lineage/graph/:id/edge') - async addLineageEdge(@Param('id') graphId: string, @Body() edgeConfig: any): Promise { - const edge = await this.lineageService.addEdge(graphId, edgeConfig); - return { success: true, edge }; - } - - /** - * Executes trace Lineage. - * @param graphId The graph identifier. - * @param nodeId The node identifier. - * @param body The body. - * @returns The operation result. - */ - @Post('lineage/graph/:id/trace/:nodeId') - async traceLineage( - @Param('id') graphId: string, - @Param('nodeId') nodeId: string, - @Body() body: { traceType?: 'upstream' | 'downstream' | 'complete' }, - ): Promise { - const traceType = body.traceType || 'complete'; - const trace = await this.lineageService.traceLineage(graphId, nodeId, traceType); - return { success: true, trace }; - } - - /** - * Analyzes impact. - * @param graphId The graph identifier. - * @param nodeId The node identifier. - * @returns The operation result. - */ - @Post('lineage/graph/:id/impact/:nodeId') - async analyzeImpact(@Param('id') graphId: string, @Param('nodeId') nodeId: string): Promise { - const analysis = await this.lineageService.analyzeImpact(graphId, nodeId); - return { success: true, analysis }; - } - - // Incremental Loading endpoints - /** - * Creates load Job. - * @param body The body. - * @returns The operation result. - */ - @Post('loading/job') - async createLoadJob(@Body() body: { config: any; source: any; target: any }): Promise { - const job = await this.loaderService.createLoadJob(body.config, body.source, body.target); - return { success: true, job }; - } - - /** - * Executes execute Load Job. - * @param jobId The job identifier. - * @param body The body. - * @returns The operation result. - */ - @Post('loading/job/:id/execute') - async executeLoadJob( - @Param('id') jobId: string, - @Body() body: { sourceTable: string; targetTable: string }, - ): Promise { - const job = await this.loaderService.executeLoad(jobId, body.sourceTable, body.targetTable); - return { success: true, job }; - } - - /** - * Returns all Load Jobs. - * @returns The operation result. - */ - @Get('loading/jobs') - async getAllLoadJobs(): Promise { - const jobs = await this.loaderService.getAllJobs(); - return { success: true, jobs }; - } - - /** - * Returns load Job. - * @param id The identifier. - * @returns The operation result. - */ - @Get('loading/job/:id') - async getLoadJob(@Param('id') id: string): Promise { - const job = await this.loaderService.getJobStatus(id); - return { success: true, job }; - } - - /** - * Sets watermark. - * @param body The body. - * @returns The operation result. - */ - @Post('loading/watermark') - async setWatermark( - @Body() body: { tableName: string; columnName: string; value: any }, - ): Promise { - const watermark = await this.loaderService.setWatermark( - body.tableName, - body.columnName, - body.value, - ); - return { success: true, watermark }; - } - - /** - * Returns watermark. - * @param tableName The table name. - * @param columnName The column name. - * @returns The operation result. - */ - @Get('loading/watermark/:tableName/:columnName') - async getWatermark( - @Param('tableName') tableName: string, - @Param('columnName') columnName: string, - ): Promise { - const watermark = await this.loaderService.getWatermark(tableName, columnName); - return { success: true, watermark }; - } -} diff --git a/src/data-warehouse/data-warehouse.module.ts b/src/data-warehouse/data-warehouse.module.ts deleted file mode 100644 index 7bdb2510..00000000 --- a/src/data-warehouse/data-warehouse.module.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ETLPipelineService } from './etl/etl-pipeline.service'; -import { DimensionalModelingService } from './modeling/dimensional-modeling.service'; -import { DataQualityService } from './quality/data-quality.service'; -import { DataLineageService } from './lineage/data-lineage.service'; -import { IncrementalLoaderService } from './loading/incremental-loader.service'; -import { DataWarehouseController } from './data-warehouse.controller'; - -/** - * Registers the data Warehouse module. - */ -@Module({ - imports: [], - controllers: [DataWarehouseController], - providers: [ - ETLPipelineService, - DimensionalModelingService, - DataQualityService, - DataLineageService, - IncrementalLoaderService, - ], - exports: [ - ETLPipelineService, - DimensionalModelingService, - DataQualityService, - DataLineageService, - IncrementalLoaderService, - ], -}) -export class DataWarehouseModule { -} diff --git a/src/data-warehouse/etl/etl-pipeline.service.ts b/src/data-warehouse/etl/etl-pipeline.service.ts deleted file mode 100644 index c9a863d5..00000000 --- a/src/data-warehouse/etl/etl-pipeline.service.ts +++ /dev/null @@ -1,478 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; - -export interface IETLJob { - id: string; - name: string; - source: string; - target: string; - status: 'pending' | 'running' | 'completed' | 'failed'; - startTime: Date; - endTime?: Date; - duration?: number; - recordsProcessed: number; - recordsFailed: number; - config: IETLConfig; -} - -export interface IETLConfig { - sourceConnection: IDataSourceConfig; - targetConnection: IDataSourceConfig; - transformations: ITransformationRule[]; - schedule?: string; - incremental?: boolean; - batchSize?: number; -} - -export interface IDataSourceConfig { - type: 'postgres' | 'mysql' | 'mongodb' | 'api' | 'file'; - host?: string; - port?: number; - database?: string; - username?: string; - password?: string; - collection?: string; - endpoint?: string; - filePath?: string; - query?: string; -} - -export interface ITransformationRule { - id: string; - sourceField: string; - targetField: string; - transformationType: 'map' | 'filter' | 'aggregate' | 'calculate' | 'format'; - config: any; -} - -export interface IExtractedData { - data: any[]; - metadata: { - source: string; - target: string; - status: 'pending' | 'running' | 'completed' | 'failed'; - startTime: Date; - endTime?: Date; - duration?: number; - recordsProcessed: number; - recordsFailed: number; - config: ETLConfig; -} -export interface ETLConfig { - sourceConnection: DataSourceConfig; - targetConnection: DataSourceConfig; - transformations: TransformationRule[]; - schedule?: string; - incremental?: boolean; - batchSize?: number; -} -export interface DataSourceConfig { - type: 'postgres' | 'mysql' | 'mongodb' | 'api' | 'file'; - host?: string; - port?: number; - database?: string; - username?: string; - password?: string; - collection?: string; - endpoint?: string; - filePath?: string; - query?: string; -} -export interface TransformationRule { - id: string; - sourceField: string; - targetField: string; - transformationType: 'map' | 'filter' | 'aggregate' | 'calculate' | 'format'; - config: unknown; -} -export interface ExtractedData { - data: unknown[]; - metadata: { - source: string; - timestamp: Date; - recordCount: number; - }; -} - -export interface ITransformedData { - data: any[]; - metadata: { - transformationsApplied: string[]; - timestamp: Date; - recordCount: number; - }; -} - -/** - * Provides eTLPipeline operations. - */ -@Injectable() -export class ETLPipelineService { - private readonly logger = new Logger(ETLPipelineService.name); - private jobs: Map = new Map(); - - /** - * Create and execute an ETL pipeline - */ - async createPipeline(config: IETLConfig): Promise { - const jobId = uuidv4(); - const job: IETLJob = { - id: jobId, - name: `ETL_Pipeline_${new Date().toISOString()}`, - source: config.sourceConnection.type, - target: config.targetConnection.type, - status: 'pending', - startTime: new Date(), - recordsProcessed: 0, - recordsFailed: 0, - config, - }; - - this.jobs.set(jobId, job); - this.logger.log(`Created ETL pipeline job ${jobId}`); - - // Start the pipeline execution - this.executePipeline(jobId); - - return job; - } - - /** - * Execute the ETL pipeline - */ - private async executePipeline(jobId: string): Promise { - const job = this.jobs.get(jobId); - if (!job) { - throw new Error(`Job ${jobId} not found`); - } - /** - * Execute the ETL pipeline - */ - private async executePipeline(jobId: string): Promise { - const job = this.jobs.get(jobId); - if (!job) { - throw new Error(`Job ${jobId} not found`); - } - job.status = 'running'; - job.startTime = new Date(); - try { - // Extract phase - this.logger.log(`Starting extraction for job ${jobId}`); - const extractedData = await this.extract(job.config.sourceConnection); - // Transform phase - this.logger.log(`Starting transformation for job ${jobId}`); - const transformedData = await this.transform(extractedData, job.config.transformations); - // Load phase - this.logger.log(`Starting loading for job ${jobId}`); - await this.load(transformedData, job.config.targetConnection); - // Update job status - job.status = 'completed'; - job.endTime = new Date(); - job.duration = job.endTime.getTime() - job.startTime.getTime(); - job.recordsProcessed = transformedData.data.length; - this.logger.log(`ETL pipeline ${jobId} completed successfully`); - } - catch (error) { - this.logger.error(`ETL pipeline ${jobId} failed: ${error.message}`); - job.status = 'failed'; - job.endTime = new Date(); - job.duration = job.endTime.getTime() - job.startTime.getTime(); - job.recordsFailed = 1; - } - } - } - - /** - * Extract data from source - */ - private async extract(sourceConfig: IDataSourceConfig): Promise { - // This is a simplified implementation - // In a real system, this would connect to various data sources - - let data: any[] = []; - - switch (sourceConfig.type) { - case 'postgres': - // Connect to PostgreSQL and execute query - data = await this.extractFromPostgres(sourceConfig); - break; - case 'mysql': - // Connect to MySQL and execute query - data = await this.extractFromMysql(sourceConfig); - break; - case 'mongodb': - // Connect to MongoDB and fetch documents - data = await this.extractFromMongoDB(sourceConfig); - break; - case 'api': - // Call external API - data = await this.extractFromAPI(sourceConfig); - break; - case 'file': - // Read from file - data = await this.extractFromFile(sourceConfig); - break; - default: - throw new Error(`Unsupported source type: ${sourceConfig.type}`); - } - - return { - data, - metadata: { - source: sourceConfig.type, - timestamp: new Date(), - recordCount: data.length, - }, - }; - } - - /** - * Transform extracted data - */ - private async transform( - extractedData: IExtractedData, - transformations: ITransformationRule[], - ): Promise { - let transformedData = [...extractedData.data]; - const appliedTransformations: string[] = []; - - for (const rule of transformations) { - switch (rule.transformationType) { - case 'map': - transformedData = transformedData.map((item) => ({ - ...item, - [rule.targetField]: this.applyMapping(item[rule.sourceField], rule.config), - })); - break; - - case 'filter': - transformedData = transformedData.filter((item) => - this.applyFilter(item[rule.sourceField], rule.config), - ); - break; - - case 'calculate': - transformedData = transformedData.map((item) => ({ - ...item, - [rule.targetField]: this.applyCalculation(item, rule.config), - })); - break; - - case 'format': - transformedData = transformedData.map((item) => ({ - ...item, - [rule.targetField]: this.applyFormatting(item[rule.sourceField], rule.config), - })); - break; - } - - appliedTransformations.push( - `${rule.transformationType}:${rule.sourceField}->${rule.targetField}`, - ); - } - - return { - data: transformedData, - metadata: { - transformationsApplied: appliedTransformations, - timestamp: new Date(), - recordCount: transformedData.length, - }, - }; - } - - /** - * Load transformed data to target - */ - private async load( - transformedData: ITransformedData, - targetConfig: IDataSourceConfig, - ): Promise { - // This is a simplified implementation - // In a real system, this would connect to the target data warehouse - - switch (targetConfig.type) { - case 'postgres': - await this.loadToPostgres(transformedData, targetConfig); - break; - case 'mysql': - await this.loadToMysql(transformedData, targetConfig); - break; - case 'mongodb': - await this.loadToMongoDB(transformedData, targetConfig); - break; - default: - throw new Error(`Unsupported target type: ${targetConfig.type}`); - } - } - - /** - * Get job status - */ - async getJobStatus(jobId: string): Promise { - return this.jobs.get(jobId) || null; - } - - /** - * Get all jobs - */ - async getAllJobs(): Promise { - return Array.from(this.jobs.values()); - } - - /** - * Cancel a running job - */ - async cancelJob(jobId: string): Promise { - const job = this.jobs.get(jobId); - if (!job || job.status !== 'running') { - return false; - } - - job.status = 'failed'; - job.endTime = new Date(); - return true; - } - - // Helper methods for data source operations - private async extractFromPostgres(config: IDataSourceConfig): Promise { - // Implementation would use a PostgreSQL client - this.logger.log(`Extracting from PostgreSQL: ${config.database}`); - return []; // Placeholder - } - - private async extractFromMysql(config: IDataSourceConfig): Promise { - // Implementation would use a MySQL client - this.logger.log(`Extracting from MySQL: ${config.database}`); - return []; // Placeholder - } - - private async extractFromMongoDB(config: IDataSourceConfig): Promise { - // Implementation would use MongoDB client - this.logger.log(`Extracting from MongoDB: ${config.database}`); - return []; // Placeholder - } - - private async extractFromAPI(config: IDataSourceConfig): Promise { - // Implementation would make HTTP requests - this.logger.log(`Extracting from API: ${config.endpoint}`); - return []; // Placeholder - } - - private async extractFromFile(config: IDataSourceConfig): Promise { - // Implementation would read from file system - this.logger.log(`Extracting from file: ${config.filePath}`); - return []; // Placeholder - } - - private async loadToPostgres(data: ITransformedData, config: IDataSourceConfig): Promise { - // Implementation would use a PostgreSQL client - this.logger.log(`Loading to PostgreSQL: ${config.database}`); - } - - private async loadToMysql(data: ITransformedData, config: IDataSourceConfig): Promise { - // Implementation would use a MySQL client - this.logger.log(`Loading to MySQL: ${config.database}`); - } - - private async loadToMongoDB(data: ITransformedData, config: IDataSourceConfig): Promise { - // Implementation would use MongoDB client - this.logger.log(`Loading to MongoDB: ${config.database}`); - } - - // Transformation helper methods - private applyMapping(value: any, config: any): any { - if (config.mapping && config.mapping[value] !== undefined) { - return config.mapping[value]; - } - /** - * Cancel a running job - */ - async cancelJob(jobId: string): Promise { - const job = this.jobs.get(jobId); - if (!job || job.status !== 'running') { - return false; - } - job.status = 'failed'; - job.endTime = new Date(); - return true; - } - // Helper methods for data source operations - private async extractFromPostgres(config: DataSourceConfig): Promise { - // Implementation would use a PostgreSQL client - this.logger.log(`Extracting from PostgreSQL: ${config.database}`); - return []; // Placeholder - } - private async extractFromMysql(config: DataSourceConfig): Promise { - // Implementation would use a MySQL client - this.logger.log(`Extracting from MySQL: ${config.database}`); - return []; // Placeholder - } - private async extractFromMongoDB(config: DataSourceConfig): Promise { - // Implementation would use MongoDB client - this.logger.log(`Extracting from MongoDB: ${config.database}`); - return []; // Placeholder - } - private async extractFromAPI(config: DataSourceConfig): Promise { - // Implementation would make HTTP requests - this.logger.log(`Extracting from API: ${config.endpoint}`); - return []; // Placeholder - } - private async extractFromFile(config: DataSourceConfig): Promise { - // Implementation would read from file system - this.logger.log(`Extracting from file: ${config.filePath}`); - return []; // Placeholder - } - private async loadToPostgres(data: TransformedData, config: DataSourceConfig): Promise { - // Implementation would use a PostgreSQL client - this.logger.log(`Loading to PostgreSQL: ${config.database}`); - } - private async loadToMysql(data: TransformedData, config: DataSourceConfig): Promise { - // Implementation would use a MySQL client - this.logger.log(`Loading to MySQL: ${config.database}`); - } - private async loadToMongoDB(data: TransformedData, config: DataSourceConfig): Promise { - // Implementation would use MongoDB client - this.logger.log(`Loading to MongoDB: ${config.database}`); - } - // Transformation helper methods - private applyMapping(value: unknown, config: unknown): unknown { - if (config.mapping && config.mapping[value] !== undefined) { - return config.mapping[value]; - } - return value; - } - private applyFilter(value: unknown, config: unknown): boolean { - if (config.operator === 'equals') { - return value === config.value; - } - else if (config.operator === 'greaterThan') { - return value > config.value; - } - else if (config.operator === 'lessThan') { - return value < config.value; - } - return true; - } - private applyCalculation(item: unknown, config: unknown): unknown { - if (config.operation === 'sum') { - return (item[config.field1] || 0) + (item[config.field2] || 0); - } - else if (config.operation === 'multiply') { - return (item[config.field1] || 0) * (item[config.field2] || 0); - } - return item[config.field1]; - } - private applyFormatting(value: unknown, config: unknown): unknown { - if (config.format === 'date') { - return new Date(value).toISOString(); - } - else if (config.format === 'uppercase') { - return String(value).toUpperCase(); - } - else if (config.format === 'lowercase') { - return String(value).toLowerCase(); - } - return value; - } -} diff --git a/src/data-warehouse/lineage/data-lineage.service.ts b/src/data-warehouse/lineage/data-lineage.service.ts deleted file mode 100644 index 6fd6f2ef..00000000 --- a/src/data-warehouse/lineage/data-lineage.service.ts +++ /dev/null @@ -1,709 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; - -export interface IDataLineageNode { - id: string; - name: string; - type: 'source' | 'transformation' | 'target' | 'process'; - description: string; - system: string; - table?: string; - column?: string; - metadata: any; - createdAt: Date; -} - -export interface IDataLineageEdge { - id: string; - sourceId: string; - targetId: string; - transformation?: string; - description: string; - timestamp: Date; - metadata: any; -} - -export interface IDataLineageGraph { - id: string; - name: string; - description: string; - nodes: IDataLineageNode[]; - edges: IDataLineageEdge[]; - createdAt: Date; - updatedAt: Date; -} - -export interface ILineageTrace { - id: string; - nodeId: string; - traceType: 'upstream' | 'downstream' | 'complete'; - path: ILineagePath[]; - createdAt: Date; -} - -export interface ILineagePath { - fromNode: string; - toNode: string; - transformation: string; - timestamp: Date; -} -export interface ImpactAnalysis { - id: string; - nodeId: string; - affectedNodes: string[]; - impactLevel: 'high' | 'medium' | 'low'; - analysis: string; - timestamp: Date; -} - -/** - * Provides data Lineage operations. - */ -@Injectable() -export class DataLineageService { - private readonly logger = new Logger(DataLineageService.name); - private graphs: Map = new Map(); - private traces: Map = new Map(); - private impactAnalyses: Map = new Map(); - - /** - * Create a new lineage graph - */ - async createGraph( - graphConfig: Omit, - ): Promise { - const graphId = uuidv4(); - const graph: IDataLineageGraph = { - id: graphId, - ...graphConfig, - nodes: [], - edges: [], - createdAt: new Date(), - updatedAt: new Date(), - }; - - this.graphs.set(graphId, graph); - this.logger.log(`Created lineage graph ${graphId}: ${graph.name}`); - - return graph; - } - - /** - * Get a lineage graph - */ - async getGraph(graphId: string): Promise { - return this.graphs.get(graphId) || null; - } - - /** - * Get all lineage graphs - */ - async getAllGraphs(): Promise { - return Array.from(this.graphs.values()); - } - - /** - * Add a node to a lineage graph - */ - async addNode( - graphId: string, - nodeConfig: Omit, - ): Promise { - const graph = this.graphs.get(graphId); - if (!graph) { - throw new Error(`Graph ${graphId} not found`); - } - - const node: IDataLineageNode = { - id: uuidv4(), - ...nodeConfig, - createdAt: new Date(), - }; - - graph.nodes.push(node); - graph.updatedAt = new Date(); - - this.logger.log(`Added node ${node.id} to graph ${graphId}`); - - return node; - } - - /** - * Add an edge to a lineage graph - */ - async addEdge( - graphId: string, - edgeConfig: Omit, - ): Promise { - const graph = this.graphs.get(graphId); - if (!graph) { - throw new Error(`Graph ${graphId} not found`); - } - /** - * Get all lineage graphs - */ - async getAllGraphs(): Promise { - return Array.from(this.graphs.values()); - } - - const edge: IDataLineageEdge = { - id: uuidv4(), - ...edgeConfig, - timestamp: new Date(), - }; - - graph.edges.push(edge); - graph.updatedAt = new Date(); - - this.logger.log(`Added edge ${edge.id} to graph ${graphId}`); - - return edge; - } - - /** - * Trace data lineage upstream or downstream - */ - async traceLineage( - graphId: string, - nodeId: string, - traceType: 'upstream' | 'downstream' | 'complete' = 'complete', - ): Promise { - const graph = this.graphs.get(graphId); - if (!graph) { - throw new Error(`Graph ${graphId} not found`); - } - - const traceId = uuidv4(); - const path: ILineagePath[] = []; - - if (traceType === 'upstream' || traceType === 'complete') { - this.traceUpstream(graph, nodeId, path); - } - - if (traceType === 'downstream' || traceType === 'complete') { - this.traceDownstream(graph, nodeId, path); - } - - const trace: ILineageTrace = { - id: traceId, - nodeId, - traceType, - path, - createdAt: new Date(), - }; - - this.traces.set(traceId, trace); - this.logger.log(`Created lineage trace ${traceId} for node ${nodeId}`); - - return trace; - } - - /** - * Perform impact analysis - */ - async analyzeImpact(graphId: string, nodeId: string): Promise { - const graph = this.graphs.get(graphId); - if (!graph) { - throw new Error(`Graph ${graphId} not found`); - } - - const analysisId = uuidv4(); - const affectedNodes = this.findAffectedNodes(graph, nodeId); - const impactLevel = this.calculateImpactLevel(affectedNodes.length, graph.nodes.length); - - const analysis: ImpactAnalysis = { - id: analysisId, - nodeId, - affectedNodes, - impactLevel, - analysis: `Node ${nodeId} affects ${affectedNodes.length} other nodes`, - timestamp: new Date(), - }; - - this.impactAnalyses.set(analysisId, analysis); - this.logger.log(`Created impact analysis ${analysisId} for node ${nodeId}`); - - return analysis; - } - - /** - * Get lineage trace - */ - async getTrace(traceId: string): Promise { - return this.traces.get(traceId) || null; - } - - /** - * Get impact analysis - */ - async getImpactAnalysis(analysisId: string): Promise { - return this.impactAnalyses.get(analysisId) || null; - } - - /** - * Get all traces for a graph - */ - async getTracesForGraph(graphId: string): Promise { - const traces = Array.from(this.traces.values()); - return traces.filter((trace) => { - const graph = this.graphs.get(graphId); - return graph && graph.nodes.some((node) => node.id === trace.nodeId); - }); - } - - /** - * Get all impact analyses for a graph - */ - async getImpactAnalysesForGraph(graphId: string): Promise { - const analyses = Array.from(this.impactAnalyses.values()); - return analyses.filter((analysis) => { - const graph = this.graphs.get(graphId); - return graph && graph.nodes.some((node) => node.id === analysis.nodeId); - }); - } - - /** - * Create standard lineage for common data flows - */ - async createStandardLineage(): Promise { - const graph = await this.createGraph({ - name: 'Standard Data Flow', - description: 'Standard lineage for user and post data flow', - }); - - // Add source nodes - const userSource = await this.addNode(graph.id, { - name: 'User Source System', - type: 'source', - description: 'Source system containing user data', - system: 'User Service', - metadata: { systemType: 'microservice' }, - }); - - const postSource = await this.addNode(graph.id, { - name: 'Post Source System', - type: 'source', - description: 'Source system containing post data', - system: 'Post Service', - metadata: { systemType: 'microservice' }, - }); - - // Add transformation nodes - const userTransform = await this.addNode(graph.id, { - name: 'User Data Transformation', - type: 'transformation', - description: 'ETL transformation for user data', - system: 'ETL Pipeline', - metadata: { transformationType: 'cleanse_enrich' }, - }); - - const postTransform = await this.addNode(graph.id, { - name: 'Post Data Transformation', - type: 'transformation', - description: 'ETL transformation for post data', - system: 'ETL Pipeline', - metadata: { transformationType: 'cleanse_enrich' }, - }); - - // Add target nodes - const dataWarehouse = await this.addNode(graph.id, { - name: 'Data Warehouse', - type: 'target', - description: 'Central data warehouse', - system: 'Snowflake', - table: 'dim_users', - metadata: { schema: 'analytics' }, - }); - - const postWarehouse = await this.addNode(graph.id, { - name: 'Post Data Warehouse', - type: 'target', - description: 'Post data in warehouse', - system: 'Snowflake', - table: 'fact_posts', - metadata: { schema: 'analytics' }, - }); - - // Add edges - await this.addEdge(graph.id, { - sourceId: userSource.id, - targetId: userTransform.id, - description: 'User data extraction', - transformation: 'extract', - metadata: { frequency: 'hourly' }, - }); - - await this.addEdge(graph.id, { - sourceId: postSource.id, - targetId: postTransform.id, - description: 'Post data extraction', - transformation: 'extract', - metadata: { frequency: 'hourly' }, - }); - - await this.addEdge(graph.id, { - sourceId: userTransform.id, - targetId: dataWarehouse.id, - description: 'Load transformed user data', - transformation: 'load', - metadata: { method: 'incremental' }, - }); - - await this.addEdge(graph.id, { - sourceId: postTransform.id, - targetId: postWarehouse.id, - description: 'Load transformed post data', - transformation: 'load', - metadata: { method: 'incremental' }, - }); - - return graph; - } - - /** - * Search for nodes in lineage graphs - */ - async searchNodes(searchTerm: string, graphId?: string): Promise { - let nodes: IDataLineageNode[] = []; - - if (graphId) { - const graph = this.graphs.get(graphId); - if (graph) { - nodes = graph.nodes; - } - } else { - // Search across all graphs - for (const graph of this.graphs.values()) { - nodes.push(...graph.nodes); - } - } - - // Filter by search term - return nodes.filter( - (node) => - node.name.toLowerCase().includes(searchTerm.toLowerCase()) || - node.description.toLowerCase().includes(searchTerm.toLowerCase()) || - node.system.toLowerCase().includes(searchTerm.toLowerCase()), - ); - } - - // Helper methods - private traceUpstream(graph: IDataLineageGraph, nodeId: string, path: ILineagePath[]): void { - const incomingEdges = graph.edges.filter((edge) => edge.targetId === nodeId); - - for (const edge of incomingEdges) { - path.push({ - fromNode: edge.sourceId, - toNode: edge.targetId, - transformation: edge.transformation || '', - timestamp: edge.timestamp, - }); - - this.traceUpstream(graph, edge.sourceId, path); - } - } - - private traceDownstream(graph: IDataLineageGraph, nodeId: string, path: ILineagePath[]): void { - const outgoingEdges = graph.edges.filter((edge) => edge.sourceId === nodeId); - - for (const edge of outgoingEdges) { - path.push({ - fromNode: edge.sourceId, - toNode: edge.targetId, - transformation: edge.transformation || '', - timestamp: edge.timestamp, - }); - - this.traceDownstream(graph, edge.targetId, path); - } - } - - private findAffectedNodes(graph: IDataLineageGraph, nodeId: string): string[] { - const affectedNodes: string[] = []; - const visited = new Set(); - - const traverse = (currentNodeId: string) => { - if (visited.has(currentNodeId)) return; - visited.add(currentNodeId); - - const outgoingEdges = graph.edges.filter((edge) => edge.sourceId === currentNodeId); - for (const edge of outgoingEdges) { - if (!affectedNodes.includes(edge.targetId)) { - affectedNodes.push(edge.targetId); - } - const node: DataLineageNode = { - id: uuidv4(), - ...nodeConfig, - createdAt: new Date(), - }; - graph.nodes.push(node); - graph.updatedAt = new Date(); - this.logger.log(`Added node ${node.id} to graph ${graphId}`); - return node; - } - /** - * Add an edge to a lineage graph - */ - async addEdge(graphId: string, edgeConfig: Omit): Promise { - const graph = this.graphs.get(graphId); - if (!graph) { - throw new Error(`Graph ${graphId} not found`); - } - // Validate that source and target nodes exist - const sourceExists = graph.nodes.some((node) => node.id === edgeConfig.sourceId); - const targetExists = graph.nodes.some((node) => node.id === edgeConfig.targetId); - if (!sourceExists || !targetExists) { - throw new Error('Source or target node not found in graph'); - } - const edge: DataLineageEdge = { - id: uuidv4(), - ...edgeConfig, - timestamp: new Date(), - }; - graph.edges.push(edge); - graph.updatedAt = new Date(); - this.logger.log(`Added edge ${edge.id} to graph ${graphId}`); - return edge; - } - /** - * Trace data lineage upstream or downstream - */ - async traceLineage(graphId: string, nodeId: string, traceType: 'upstream' | 'downstream' | 'complete' = 'complete'): Promise { - const graph = this.graphs.get(graphId); - if (!graph) { - throw new Error(`Graph ${graphId} not found`); - } - const traceId = uuidv4(); - const path: LineagePath[] = []; - if (traceType === 'upstream' || traceType === 'complete') { - this.traceUpstream(graph, nodeId, path); - } - if (traceType === 'downstream' || traceType === 'complete') { - this.traceDownstream(graph, nodeId, path); - } - const trace: LineageTrace = { - id: traceId, - nodeId, - traceType, - path, - createdAt: new Date(), - }; - this.traces.set(traceId, trace); - this.logger.log(`Created lineage trace ${traceId} for node ${nodeId}`); - return trace; - } - /** - * Perform impact analysis - */ - async analyzeImpact(graphId: string, nodeId: string): Promise { - const graph = this.graphs.get(graphId); - if (!graph) { - throw new Error(`Graph ${graphId} not found`); - } - const analysisId = uuidv4(); - const affectedNodes = this.findAffectedNodes(graph, nodeId); - const impactLevel = this.calculateImpactLevel(affectedNodes.length, graph.nodes.length); - const analysis: ImpactAnalysis = { - id: analysisId, - nodeId, - affectedNodes, - impactLevel, - analysis: `Node ${nodeId} affects ${affectedNodes.length} other nodes`, - timestamp: new Date(), - }; - this.impactAnalyses.set(analysisId, analysis); - this.logger.log(`Created impact analysis ${analysisId} for node ${nodeId}`); - return analysis; - } - /** - * Get lineage trace - */ - async getTrace(traceId: string): Promise { - return this.traces.get(traceId) || null; - } - /** - * Get impact analysis - */ - async getImpactAnalysis(analysisId: string): Promise { - return this.impactAnalyses.get(analysisId) || null; - } - /** - * Get all traces for a graph - */ - async getTracesForGraph(graphId: string): Promise { - const traces = Array.from(this.traces.values()); - return traces.filter((trace) => { - const graph = this.graphs.get(graphId); - return graph && graph.nodes.some((node) => node.id === trace.nodeId); - }); - } - /** - * Get all impact analyses for a graph - */ - async getImpactAnalysesForGraph(graphId: string): Promise { - const analyses = Array.from(this.impactAnalyses.values()); - return analyses.filter((analysis) => { - const graph = this.graphs.get(graphId); - return graph && graph.nodes.some((node) => node.id === analysis.nodeId); - }); - } - /** - * Create standard lineage for common data flows - */ - async createStandardLineage(): Promise { - const graph = await this.createGraph({ - name: 'Standard Data Flow', - description: 'Standard lineage for user and post data flow', - }); - // Add source nodes - const userSource = await this.addNode(graph.id, { - name: 'User Source System', - type: 'source', - description: 'Source system containing user data', - system: 'User Service', - metadata: { systemType: 'microservice' }, - }); - const postSource = await this.addNode(graph.id, { - name: 'Post Source System', - type: 'source', - description: 'Source system containing post data', - system: 'Post Service', - metadata: { systemType: 'microservice' }, - }); - // Add transformation nodes - const userTransform = await this.addNode(graph.id, { - name: 'User Data Transformation', - type: 'transformation', - description: 'ETL transformation for user data', - system: 'ETL Pipeline', - metadata: { transformationType: 'cleanse_enrich' }, - }); - const postTransform = await this.addNode(graph.id, { - name: 'Post Data Transformation', - type: 'transformation', - description: 'ETL transformation for post data', - system: 'ETL Pipeline', - metadata: { transformationType: 'cleanse_enrich' }, - }); - // Add target nodes - const dataWarehouse = await this.addNode(graph.id, { - name: 'Data Warehouse', - type: 'target', - description: 'Central data warehouse', - system: 'Snowflake', - table: 'dim_users', - metadata: { schema: 'analytics' }, - }); - const postWarehouse = await this.addNode(graph.id, { - name: 'Post Data Warehouse', - type: 'target', - description: 'Post data in warehouse', - system: 'Snowflake', - table: 'fact_posts', - metadata: { schema: 'analytics' }, - }); - // Add edges - await this.addEdge(graph.id, { - sourceId: userSource.id, - targetId: userTransform.id, - description: 'User data extraction', - transformation: 'extract', - metadata: { frequency: 'hourly' }, - }); - await this.addEdge(graph.id, { - sourceId: postSource.id, - targetId: postTransform.id, - description: 'Post data extraction', - transformation: 'extract', - metadata: { frequency: 'hourly' }, - }); - await this.addEdge(graph.id, { - sourceId: userTransform.id, - targetId: dataWarehouse.id, - description: 'Load transformed user data', - transformation: 'load', - metadata: { method: 'incremental' }, - }); - await this.addEdge(graph.id, { - sourceId: postTransform.id, - targetId: postWarehouse.id, - description: 'Load transformed post data', - transformation: 'load', - metadata: { method: 'incremental' }, - }); - return graph; - } - /** - * Search for nodes in lineage graphs - */ - async searchNodes(searchTerm: string, graphId?: string): Promise { - let nodes: DataLineageNode[] = []; - if (graphId) { - const graph = this.graphs.get(graphId); - if (graph) { - nodes = graph.nodes; - } - } - else { - // Search across all graphs - for (const graph of this.graphs.values()) { - nodes.push(...graph.nodes); - } - } - // Filter by search term - return nodes.filter((node) => node.name.toLowerCase().includes(searchTerm.toLowerCase()) || - node.description.toLowerCase().includes(searchTerm.toLowerCase()) || - node.system.toLowerCase().includes(searchTerm.toLowerCase())); - } - // Helper methods - private traceUpstream(graph: DataLineageGraph, nodeId: string, path: LineagePath[]): void { - const incomingEdges = graph.edges.filter((edge) => edge.targetId === nodeId); - for (const edge of incomingEdges) { - path.push({ - fromNode: edge.sourceId, - toNode: edge.targetId, - transformation: edge.transformation || '', - timestamp: edge.timestamp, - }); - this.traceUpstream(graph, edge.sourceId, path); - } - } - private traceDownstream(graph: DataLineageGraph, nodeId: string, path: LineagePath[]): void { - const outgoingEdges = graph.edges.filter((edge) => edge.sourceId === nodeId); - for (const edge of outgoingEdges) { - path.push({ - fromNode: edge.sourceId, - toNode: edge.targetId, - transformation: edge.transformation || '', - timestamp: edge.timestamp, - }); - this.traceDownstream(graph, edge.targetId, path); - } - } - private findAffectedNodes(graph: DataLineageGraph, nodeId: string): string[] { - const affectedNodes: string[] = []; - const visited = new Set(); - const traverse = (currentNodeId: string) => { - if (visited.has(currentNodeId)) - return; - visited.add(currentNodeId); - const outgoingEdges = graph.edges.filter((edge) => edge.sourceId === currentNodeId); - for (const edge of outgoingEdges) { - if (!affectedNodes.includes(edge.targetId)) { - affectedNodes.push(edge.targetId); - } - traverse(edge.targetId); - } - }; - traverse(nodeId); - return affectedNodes; - } - private calculateImpactLevel(affectedCount: number, totalCount: number): 'high' | 'medium' | 'low' { - const percentage = (affectedCount / totalCount) * 100; - if (percentage >= 50) - return 'high'; - if (percentage >= 20) - return 'medium'; - return 'low'; - } -} diff --git a/src/data-warehouse/loading/incremental-loader.service.ts b/src/data-warehouse/loading/incremental-loader.service.ts deleted file mode 100644 index ebd6fcd3..00000000 --- a/src/data-warehouse/loading/incremental-loader.service.ts +++ /dev/null @@ -1,570 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; -export interface IncrementalLoadJob { - id: string; - name: string; - sourceTable: string; - targetTable: string; - lastProcessedId?: number; - lastProcessedTimestamp?: Date; - status: 'pending' | 'running' | 'completed' | 'failed'; - startTime: Date; - endTime?: Date; - duration?: number; - recordsProcessed: number; - recordsInserted: number; - recordsUpdated: number; - recordsDeleted: number; - config: IncrementalLoadConfig; -} -export interface IncrementalLoadConfig { - loadType: 'timestamp' | 'sequence' | 'cdc' | 'watermark'; - sourceConnection: IDataSourceConfig; - targetConnection: IDataSourceConfig; - batchSize: number; - maxRetries: number; - retryDelay: number; - watermarkColumn?: string; - timestampColumn?: string; - sequenceColumn?: string; - primaryKey: string[]; - incrementalColumns: string[]; -} - -export interface IDataSourceConfig { - type: 'postgres' | 'mysql' | 'mongodb' | 'snowflake'; - host?: string; - port?: number; - database?: string; - username?: string; - password?: string; - schema?: string; - warehouse?: string; -} - -export interface IWatermark { - id: string; - tableName: string; - columnName: string; - lastValue: any; - lastUpdated: Date; -} - -export interface ICDCEvent { - id: string; - tableName: string; - operation: 'INSERT' | 'UPDATE' | 'DELETE'; - primaryKey: { [key: string]: any }; - oldValues?: { [key: string]: any }; - newValues?: { [key: string]: any }; - timestamp: Date; - transactionId?: string; -} - -/** - * Provides incremental Loader operations. - */ -@Injectable() -export class IncrementalLoaderService { - private readonly logger = new Logger(IncrementalLoaderService.name); - private jobs: Map = new Map(); - private watermarks: Map = new Map(); - private cdcEvents: Map = new Map(); - - /** - * Create an incremental load job - */ - async createLoadJob( - config: Omit, - sourceConfig: IDataSourceConfig, - targetConfig: IDataSourceConfig, - ): Promise { - const jobId = uuidv4(); - const jobName = `Incremental_Load_${config.loadType}_${new Date().toISOString()}`; - - const job: IncrementalLoadJob = { - id: jobId, - name: jobName, - sourceTable: '', - targetTable: '', - status: 'pending', - startTime: new Date(), - recordsProcessed: 0, - recordsInserted: 0, - recordsUpdated: 0, - recordsDeleted: 0, - config: { - ...config, - sourceConnection: sourceConfig, - targetConnection: targetConfig, - }, - }; - - this.jobs.set(jobId, job); - this.logger.log(`Created incremental load job ${jobId}: ${jobName}`); - - return job; - } - - /** - * Execute incremental load - */ - async executeLoad( - jobId: string, - sourceTable: string, - targetTable: string, - ): Promise { - const job = this.jobs.get(jobId); - if (!job) { - throw new Error(`Job ${jobId} not found`); - } - /** - * Execute incremental load - */ - async executeLoad(jobId: string, sourceTable: string, targetTable: string): Promise { - const job = this.jobs.get(jobId); - if (!job) { - throw new Error(`Job ${jobId} not found`); - } - job.sourceTable = sourceTable; - job.targetTable = targetTable; - job.status = 'running'; - job.startTime = new Date(); - try { - this.logger.log(`Starting incremental load for job ${jobId}: ${sourceTable} -> ${targetTable}`); - let recordsProcessed = 0; - let recordsInserted = 0; - let recordsUpdated = 0; - let recordsDeleted = 0; - switch (job.config.loadType) { - case 'timestamp': { - const timestampResult = await this.loadByTimestamp(job); - recordsProcessed = timestampResult.processed; - recordsInserted = timestampResult.inserted; - recordsUpdated = timestampResult.updated; - break; - } - case 'sequence': { - const sequenceResult = await this.loadBySequence(job); - recordsProcessed = sequenceResult.processed; - recordsInserted = sequenceResult.inserted; - recordsUpdated = sequenceResult.updated; - break; - } - case 'watermark': { - const watermarkResult = await this.loadByWatermark(job); - recordsProcessed = watermarkResult.processed; - recordsInserted = watermarkResult.inserted; - recordsUpdated = watermarkResult.updated; - break; - } - case 'cdc': { - const cdcResult = await this.loadByCDC(job); - recordsProcessed = cdcResult.processed; - recordsInserted = cdcResult.inserted; - recordsUpdated = cdcResult.updated; - recordsDeleted = cdcResult.deleted; - break; - } - } - job.status = 'completed'; - job.endTime = new Date(); - job.duration = job.endTime.getTime() - job.startTime.getTime(); - job.recordsProcessed = recordsProcessed; - job.recordsInserted = recordsInserted; - job.recordsUpdated = recordsUpdated; - job.recordsDeleted = recordsDeleted; - this.logger.log(`Incremental load job ${jobId} completed successfully`); - this.logger.log(`Records processed: ${recordsProcessed}, Inserted: ${recordsInserted}, Updated: ${recordsUpdated}, Deleted: ${recordsDeleted}`); - } - catch (error) { - this.logger.error(`Incremental load job ${jobId} failed: ${error.message}`); - job.status = 'failed'; - job.endTime = new Date(); - job.duration = job.endTime.getTime() - job.startTime.getTime(); - } - return job; - } - /** - * Load data using timestamp-based approach - */ - private async loadByTimestamp(job: IncrementalLoadJob): Promise<{ - processed: number; - inserted: number; - updated: number; - }> { - const timestampColumn = job.config.timestampColumn || 'updated_at'; - const lastTimestamp = job.lastProcessedTimestamp || new Date(0); - this.logger.log(`Loading data newer than ${lastTimestamp.toISOString()}`); - // Get incremental data from source - const incrementalData = await this.getSourceDataSince(job.config.sourceConnection, job.sourceTable, timestampColumn, lastTimestamp); - // Apply changes to target - const result = await this.applyChangesToTarget(job.config.targetConnection, job.targetTable, incrementalData, job.config.primaryKey, 'timestamp'); - // Update last processed timestamp - if (incrementalData.length > 0) { - const maxTimestamp = Math.max(...incrementalData.map((row) => new Date(row[timestampColumn]).getTime())); - job.lastProcessedTimestamp = new Date(maxTimestamp); - } - return result; - } - /** - * Load data using sequence-based approach - */ - private async loadBySequence(job: IncrementalLoadJob): Promise<{ - processed: number; - inserted: number; - updated: number; - }> { - const sequenceColumn = job.config.sequenceColumn || 'id'; - const lastId = job.lastProcessedId || 0; - this.logger.log(`Loading data with ${sequenceColumn} > ${lastId}`); - // Get incremental data from source - const incrementalData = await this.getSourceDataAfter(job.config.sourceConnection, job.sourceTable, sequenceColumn, lastId); - // Apply changes to target - const result = await this.applyChangesToTarget(job.config.targetConnection, job.targetTable, incrementalData, job.config.primaryKey, 'sequence'); - // Update last processed ID - if (incrementalData.length > 0) { - const maxId = Math.max(...incrementalData.map((row) => row[sequenceColumn])); - job.lastProcessedId = maxId; - } - return result; - } - /** - * Load data using watermark approach - */ - private async loadByWatermark(job: IncrementalLoadJob): Promise<{ - processed: number; - inserted: number; - updated: number; - }> { - const watermarkColumn = job.config.watermarkColumn || 'updated_at'; - const watermarkKey = `${job.sourceTable}_${watermarkColumn}`; - const watermark = this.watermarks.get(watermarkKey); - const lastValue = watermark?.lastValue || 0; - this.logger.log(`Loading data with ${watermarkColumn} > ${lastValue}`); - // Get incremental data from source - const incrementalData = await this.getSourceDataAfter(job.config.sourceConnection, job.sourceTable, watermarkColumn, lastValue); - // Apply changes to target - const result = await this.applyChangesToTarget(job.config.targetConnection, job.targetTable, incrementalData, job.config.primaryKey, 'watermark'); - // Update watermark - if (incrementalData.length > 0) { - const maxValue = Math.max(...incrementalData.map((row) => row[watermarkColumn])); - const newWatermark: Watermark = { - id: uuidv4(), - tableName: job.sourceTable, - columnName: watermarkColumn, - lastValue: maxValue, - lastUpdated: new Date(), - }; - this.watermarks.set(watermarkKey, newWatermark); - } - return result; - } - - return result; - } - - /** - * Load data using watermark approach - */ - private async loadByWatermark(job: IncrementalLoadJob): Promise<{ - processed: number; - inserted: number; - updated: number; - }> { - const watermarkColumn = job.config.watermarkColumn || 'updated_at'; - const watermarkKey = `${job.sourceTable}_${watermarkColumn}`; - const watermark = this.watermarks.get(watermarkKey); - const lastValue = watermark?.lastValue || 0; - - this.logger.log(`Loading data with ${watermarkColumn} > ${lastValue}`); - - // Get incremental data from source - const incrementalData = await this.getSourceDataAfter( - job.config.sourceConnection, - job.sourceTable, - watermarkColumn, - lastValue, - ); - - // Apply changes to target - const result = await this.applyChangesToTarget( - job.config.targetConnection, - job.targetTable, - incrementalData, - job.config.primaryKey, - 'watermark', - ); - - // Update watermark - if (incrementalData.length > 0) { - const maxValue = Math.max(...incrementalData.map((row) => row[watermarkColumn])); - const newWatermark: IWatermark = { - id: uuidv4(), - tableName: job.sourceTable, - columnName: watermarkColumn, - lastValue: maxValue, - lastUpdated: new Date(), - }; - this.watermarks.set(watermarkKey, newWatermark); - } - /** - * Get job status - */ - async getJobStatus(jobId: string): Promise { - return this.jobs.get(jobId) || null; - } - /** - * Get all jobs - */ - async getAllJobs(): Promise { - return Array.from(this.jobs.values()); - } - /** - * Get watermark for a table and column - */ - async getWatermark(tableName: string, columnName: string): Promise { - const key = `${tableName}_${columnName}`; - return this.watermarks.get(key) || null; - } - /** - * Set watermark manually - */ - async setWatermark(tableName: string, columnName: string, value: unknown): Promise { - const key = `${tableName}_${columnName}`; - const watermark: Watermark = { - id: uuidv4(), - tableName, - columnName, - lastValue: value, - lastUpdated: new Date(), - }; - this.watermarks.set(key, watermark); - this.logger.log(`Set watermark for ${tableName}.${columnName} = ${value}`); - return watermark; - } - /** - * Add CDC event - */ - async addCDCEvent(event: Omit): Promise { - const cdcEvent: CDCEvent = { - id: uuidv4(), - ...event, - timestamp: new Date(), - }; - const key = `${event.tableName}_cdc`; - const events = this.cdcEvents.get(key) || []; - events.push(cdcEvent); - this.cdcEvents.set(key, events); - this.logger.log(`Added CDC event for ${event.tableName}: ${event.operation}`); - return cdcEvent; - } - /** - * Get CDC events for a table - */ - async getCDCEvents(tableName: string): Promise { - const key = `${tableName}_cdc`; - return this.cdcEvents.get(key) || []; - } - // Helper methods for data operations - private async getSourceDataSince(_connection: DataSourceConfig, table: string, column: string, timestamp: Date): Promise { - // Implementation would connect to source database and query - this.logger.log(`Querying ${table} where ${column} > ${timestamp.toISOString()}`); - return []; // Placeholder - } - private async getSourceDataAfter(_connection: DataSourceConfig, table: string, column: string, value: unknown): Promise { - // Implementation would connect to source database and query - this.logger.log(`Querying ${table} where ${column} > ${value}`); - return []; // Placeholder - } - private async applyChangesToTarget(_connection: DataSourceConfig, table: string, data: unknown[], _primaryKey: string[], loadType: string): Promise<{ - processed: number; - inserted: number; - updated: number; - }> { - // Implementation would apply changes to target database - this.logger.log(`Applying ${data.length} records to ${table} using ${loadType} strategy`); - return { - processed: data.length, - inserted: data.length, // Simplified logic - updated: 0, - }; - } - private async insertRecord(_connection: DataSourceConfig, table: string, _values: { - [key: string]: unknown; - }): Promise { - // Implementation would insert record into target - this.logger.log(`Inserting record into ${table}`); - } - private async validateRecord(_values: unknown): Promise { - // Implementation would validate record - return true; - } - private async updateRecord(_connection: DataSourceConfig, table: string, primaryKey: { - [key: string]: unknown; - }, _values: { - [key: string]: unknown; - }): Promise { - // Implementation would update record in target - this.logger.log(`Updating record in ${table} where ${JSON.stringify(primaryKey)}`); - } - private async deleteRecord(_connection: DataSourceConfig, table: string, primaryKey: { - [key: string]: unknown; - }): Promise { - // Implementation would delete record from target - this.logger.log(`Deleting record from ${table} where ${JSON.stringify(primaryKey)}`); - } - - // Clear processed events - this.cdcEvents.delete(cdcKey); - - return { - processed: events.length, - inserted, - updated, - deleted, - }; - } - - /** - * Get job status - */ - async getJobStatus(jobId: string): Promise { - return this.jobs.get(jobId) || null; - } - - /** - * Get all jobs - */ - async getAllJobs(): Promise { - return Array.from(this.jobs.values()); - } - - /** - * Get watermark for a table and column - */ - async getWatermark(tableName: string, columnName: string): Promise { - const key = `${tableName}_${columnName}`; - return this.watermarks.get(key) || null; - } - - /** - * Set watermark manually - */ - async setWatermark(tableName: string, columnName: string, value: any): Promise { - const key = `${tableName}_${columnName}`; - const watermark: IWatermark = { - id: uuidv4(), - tableName, - columnName, - lastValue: value, - lastUpdated: new Date(), - }; - - this.watermarks.set(key, watermark); - this.logger.log(`Set watermark for ${tableName}.${columnName} = ${value}`); - - return watermark; - } - - /** - * Add CDC event - */ - async addCDCEvent(event: Omit): Promise { - const cdcEvent: ICDCEvent = { - id: uuidv4(), - ...event, - timestamp: new Date(), - }; - - const key = `${event.tableName}_cdc`; - const events = this.cdcEvents.get(key) || []; - events.push(cdcEvent); - this.cdcEvents.set(key, events); - - this.logger.log(`Added CDC event for ${event.tableName}: ${event.operation}`); - - return cdcEvent; - } - - /** - * Get CDC events for a table - */ - async getCDCEvents(tableName: string): Promise { - const key = `${tableName}_cdc`; - return this.cdcEvents.get(key) || []; - } - - // Helper methods for data operations - private async getSourceDataSince( - _connection: IDataSourceConfig, - table: string, - column: string, - timestamp: Date, - ): Promise { - // Implementation would connect to source database and query - this.logger.log(`Querying ${table} where ${column} > ${timestamp.toISOString()}`); - return []; // Placeholder - } - - private async getSourceDataAfter( - _connection: IDataSourceConfig, - table: string, - column: string, - value: any, - ): Promise { - // Implementation would connect to source database and query - this.logger.log(`Querying ${table} where ${column} > ${value}`); - return []; // Placeholder - } - - private async applyChangesToTarget( - _connection: IDataSourceConfig, - table: string, - data: any[], - _primaryKey: string[], - loadType: string, - ): Promise<{ processed: number; inserted: number; updated: number }> { - // Implementation would apply changes to target database - this.logger.log(`Applying ${data.length} records to ${table} using ${loadType} strategy`); - - return { - processed: data.length, - inserted: data.length, // Simplified logic - updated: 0, - }; - } - - private async insertRecord( - _connection: IDataSourceConfig, - table: string, - _values: { [key: string]: any }, - ): Promise { - // Implementation would insert record into target - this.logger.log(`Inserting record into ${table}`); - } - - private async validateRecord(_values: any): Promise { - // Implementation would validate record - return true; - } - - private async updateRecord( - _connection: IDataSourceConfig, - table: string, - primaryKey: { [key: string]: any }, - _values: { [key: string]: any }, - ): Promise { - // Implementation would update record in target - this.logger.log(`Updating record in ${table} where ${JSON.stringify(primaryKey)}`); - } - - private async deleteRecord( - _connection: IDataSourceConfig, - table: string, - primaryKey: { [key: string]: any }, - ): Promise { - // Implementation would delete record from target - this.logger.log(`Deleting record from ${table} where ${JSON.stringify(primaryKey)}`); - } -} diff --git a/src/data-warehouse/modeling/dimensional-modeling.service.ts b/src/data-warehouse/modeling/dimensional-modeling.service.ts deleted file mode 100644 index 3913bfb4..00000000 --- a/src/data-warehouse/modeling/dimensional-modeling.service.ts +++ /dev/null @@ -1,468 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; - -export interface IDimensionalModel { - id: string; - name: string; - type: 'star' | 'snowflake' | 'galaxy'; - factTables: IFactTable[]; - dimensionTables: IDimensionTable[]; - relationships: IRelationship[]; - createdAt: Date; - updatedAt: Date; -} - -export interface IFactTable { - id: string; - name: string; - description: string; - measures: IMeasure[]; - foreignKeys: IForeignKey[]; - granularity: string; -} - -export interface IDimensionTable { - id: string; - name: string; - description: string; - attributes: IDimensionAttribute[]; - hierarchy?: IDimensionHierarchy; - type: 'conformed' | 'degenerate' | 'junk' | 'role-playing'; -} - -export interface IMeasure { - id: string; - name: string; - description: string; - dataType: string; - aggregationType: 'sum' | 'count' | 'avg' | 'min' | 'max'; - formula?: string; -} - -export interface IDimensionAttribute { - id: string; - name: string; - description: string; - dataType: string; - isKey: boolean; - isNullable: boolean; -} - -export interface IForeignKey { - id: string; - name: string; - referencedTable: string; - referencedColumn: string; -} - -export interface IRelationship { - id: string; - fromTable: string; - toTable: string; - relationshipType: 'one-to-one' | 'one-to-many' | 'many-to-many'; - joinCondition: string; -} - -export interface IDimensionHierarchy { - levels: IHierarchyLevel[]; - rollupPaths: string[][]; -} - -export interface IHierarchyLevel { - id: string; - name: string; - level: number; - attributes: string[]; -} - -export interface IAnalyticsQuery { - id: string; - name: string; - description: string; - modelId: string; - query: string; - parameters: IQueryParameter[]; - metrics: string[]; - dimensions: string[]; - filters: IQueryFilter[]; -} - -export interface IQueryParameter { - name: string; - type: string; - defaultValue?: any; - required: boolean; -} - -export interface IQueryFilter { - field: string; - operator: string; - value: any; -} - -/** - * Provides dimensional Modeling operations. - */ -@Injectable() -export class DimensionalModelingService { - private readonly logger = new Logger(DimensionalModelingService.name); - private models: Map = new Map(); - private queries: Map = new Map(); - - /** - * Create a new dimensional model - */ - async createModel( - modelConfig: Omit, - ): Promise { - const modelId = uuidv4(); - const model: IDimensionalModel = { - id: modelId, - ...modelConfig, - createdAt: new Date(), - updatedAt: new Date(), - }; - - this.models.set(modelId, model); - this.logger.log(`Created dimensional model ${modelId}: ${model.name}`); - - return model; - } - - /** - * Get a dimensional model - */ - async getModel(modelId: string): Promise { - return this.models.get(modelId) || null; - } - - /** - * Get all dimensional models - */ - async getAllModels(): Promise { - return Array.from(this.models.values()); - } - - /** - * Update a dimensional model - */ - async updateModel( - modelId: string, - updates: Partial, - ): Promise { - const model = this.models.get(modelId); - if (!model) { - return null; - } - /** - * Get a dimensional model - */ - async getModel(modelId: string): Promise { - return this.models.get(modelId) || null; - } - return exists; - } - - /** - * Create a star schema model - */ - async createStarSchema( - name: string, - factTable: Omit, - dimensionTables: Array>, - ): Promise { - const factTableWithId: IFactTable = { - ...factTable, - id: uuidv4(), - }; - - const dimensionTablesWithIds: IDimensionTable[] = dimensionTables.map((dim) => ({ - ...dim, - id: uuidv4(), - })); - - // Create foreign keys for each dimension - const foreignKeys: IForeignKey[] = dimensionTablesWithIds.map((dim) => ({ - id: uuidv4(), - name: `${dim.name}_id`, - referencedTable: dim.name, - referencedColumn: 'id', - })); - - factTableWithId.foreignKeys = foreignKeys; - - const model = await this.createModel({ - name, - type: 'star', - factTables: [factTableWithId], - dimensionTables: dimensionTablesWithIds, - relationships: this.createStarRelationships(factTableWithId, dimensionTablesWithIds), - }); - - return model; - } - - /** - * Create a snowflake schema model - */ - async createSnowflakeSchema( - name: string, - factTable: Omit, - dimensionTables: Array>, - subDimensions: { [key: string]: Array> }, - ): Promise { - const factTableWithId: IFactTable = { - ...factTable, - id: uuidv4(), - }; - - const dimensionTablesWithIds: IDimensionTable[] = dimensionTables.map((dim) => ({ - ...dim, - id: uuidv4(), - })); - - // Process sub-dimensions - const allDimensions: IDimensionTable[] = [...dimensionTablesWithIds]; - const relationships: IRelationship[] = []; - - for (const [parentDimName, subDims] of Object.entries(subDimensions)) { - const parentDim = dimensionTablesWithIds.find((d) => d.name === parentDimName); - if (parentDim) { - const subDimWithIds = subDims.map((sub) => ({ - ...sub, - id: uuidv4(), - })); - - allDimensions.push(...subDimWithIds); - - // Create relationships between parent and sub-dimensions - subDimWithIds.forEach((subDim) => { - relationships.push({ - id: uuidv4(), - }; - const dimensionTablesWithIds: DimensionTable[] = dimensionTables.map((dim) => ({ - ...dim, - id: uuidv4(), - })); - // Create foreign keys for each dimension - const foreignKeys: ForeignKey[] = dimensionTablesWithIds.map((dim) => ({ - id: uuidv4(), - name: `${dim.name}_id`, - referencedTable: dim.name, - referencedColumn: 'id', - })); - factTableWithId.foreignKeys = foreignKeys; - const model = await this.createModel({ - name, - type: 'star', - factTables: [factTableWithId], - dimensionTables: dimensionTablesWithIds, - relationships: this.createStarRelationships(factTableWithId, dimensionTablesWithIds), - }); - return model; - } - - // Create foreign keys for fact table - const foreignKeys: IForeignKey[] = allDimensions.map((dim) => ({ - id: uuidv4(), - name: `${dim.name}_id`, - referencedTable: dim.name, - referencedColumn: 'id', - })); - - factTableWithId.foreignKeys = foreignKeys; - - const model = await this.createModel({ - name, - type: 'snowflake', - factTables: [factTableWithId], - dimensionTables: allDimensions, - relationships: [ - ...this.createStarRelationships(factTableWithId, allDimensions), - ...relationships, - ], - }); - - return model; - } - - /** - * Create an analytics query - */ - async createQuery(queryConfig: Omit): Promise { - const queryId = uuidv4(); - const query: IAnalyticsQuery = { - id: queryId, - ...queryConfig, - }; - - this.queries.set(queryId, query); - this.logger.log(`Created analytics query ${queryId}: ${query.name}`); - - return query; - } - - /** - * Execute an analytics query - */ - async executeQuery(queryId: string, parameters: { [key: string]: any } = {}): Promise { - const query = this.queries.get(queryId); - if (!query) { - throw new Error(`Query ${queryId} not found`); - } - /** - * Create an analytics query - */ - async createQuery(queryConfig: Omit): Promise { - const queryId = uuidv4(); - const query: AnalyticsQuery = { - id: queryId, - ...queryConfig, - }; - this.queries.set(queryId, query); - this.logger.log(`Created analytics query ${queryId}: ${query.name}`); - return query; - } - - // In a real implementation, this would execute against the data warehouse - this.logger.log(`Executing query ${queryId} with parameters:`, parameters); - - // Return mock data for demonstration - return this.generateMockResults(query, parameters); - } - - /** - * Get query results with pagination - */ - async getQueryResults( - queryId: string, - parameters: { [key: string]: any } = {}, - page: number = 1, - limit: number = 100, - ): Promise<{ data: any[]; total: number; page: number; limit: number }> { - const results = await this.executeQuery(queryId, parameters); - const total = results.length; - const startIndex = (page - 1) * limit; - const endIndex = startIndex + limit; - const paginatedData = results.slice(startIndex, endIndex); - - return { - data: paginatedData, - total, - page, - limit, - }; - } - - /** - * Get all queries for a model - */ - async getQueriesForModel(modelId: string): Promise { - const queries = Array.from(this.queries.values()); - return queries.filter((query) => query.modelId === modelId); - } - - /** - * Validate model integrity - */ - async validateModel(modelId: string): Promise<{ valid: boolean; errors: string[] }> { - const model = this.models.get(modelId); - if (!model) { - return { valid: false, errors: [`Model ${modelId} not found`] }; - } - /** - * Get query results with pagination - */ - async getQueryResults(queryId: string, parameters: { - [key: string]: unknown; - } = {}, page: number = 1, limit: number = 100): Promise<{ - data: unknown[]; - total: number; - page: number; - limit: number; - }> { - const results = await this.executeQuery(queryId, parameters); - const total = results.length; - const startIndex = (page - 1) * limit; - const endIndex = startIndex + limit; - const paginatedData = results.slice(startIndex, endIndex); - return { - data: paginatedData, - total, - page, - limit, - }; - } - /** - * Get all queries for a model - */ - async getQueriesForModel(modelId: string): Promise { - const queries = Array.from(this.queries.values()); - return queries.filter((query) => query.modelId === modelId); - } - - // Validate relationships - for (const relationship of model.relationships) { - const fromTable = this.findTableByName(model, relationship.fromTable); - const toTable = this.findTableByName(model, relationship.toTable); - - if (!fromTable) { - errors.push(`IRelationship references non-existent table: ${relationship.fromTable}`); - } - - if (!toTable) { - errors.push(`IRelationship references non-existent table: ${relationship.toTable}`); - } - } - - return { - valid: errors.length === 0, - errors, - }; - } - - // Helper methods - private createStarRelationships( - factTable: IFactTable, - dimensionTables: IDimensionTable[], - ): IRelationship[] { - return dimensionTables.map((dim) => ({ - id: uuidv4(), - fromTable: factTable.name, - toTable: dim.name, - relationshipType: 'one-to-many', - joinCondition: `${factTable.name}.${dim.name}_id = ${dim.name}.id`, - })); - } - - private findTableByName( - model: IDimensionalModel, - tableName: string, - ): IFactTable | IDimensionTable | undefined { - const factTable = model.factTables.find((ft) => ft.name === tableName); - if (factTable) return factTable; - - return model.dimensionTables.find((dt) => dt.name === tableName); - } - - private generateMockResults(query: IAnalyticsQuery, _parameters: { [key: string]: any }): any[] { - // Generate mock data based on query configuration - const results: any[] = []; - const rowCount = Math.floor(Math.random() * 100) + 10; // 10-110 rows - - for (let i = 0; i < rowCount; i++) { - const row: any = {}; - - // Add metrics - query.metrics.forEach((metric) => { - row[metric] = Math.floor(Math.random() * 10000); - }); - - // Add dimensions - query.dimensions.forEach((dimension) => { - row[dimension] = `Value_${dimension}_${i}`; - }); - - results.push(row); - } -} diff --git a/src/data-warehouse/quality/data-quality.service.ts b/src/data-warehouse/quality/data-quality.service.ts deleted file mode 100644 index 22fff649..00000000 --- a/src/data-warehouse/quality/data-quality.service.ts +++ /dev/null @@ -1,747 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; - -export interface IDataQualityProfile { - id: string; - name: string; - description: string; - rules: IDataQualityRule[]; - createdAt: Date; - updatedAt: Date; -} - -export interface IDataQualityRule { - id: string; - name: string; - description: string; - type: 'completeness' | 'accuracy' | 'consistency' | 'uniqueness' | 'validity' | 'timeliness'; - field: string; - condition: string; - threshold: number; - severity: 'critical' | 'high' | 'medium' | 'low'; -} - -export interface IDataQualityCheck { - id: string; - profileId: string; - startTime: Date; - endTime?: Date; - status: 'pending' | 'running' | 'completed' | 'failed'; - results: IDataQualityResult[]; - summary: IDataQualitySummary; -} - -export interface IDataQualityResult { - ruleId: string; - ruleName: string; - passed: boolean; - actualValue: number; - expectedValue: number; - message: string; - sampleData?: any[]; -} - -export interface IDataQualitySummary { - totalRules: number; - passedRules: number; - failedRules: number; - overallScore: number; - criticalFailures: number; - issuesBySeverity: { - critical: number; - high: number; - medium: number; - low: number; - }; -} - -export interface IDataQualityIssue { - id: string; - profileId: string; - ruleId: string; - severity: string; - description: string; - affectedRecords: number; - sampleRecords: any[]; - createdAt: Date; - resolved: boolean; - resolvedAt?: Date; - resolvedBy?: string; -} - -/** - * Provides data Quality operations. - */ -@Injectable() -export class DataQualityService { - private readonly logger = new Logger(DataQualityService.name); - private profiles: Map = new Map(); - private checks: Map = new Map(); - private issues: Map = new Map(); - - /** - * Create a data quality profile - */ - async createProfile( - profileConfig: Omit, - ): Promise { - const profileId = uuidv4(); - const profile: IDataQualityProfile = { - id: profileId, - ...profileConfig, - createdAt: new Date(), - updatedAt: new Date(), - }; - - this.profiles.set(profileId, profile); - this.logger.log(`Created data quality profile ${profileId}: ${profile.name}`); - - return profile; - } - - /** - * Get a data quality profile - */ - async getProfile(profileId: string): Promise { - return this.profiles.get(profileId) || null; - } - - /** - * Get all data quality profiles - */ - async getAllProfiles(): Promise { - return Array.from(this.profiles.values()); - } - - /** - * Update a data quality profile - */ - async updateProfile( - profileId: string, - updates: Partial, - ): Promise { - const profile = this.profiles.get(profileId); - if (!profile) { - return null; - } - /** - * Get a data quality profile - */ - async getProfile(profileId: string): Promise { - return this.profiles.get(profileId) || null; - } - return exists; - } - - /** - * Run data quality checks - */ - async runQualityChecks(profileId: string, data: any[]): Promise { - const profile = this.profiles.get(profileId); - if (!profile) { - throw new Error(`Profile ${profileId} not found`); - } - - const checkId = uuidv4(); - const check: IDataQualityCheck = { - id: checkId, - profileId, - startTime: new Date(), - status: 'running', - results: [], - summary: { - totalRules: 0, - passedRules: 0, - failedRules: 0, - overallScore: 0, - criticalFailures: 0, - issuesBySeverity: { - critical: 0, - high: 0, - medium: 0, - low: 0, - }, - }, - }; - - this.checks.set(checkId, check); - - try { - // Execute each rule - const results: IDataQualityResult[] = []; - let passedRules = 0; - let failedRules = 0; - let criticalFailures = 0; - const issuesBySeverity = { critical: 0, high: 0, medium: 0, low: 0 }; - - for (const rule of profile.rules) { - const result = await this.executeRule(rule, data); - results.push(result); - - if (result.passed) { - passedRules++; - } else { - failedRules++; - - // Count severity issues - issuesBySeverity[rule.severity]++; - - if (rule.severity === 'critical') { - criticalFailures++; - } - - // Create quality issue record - await this.createQualityIssue(profileId, rule, result); - } - const updatedProfile = { - ...profile, - ...updates, - updatedAt: new Date(), - }; - this.profiles.set(profileId, updatedProfile); - this.logger.log(`Updated data quality profile ${profileId}`); - return updatedProfile; - } - - return check; - } - - /** - * Get quality check results - */ - async getCheckResults(checkId: string): Promise { - return this.checks.get(checkId) || null; - } - - /** - * Get all quality checks for a profile - */ - async getChecksForProfile(profileId: string): Promise { - const checks = Array.from(this.checks.values()); - return checks.filter((check) => check.profileId === profileId); - } - - /** - * Create a quality issue - */ - private async createQualityIssue( - profileId: string, - rule: IDataQualityRule, - result: IDataQualityResult, - ): Promise { - const issueId = uuidv4(); - const issue: IDataQualityIssue = { - id: issueId, - profileId, - ruleId: rule.id, - severity: rule.severity, - description: result.message, - affectedRecords: result.sampleData?.length || 0, - sampleRecords: result.sampleData || [], - createdAt: new Date(), - resolved: false, - }; - - this.issues.set(issueId, issue); - this.logger.log(`Created quality issue ${issueId} for rule ${rule.name}`); - - return issue; - } - - /** - * Get quality issues - */ - async getQualityIssues( - profileId?: string, - severity?: string, - resolved?: boolean, - ): Promise { - let issues = Array.from(this.issues.values()); - - if (profileId) { - issues = issues.filter((issue) => issue.profileId === profileId); - } - - if (severity) { - issues = issues.filter((issue) => issue.severity === severity); - } - - if (resolved !== undefined) { - issues = issues.filter((issue) => issue.resolved === resolved); - } - - return issues; - } - - /** - * Resolve a quality issue - */ - async resolveIssue(issueId: string, resolvedBy: string): Promise { - const issue = this.issues.get(issueId); - if (!issue || issue.resolved) { - return false; - } - - issue.resolved = true; - issue.resolvedAt = new Date(); - issue.resolvedBy = resolvedBy; - - this.logger.log(`Resolved quality issue ${issueId} by ${resolvedBy}`); - return true; - } - - /** - * Create standard quality profiles - */ - async createStandardProfiles(): Promise { - const profiles: IDataQualityProfile[] = []; - - // Completeness profile - profiles.push( - await this.createProfile({ - name: 'Completeness Check', - description: 'Check for missing or null values in critical fields', - rules: [ - { - id: uuidv4(), - name: 'User Email Completeness', - description: 'Ensure user email addresses are not null or empty', - type: 'completeness', - field: 'email', - condition: 'not null', - threshold: 99.5, - severity: 'high', - }, - { - id: uuidv4(), - name: 'Post Content Completeness', - description: 'Ensure post content is not empty', - type: 'completeness', - field: 'content', - condition: 'not empty', - threshold: 98, - severity: 'medium', - }, - ], - }), - ); - - // Uniqueness profile - profiles.push( - await this.createProfile({ - name: 'Uniqueness Check', - description: 'Check for duplicate or duplicate-like values', - rules: [ - { - id: uuidv4(), - name: 'User ID Uniqueness', - description: 'Ensure user IDs are unique', - type: 'uniqueness', - field: 'id', - condition: 'unique', - threshold: 100, - severity: 'critical', - }, - { - id: uuidv4(), - name: 'Email Uniqueness', - description: 'Ensure email addresses are unique', - type: 'uniqueness', - field: 'email', - condition: 'unique', - threshold: 99.9, - severity: 'high', - }, - ], - }), - ); - - // Validity profile - profiles.push( - await this.createProfile({ - name: 'Validity Check', - description: 'Check data against business rules and constraints', - rules: [ - { - id: uuidv4(), - name: 'Email Format Validation', - description: 'Validate email format', - type: 'validity', - field: 'email', - condition: 'valid email format', - threshold: 99, - severity: 'high', - }, - { - id: uuidv4(), - name: 'Date Range Validation', - description: 'Validate dates are within reasonable ranges', - type: 'validity', - field: 'created_at', - condition: 'within last 5 years', - threshold: 99.5, - severity: 'medium', - }, - ], - }), - ); - - return profiles; - } - - /** - * Execute a single quality rule - */ - private async executeRule(rule: IDataQualityRule, data: any[]): Promise { - let passed = true; - let actualValue = 0; - let sampleData: any[] = []; - let message = ''; - - switch (rule.type) { - case 'completeness': - actualValue = this.calculateCompleteness(data, rule.field); - passed = actualValue >= rule.threshold; - message = `Completeness of ${rule.field}: ${actualValue.toFixed(2)}% (threshold: ${rule.threshold}%)`; - if (!passed) { - sampleData = data.filter((item) => !item[rule.field] || item[rule.field] === ''); - } - return exists; - } - /** - * Run data quality checks - */ - async runQualityChecks(profileId: string, data: unknown[]): Promise { - const profile = this.profiles.get(profileId); - if (!profile) { - throw new Error(`Profile ${profileId} not found`); - } - const checkId = uuidv4(); - const check: DataQualityCheck = { - id: checkId, - profileId, - startTime: new Date(), - status: 'running', - results: [], - summary: { - totalRules: 0, - passedRules: 0, - failedRules: 0, - overallScore: 0, - criticalFailures: 0, - issuesBySeverity: { - critical: 0, - high: 0, - medium: 0, - low: 0, - }, - }, - }; - this.checks.set(checkId, check); - try { - // Execute each rule - const results: DataQualityResult[] = []; - let passedRules = 0; - let failedRules = 0; - let criticalFailures = 0; - const issuesBySeverity = { critical: 0, high: 0, medium: 0, low: 0 }; - for (const rule of profile.rules) { - const result = await this.executeRule(rule, data); - results.push(result); - if (result.passed) { - passedRules++; - } - else { - failedRules++; - // Count severity issues - issuesBySeverity[rule.severity]++; - if (rule.severity === 'critical') { - criticalFailures++; - } - // Create quality issue record - await this.createQualityIssue(profileId, rule, result); - } - } - // Calculate overall score - const overallScore = profile.rules.length > 0 ? (passedRules / profile.rules.length) * 100 : 0; - // Update check with results - check.results = results; - check.status = 'completed'; - check.endTime = new Date(); - check.summary = { - totalRules: profile.rules.length, - passedRules, - failedRules, - overallScore, - criticalFailures, - issuesBySeverity, - }; - this.logger.log(`Data quality check ${checkId} completed with score: ${overallScore}%`); - } - catch (error) { - this.logger.error(`Data quality check ${checkId} failed: ${error.message}`); - check.status = 'failed'; - check.endTime = new Date(); - } - return check; - } - /** - * Get quality check results - */ - async getCheckResults(checkId: string): Promise { - return this.checks.get(checkId) || null; - } - /** - * Get all quality checks for a profile - */ - async getChecksForProfile(profileId: string): Promise { - const checks = Array.from(this.checks.values()); - return checks.filter((check) => check.profileId === profileId); - } - /** - * Create a quality issue - */ - private async createQualityIssue(profileId: string, rule: DataQualityRule, result: DataQualityResult): Promise { - const issueId = uuidv4(); - const issue: DataQualityIssue = { - id: issueId, - profileId, - ruleId: rule.id, - severity: rule.severity, - description: result.message, - affectedRecords: result.sampleData?.length || 0, - sampleRecords: result.sampleData || [], - createdAt: new Date(), - resolved: false, - }; - this.issues.set(issueId, issue); - this.logger.log(`Created quality issue ${issueId} for rule ${rule.name}`); - return issue; - } - /** - * Get quality issues - */ - async getQualityIssues(profileId?: string, severity?: string, resolved?: boolean): Promise { - let issues = Array.from(this.issues.values()); - if (profileId) { - issues = issues.filter((issue) => issue.profileId === profileId); - } - if (severity) { - issues = issues.filter((issue) => issue.severity === severity); - } - if (resolved !== undefined) { - issues = issues.filter((issue) => issue.resolved === resolved); - } - return issues; - } - /** - * Resolve a quality issue - */ - async resolveIssue(issueId: string, resolvedBy: string): Promise { - const issue = this.issues.get(issueId); - if (!issue || issue.resolved) { - return false; - } - issue.resolved = true; - issue.resolvedAt = new Date(); - issue.resolvedBy = resolvedBy; - this.logger.log(`Resolved quality issue ${issueId} by ${resolvedBy}`); - return true; - } - /** - * Create standard quality profiles - */ - async createStandardProfiles(): Promise { - const profiles: DataQualityProfile[] = []; - // Completeness profile - profiles.push(await this.createProfile({ - name: 'Completeness Check', - description: 'Check for missing or null values in critical fields', - rules: [ - { - id: uuidv4(), - name: 'User Email Completeness', - description: 'Ensure user email addresses are not null or empty', - type: 'completeness', - field: 'email', - condition: 'not null', - threshold: 99.5, - severity: 'high', - }, - { - id: uuidv4(), - name: 'Post Content Completeness', - description: 'Ensure post content is not empty', - type: 'completeness', - field: 'content', - condition: 'not empty', - threshold: 98, - severity: 'medium', - }, - ], - })); - // Uniqueness profile - profiles.push(await this.createProfile({ - name: 'Uniqueness Check', - description: 'Check for duplicate or duplicate-like values', - rules: [ - { - id: uuidv4(), - name: 'User ID Uniqueness', - description: 'Ensure user IDs are unique', - type: 'uniqueness', - field: 'id', - condition: 'unique', - threshold: 100, - severity: 'critical', - }, - { - id: uuidv4(), - name: 'Email Uniqueness', - description: 'Ensure email addresses are unique', - type: 'uniqueness', - field: 'email', - condition: 'unique', - threshold: 99.9, - severity: 'high', - }, - ], - })); - // Validity profile - profiles.push(await this.createProfile({ - name: 'Validity Check', - description: 'Check data against business rules and constraints', - rules: [ - { - id: uuidv4(), - name: 'Email Format Validation', - description: 'Validate email format', - type: 'validity', - field: 'email', - condition: 'valid email format', - threshold: 99, - severity: 'high', - }, - { - id: uuidv4(), - name: 'Date Range Validation', - description: 'Validate dates are within reasonable ranges', - type: 'validity', - field: 'created_at', - condition: 'within last 5 years', - threshold: 99.5, - severity: 'medium', - }, - ], - })); - return profiles; - } - /** - * Execute a single quality rule - */ - private async executeRule(rule: DataQualityRule, data: unknown[]): Promise { - let passed = true; - let actualValue = 0; - let sampleData: unknown[] = []; - let message = ''; - switch (rule.type) { - case 'completeness': - actualValue = this.calculateCompleteness(data, rule.field); - passed = actualValue >= rule.threshold; - message = `Completeness of ${rule.field}: ${actualValue.toFixed(2)}% (threshold: ${rule.threshold}%)`; - if (!passed) { - sampleData = data.filter((item) => !item[rule.field] || item[rule.field] === ''); - } - break; - case 'uniqueness': - actualValue = this.calculateUniqueness(data, rule.field); - passed = actualValue >= rule.threshold; - message = `Uniqueness of ${rule.field}: ${actualValue.toFixed(2)}% (threshold: ${rule.threshold}%)`; - if (!passed) { - const duplicates = this.findDuplicates(data, rule.field); - sampleData = duplicates.slice(0, 10); // Sample first 10 duplicates - } - break; - case 'validity': - actualValue = this.calculateValidity(data, rule.field, rule.condition); - passed = actualValue >= rule.threshold; - message = `Validity of ${rule.field}: ${actualValue.toFixed(2)}% (threshold: ${rule.threshold}%)`; - if (!passed) { - sampleData = data - .filter((item) => !this.isValid(item[rule.field], rule.condition)) - .slice(0, 10); - } - break; - default: - actualValue = 100; - message = `Rule type ${rule.type} not implemented`; - } - return { - ruleId: rule.id, - ruleName: rule.name, - passed, - actualValue, - expectedValue: rule.threshold, - message, - sampleData, - }; - } - // Helper methods for quality calculations - private calculateCompleteness(data: unknown[], field: string): number { - if (data.length === 0) - return 100; - const completeCount = data.filter((item) => item[field] !== null && item[field] !== undefined && item[field] !== '').length; - return (completeCount / data.length) * 100; - } - private calculateUniqueness(data: unknown[], field: string): number { - if (data.length === 0) - return 100; - const uniqueValues = new Set(data.map((item) => item[field])); - return (uniqueValues.size / data.length) * 100; - } - private calculateValidity(data: unknown[], field: string, condition: string): number { - if (data.length === 0) - return 100; - const validCount = data.filter((item) => this.isValid(item[field], condition)).length; - return (validCount / data.length) * 100; - } - private isValid(value: unknown, condition: string): boolean { - if (value === null || value === undefined) - return false; - switch (condition) { - case 'not null': - return value !== null && value !== undefined; - case 'not empty': - return value !== '' && value !== null && value !== undefined; - case 'valid email format': - return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); - case 'within last 5 years': { - const date = new Date(value); - const fiveYearsAgo = new Date(); - fiveYearsAgo.setFullYear(fiveYearsAgo.getFullYear() - 5); - return date >= fiveYearsAgo; - } - default: - return true; - } - } - private findDuplicates(data: unknown[], field: string): unknown[] { - const seen = new Map(); - const duplicates: unknown[] = []; - data.forEach((item) => { - const value = item[field]; - if (seen.has(value)) { - if (seen.get(value) === 1) { - duplicates.push(seen.get('items')[0]); - } - duplicates.push(item); - seen.set(value, (seen.get(value) || 0) + 1); - } - else { - seen.set(value, 1); - seen.set('items', [item]); - } - }); - return duplicates; - } -} diff --git a/src/email-marketing/ab-testing/ab-testing.controller.ts b/src/email-marketing/ab-testing/ab-testing.controller.ts deleted file mode 100644 index 4e26183c..00000000 --- a/src/email-marketing/ab-testing/ab-testing.controller.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Controller, Get, Post, Body, Param, Query, ParseUUIDPipe } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; -import { ABTestingService } from './ab-testing.service'; -import { CreateABTestDto } from '../dto/create-ab-test.dto'; -import { ABTest } from '../entities/ab-test.entity'; - -/** - * Exposes AB testing endpoints. - */ -@ApiTags('Email Marketing - A/B Testing') -@ApiBearerAuth() -@Controller('email-marketing/ab-tests') -export class ABTestingController { - constructor(private readonly abTestingService: ABTestingService) {} - - /** - * Creates a new record. - * @param createABTestDto The request payload. - * @returns The resulting abtest. - */ - @Post() - @ApiOperation({ summary: 'Create a new A/B test' }) - @ApiResponse({ status: 201, description: 'A/B test created successfully' }) - async create(@Body() createABTestDto: CreateABTestDto): Promise { - return this.abTestingService.create(createABTestDto); - } - - /** - * Returns all. - * @param page The page number. - * @param limit The maximum number of results. - * @returns The operation result. - */ - @Get() - @ApiOperation({ summary: 'Get all A/B tests' }) - @ApiQuery({ name: 'page', required: false, type: Number }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - async findAll(@Query('page') page = 1, @Query('limit') limit = 10) { - return this.abTestingService.findAll(page, limit); - } - - /** - * Returns one. - * @param id The identifier. - * @returns The resulting abtest. - */ - @Get(':id') - @ApiOperation({ summary: 'Get an A/B test by ID' }) - @ApiResponse({ status: 404, description: 'A/B test not found' }) - async findOne(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.abTestingService.findOne(id); - } - - /** - * Starts test. - * @param id The identifier. - * @returns The resulting abtest. - */ - @Post(':id/start') - @ApiOperation({ summary: 'Start an A/B test' }) - async startTest(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.abTestingService.startTest(id); - } - - /** - * Returns results. - * @param id The identifier. - * @returns The operation result. - */ - @Get(':id/results') - @ApiOperation({ summary: 'Get A/B test results with statistical analysis' }) - async getResults(@Param('id', ParseUUIDPipe) id: string) { - return this.abTestingService.getTestResults(id); - } - - /** - * Executes declare Winner. - * @param id The identifier. - * @param variantId The variant identifier. - * @returns The resulting abtest. - */ - @Post(':id/winner/:variantId') - @ApiOperation({ summary: 'Declare a winner for the A/B test' }) - async declareWinner( - @Param('id', ParseUUIDPipe) id: string, - @Param('variantId', ParseUUIDPipe) variantId: string, - ): Promise { - return this.abTestingService.declareWinner(id, variantId); - } -} diff --git a/src/email-marketing/ab-testing/ab-testing.service.ts b/src/email-marketing/ab-testing/ab-testing.service.ts deleted file mode 100644 index ac8f99d9..00000000 --- a/src/email-marketing/ab-testing/ab-testing.service.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { ABTest } from '../entities/ab-test.entity'; -import { ABTestVariant } from '../entities/ab-test-variant.entity'; -import { EmailEvent } from '../entities/email-event.entity'; -import { CreateABTestDto } from '../dto/create-ab-test.dto'; -import { ABTestStatus } from '../enums/ab-test-status.enum'; -import { EmailEventType } from '../enums/email-event-type.enum'; - -export interface IVariantStats { - variantId: string; - name: string; - sent: number; - opened: number; - clicked: number; - openRate: number; - clickRate: number; - isWinner: boolean; - confidenceLevel: number; -} - -/** - * Provides aBTesting operations. - */ -@Injectable() -export class ABTestingService { - constructor( - @InjectRepository(ABTest) - private readonly abTestRepository: Repository, - @InjectRepository(ABTestVariant) - private readonly variantRepository: Repository, - @InjectRepository(EmailEvent) - private readonly eventRepository: Repository) { } - /** - * Create a new A/B test - */ - async create(createABTestDto: CreateABTestDto): Promise { - if (createABTestDto.variants.length < 2) { - throw new BadRequestException('A/B test requires at least 2 variants'); - } - const totalWeight = createABTestDto.variants.reduce((sum, v) => sum + (v.weight || 50), 0); - if (totalWeight !== 100) { - throw new BadRequestException('Variant weights must sum to 100'); - } - const abTest = this.abTestRepository.create({ - name: createABTestDto.name, - campaignId: createABTestDto.campaignId, - testField: createABTestDto.testField, - winnerCriteria: createABTestDto.winnerCriteria, - sampleSize: createABTestDto.sampleSize, - status: ABTestStatus.DRAFT, - }); - const savedTest = await this.abTestRepository.save(abTest); - const variants = createABTestDto.variants.map((v, index) => this.variantRepository.create({ - ...v, - abTestId: savedTest.id, - name: v.name || `Variant ${String.fromCharCode(65 + index)}`, - })); - await this.variantRepository.save(variants); - return this.findOne(savedTest.id); - } - /** - * Get all A/B tests - */ - async findAll(page = 1, limit = 10) { - const [tests, total] = await this.abTestRepository.findAndCount({ - skip: (page - 1) * limit, - take: limit, - order: { createdAt: 'DESC' }, - relations: ['variants'], - }); - return { tests, total, page, totalPages: Math.ceil(total / limit) }; - } - /** - * Get a single A/B test by ID - */ - async findOne(id: string): Promise { - const test = await this.abTestRepository.findOne({ - where: { id }, - relations: ['variants'], - }); - if (!test) { - throw new NotFoundException(`A/B test with ID ${id} not found`); - } - return test; - } - /** - * Start an A/B test - */ - async startTest(id: string): Promise { - const test = await this.findOne(id); - if (test.status !== ABTestStatus.DRAFT) { - throw new BadRequestException('Only draft tests can be started'); - } - test.status = ABTestStatus.RUNNING; - test.startedAt = new Date(); - return this.abTestRepository.save(test); - } - - test.status = ABTestStatus.RUNNING; - test.startedAt = new Date(); - return this.abTestRepository.save(test); - } - - /** - * Get test results with statistical analysis - */ - async getTestResults(id: string): Promise<{ - test: ABTest; - variants: IVariantStats[]; - isSignificant: boolean; - recommendedWinner: string | null; - }> { - const test = await this.findOne(id); - const variantStats: IVariantStats[] = []; - - for (const variant of test.variants) { - const sent = variant.recipientCount || 0; - const opened = await this.countVariantEvents(variant.id, EmailEventType.OPENED); - const clicked = await this.countVariantEvents(variant.id, EmailEventType.CLICKED); - - variantStats.push({ - variantId: variant.id, - name: variant.name, - sent, - opened, - clicked, - openRate: sent > 0 ? (opened / sent) * 100 : 0, - clickRate: opened > 0 ? (clicked / opened) * 100 : 0, - isWinner: false, - confidenceLevel: 0, - }); - } - /** - * Declare a winner and end the test - */ - async declareWinner(testId: string, variantId: string): Promise { - const test = await this.findOne(testId); - if (test.status !== ABTestStatus.RUNNING) { - throw new BadRequestException('Only running tests can have a winner declared'); - } - const variant = test.variants.find((v) => v.id === variantId); - if (!variant) { - throw new BadRequestException('Variant not found in this test'); - } - test.status = ABTestStatus.COMPLETED; - test.winnerId = variantId; - test.endedAt = new Date(); - return this.abTestRepository.save(test); - } - /** - * Select variant for a recipient (weighted random) - */ - selectVariantForRecipient(test: ABTest): ABTestVariant { - const random = Math.random() * 100; - let cumulative = 0; - for (const variant of test.variants) { - cumulative += variant.weight; - if (random <= cumulative) { - return variant; - } - } - return test.variants[0]; - } - // Private helper methods - private async countVariantEvents(variantId: string, eventType: EmailEventType): Promise { - return this.eventRepository.count({ - where: { metadata: { variantId }, eventType }, - }); - } - private calculateSignificance(variants: VariantStats[], criteria: string): { - isSignificant: boolean; - winner: string | null; - confidenceLevel: number; - } { - if (variants.length < 2) { - return { isSignificant: false, winner: null, confidenceLevel: 0 }; - } - // Sort by the winning criteria - const sorted = [...variants].sort((a, b) => { - const metricA = criteria === 'click_rate' ? a.clickRate : a.openRate; - const metricB = criteria === 'click_rate' ? b.clickRate : b.openRate; - return metricB - metricA; - }); - const best = sorted[0]; - const second = sorted[1]; - // Simple z-test approximation - const n1 = best.sent; - const n2 = second.sent; - const p1 = criteria === 'click_rate' ? best.clickRate / 100 : best.openRate / 100; - const p2 = criteria === 'click_rate' ? second.clickRate / 100 : second.openRate / 100; - if (n1 < 30 || n2 < 30) { - return { isSignificant: false, winner: null, confidenceLevel: 0 }; - } - const pooledP = (p1 * n1 + p2 * n2) / (n1 + n2); - const se = Math.sqrt(pooledP * (1 - pooledP) * (1 / n1 + 1 / n2)); - if (se === 0) { - return { isSignificant: false, winner: null, confidenceLevel: 0 }; - } - const zScore = Math.abs(p1 - p2) / se; - // Z-score to confidence level - let confidenceLevel = 0; - if (zScore >= 2.576) - confidenceLevel = 99; - else if (zScore >= 1.96) - confidenceLevel = 95; - else if (zScore >= 1.645) - confidenceLevel = 90; - return { - isSignificant: confidenceLevel >= 95, - winner: confidenceLevel >= 95 ? best.variantId : null, - confidenceLevel, - }; - } - - return test.variants[0]; - } - - // Private helper methods - private async countVariantEvents(variantId: string, eventType: EmailEventType): Promise { - return this.eventRepository.count({ - where: { metadata: { variantId }, eventType }, - }); - } - - private calculateSignificance( - variants: IVariantStats[], - criteria: string, - ): { isSignificant: boolean; winner: string | null; confidenceLevel: number } { - if (variants.length < 2) { - return { isSignificant: false, winner: null, confidenceLevel: 0 }; - } - - // Sort by the winning criteria - const sorted = [...variants].sort((a, b) => { - const metricA = criteria === 'click_rate' ? a.clickRate : a.openRate; - const metricB = criteria === 'click_rate' ? b.clickRate : b.openRate; - return metricB - metricA; - }); - - const best = sorted[0]; - const second = sorted[1]; - - // Simple z-test approximation - const n1 = best.sent; - const n2 = second.sent; - const p1 = criteria === 'click_rate' ? best.clickRate / 100 : best.openRate / 100; - const p2 = criteria === 'click_rate' ? second.clickRate / 100 : second.openRate / 100; - - if (n1 < 30 || n2 < 30) { - return { isSignificant: false, winner: null, confidenceLevel: 0 }; - } - - const pooledP = (p1 * n1 + p2 * n2) / (n1 + n2); - const se = Math.sqrt(pooledP * (1 - pooledP) * (1 / n1 + 1 / n2)); - - if (se === 0) { - return { isSignificant: false, winner: null, confidenceLevel: 0 }; - } - - const zScore = Math.abs(p1 - p2) / se; - - // Z-score to confidence level - let confidenceLevel = 0; - if (zScore >= 2.576) confidenceLevel = 99; - else if (zScore >= 1.96) confidenceLevel = 95; - else if (zScore >= 1.645) confidenceLevel = 90; - - return { - isSignificant: confidenceLevel >= 95, - winner: confidenceLevel >= 95 ? best.variantId : null, - confidenceLevel, - }; - } -} diff --git a/src/email-marketing/analytics/email-analytics.controller.ts b/src/email-marketing/analytics/email-analytics.controller.ts deleted file mode 100644 index d8e67a80..00000000 --- a/src/email-marketing/analytics/email-analytics.controller.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Controller, Get, Param, Query, ParseUUIDPipe } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; -import { EmailAnalyticsService } from './email-analytics.service'; - -/** - * Exposes email Analytics endpoints. - */ -@ApiTags('Email Marketing - Analytics') -@ApiBearerAuth() -@Controller('email-marketing/analytics') -export class EmailAnalyticsController { - constructor(private readonly analyticsService: EmailAnalyticsService) {} - - /** - * Returns campaign Metrics. - * @param id The identifier. - * @returns The operation result. - */ - @Get('campaigns/:id') - @ApiOperation({ summary: 'Get campaign performance metrics' }) - @ApiResponse({ status: 200, description: 'Campaign metrics' }) - @ApiResponse({ status: 404, description: 'Campaign not found' }) - async getCampaignMetrics(@Param('id', ParseUUIDPipe) id: string) { - return this.analyticsService.getCampaignMetrics(id); - } - - /** - * Returns campaign Timeline. - * @param id The identifier. - * @param startDate The start date. - * @param endDate The end date. - * @returns The operation result. - */ - @Get('campaigns/:id/timeline') - @ApiOperation({ summary: 'Get campaign time series data' }) - @ApiQuery({ name: 'startDate', required: true, type: String }) - @ApiQuery({ name: 'endDate', required: true, type: String }) - async getCampaignTimeline( - @Param('id', ParseUUIDPipe) id: string, - @Query('startDate') startDate: string, - @Query('endDate') endDate: string, - ) { - return this.analyticsService.getCampaignTimeSeries(id, new Date(startDate), new Date(endDate)); - } - - /** - * Returns link Analytics. - * @param id The identifier. - * @returns The operation result. - */ - @Get('campaigns/:id/links') - @ApiOperation({ summary: 'Get link click analytics for a campaign' }) - async getLinkAnalytics(@Param('id', ParseUUIDPipe) id: string) { - return this.analyticsService.getLinkAnalytics(id); - } - - /** - * Returns overall Stats. - * @param startDate The start date. - * @param endDate The end date. - * @returns The operation result. - */ - @Get('overview') - @ApiOperation({ summary: 'Get overall email marketing statistics' }) - @ApiQuery({ name: 'startDate', required: false, type: String }) - @ApiQuery({ name: 'endDate', required: false, type: String }) - async getOverallStats( - @Query('startDate') startDate?: string, - @Query('endDate') endDate?: string, - ) { - return this.analyticsService.getOverallStats( - startDate ? new Date(startDate) : undefined, - endDate ? new Date(endDate) : undefined, - ); - } -} diff --git a/src/email-marketing/analytics/email-analytics.service.ts b/src/email-marketing/analytics/email-analytics.service.ts deleted file mode 100644 index 3d4731d3..00000000 --- a/src/email-marketing/analytics/email-analytics.service.ts +++ /dev/null @@ -1,481 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, Between } from 'typeorm'; -import { EmailEvent } from '../entities/email-event.entity'; -import { Campaign } from '../entities/campaign.entity'; -import { EmailEventType } from '../enums/email-event-type.enum'; - -export interface ICampaignMetrics { - sent: number; - delivered: number; - opened: number; - clicked: number; - bounced: number; - unsubscribed: number; - openRate: number; - clickRate: number; - bounceRate: number; -} - -export interface ITimeSeriesData { - date: string; - opens: number; - clicks: number; - bounces: number; -} - -/** - * Provides email Analytics operations. - */ -@Injectable() -export class EmailAnalyticsService { - constructor( - @InjectRepository(EmailEvent) - private readonly eventRepository: Repository, - @InjectRepository(Campaign) - private readonly campaignRepository: Repository, - ) {} - - /** - * Record an email event - */ - async recordEvent( - campaignId: string, - recipientId: string, - eventType: EmailEventType, - metadata?: Record, - ): Promise { - const event = this.eventRepository.create({ - campaignId, - recipientId, - eventType, - metadata, - occurredAt: new Date(), - }); - - return this.eventRepository.save(event); - } - - /** - * Get campaign metrics - */ - async getCampaignMetrics(campaignId: string): Promise { - const campaign = await this.campaignRepository.findOne({ - where: { id: campaignId }, - }); - - if (!campaign) { - throw new NotFoundException(`Campaign ${campaignId} not found`); - } - - const sent = campaign.totalRecipients || 0; - - const [delivered, opened, clicked, bounced, unsubscribed] = await Promise.all([ - this.countEvents(campaignId, EmailEventType.DELIVERED), - this.countUniqueEvents(campaignId, EmailEventType.OPENED), - this.countUniqueEvents(campaignId, EmailEventType.CLICKED), - this.countEvents(campaignId, EmailEventType.BOUNCED), - this.countEvents(campaignId, EmailEventType.UNSUBSCRIBED), - ]); - - return { - sent, - delivered, - opened, - clicked, - bounced, - unsubscribed, - openRate: sent > 0 ? (opened / sent) * 100 : 0, - clickRate: opened > 0 ? (clicked / opened) * 100 : 0, - bounceRate: sent > 0 ? (bounced / sent) * 100 : 0, - }; - } - - /** - * Get time series data for a campaign - */ - async getCampaignTimeSeries( - campaignId: string, - startDate: Date, - endDate: Date, - ): Promise { - const events = await this.eventRepository.find({ - where: { - campaignId, - occurredAt: Between(startDate, endDate), - }, - order: { occurredAt: 'ASC' }, - }); - - const dataMap = new Map(); - - for (const event of events) { - const dateKey = event.occurredAt.toISOString().split('T')[0]; - - if (!dataMap.has(dateKey)) { - dataMap.set(dateKey, { date: dateKey, opens: 0, clicks: 0, bounces: 0 }); - } - - const data = dataMap.get(dateKey) ?? { opens: 0, clicks: 0, bounces: 0, unsubscribes: 0 }; - - if (event.eventType === EmailEventType.OPENED) data.opens++; - if (event.eventType === EmailEventType.CLICKED) data.clicks++; - if (event.eventType === EmailEventType.BOUNCED) data.bounces++; - } - - return Array.from(dataMap.values()); - } - - /** - * Get link click analytics - */ - async getLinkAnalytics(campaignId: string): Promise< - Array<{ - url: string; - clicks: number; - uniqueClicks: number; - }> - > { - const clickEvents = await this.eventRepository.find({ - where: { campaignId, eventType: EmailEventType.CLICKED }, - }); - - const linkMap = new Map }>(); - - for (const event of clickEvents) { - const url = event.metadata?.url || 'unknown'; - - if (!linkMap.has(url)) { - linkMap.set(url, { clicks: 0, recipients: new Set() }); - } - - const data = linkMap.get(url) ?? { url, clicks: 0, recipients: new Set() }; - data.clicks++; - data.recipients.add(event.recipientId); - } - - return Array.from(linkMap.entries()).map(([url, data]) => ({ - url, - clicks: data.clicks, - uniqueClicks: data.recipients.size, - })); - } - - /** - * Get overall email marketing stats - */ - async getOverallStats( - startDate?: Date, - endDate?: Date, - ): Promise<{ - totalCampaigns: number; - totalEmailsSent: number; - averageOpenRate: number; - averageClickRate: number; - }> { - const query = this.campaignRepository - .createQueryBuilder('campaign') - .where('campaign.sentAt IS NOT NULL'); - - if (startDate && endDate) { - query.andWhere('campaign.sentAt BETWEEN :start AND :end', { - start: startDate, - end: endDate, - }); - } - - const campaigns = await query.getMany(); - - const totalCampaigns = campaigns.length; - const totalEmailsSent = campaigns.reduce((sum, c) => sum + (c.totalRecipients || 0), 0); - - const campaignIds = campaigns.map((c) => c.id); - const metricsMap = await this.getBatchMetrics(campaignIds); - - let totalOpenRate = 0; - let totalClickRate = 0; - - for (const campaign of campaigns) { - const metrics = metricsMap.get(campaign.id) || { - sent: campaign.totalRecipients || 0, - delivered: 0, - opened: 0, - clicked: 0, - bounced: 0, - unsubscribed: 0, - openRate: 0, - clickRate: 0, - bounceRate: 0, - }; - totalOpenRate += metrics.openRate; - totalClickRate += metrics.clickRate; - } - - return { - totalCampaigns, - totalEmailsSent, - averageOpenRate: totalCampaigns > 0 ? totalOpenRate / totalCampaigns : 0, - averageClickRate: totalCampaigns > 0 ? totalClickRate / totalCampaigns : 0, - }; - } - - /** - * Batch fetch metrics for multiple campaigns to prevent N+1 queries - */ - private async getBatchMetrics(campaignIds: string[]): Promise> { - if (!campaignIds.length) return new Map(); - - // Query 1: Regular counts (delivered, bounced, unsubscribed) - const regularCounts = await this.eventRepository - .createQueryBuilder('event') - .select('event.campaignId', 'campaignId') - .addSelect('event.eventType', 'eventType') - .addSelect('COUNT(*)', 'count') - .where('event.campaignId IN (:...campaignIds)', { campaignIds }) - .andWhere('event.eventType IN (:...eventTypes)', { - eventTypes: [EmailEventType.DELIVERED, EmailEventType.BOUNCED, EmailEventType.UNSUBSCRIBED], - }) - .groupBy('event.campaignId') - .addGroupBy('event.eventType') - .getRawMany(); - - // Query 2: Unique recipient counts (opened, clicked) - const uniqueCounts = await this.eventRepository - .createQueryBuilder('event') - .select('event.campaignId', 'campaignId') - .addSelect('event.eventType', 'eventType') - .addSelect('COUNT(DISTINCT event.recipientId)', 'count') - .where('event.campaignId IN (:...campaignIds)', { campaignIds }) - .andWhere('event.eventType IN (:...eventTypes)', { - eventTypes: [EmailEventType.OPENED, EmailEventType.CLICKED], - }) - .groupBy('event.campaignId') - .addGroupBy('event.eventType') - .getRawMany(); - - // Fetch campaigns to get totalRecipients - const campaigns = await this.campaignRepository.findByIds(campaignIds); - const campaignMap = new Map(campaigns.map((c) => [c.id, c])); - - const metricsMap = new Map(); - - const getMetrics = (id: string): ICampaignMetrics => { - let metrics = metricsMap.get(id); - if (!metrics) { - const campaign = campaignMap.get(id); - const sent = campaign?.totalRecipients || 0; - metrics = { - sent, - delivered: 0, - opened: 0, - clicked: 0, - bounced: 0, - unsubscribed: 0, - openRate: 0, - clickRate: 0, - bounceRate: 0, - }; - } - /** - * Get time series data for a campaign - */ - async getCampaignTimeSeries(campaignId: string, startDate: Date, endDate: Date): Promise { - const events = await this.eventRepository.find({ - where: { - campaignId, - occurredAt: Between(startDate, endDate), - }, - order: { occurredAt: 'ASC' }, - }); - const dataMap = new Map(); - for (const event of events) { - const dateKey = event.occurredAt.toISOString().split('T')[0]; - if (!dataMap.has(dateKey)) { - dataMap.set(dateKey, { date: dateKey, opens: 0, clicks: 0, bounces: 0 }); - } - const data = dataMap.get(dateKey) ?? { opens: 0, clicks: 0, bounces: 0, unsubscribes: 0 }; - if (event.eventType === EmailEventType.OPENED) - data.opens++; - if (event.eventType === EmailEventType.CLICKED) - data.clicks++; - if (event.eventType === EmailEventType.BOUNCED) - data.bounces++; - } - return Array.from(dataMap.values()); - } - /** - * Get link click analytics - */ - async getLinkAnalytics(campaignId: string): Promise> { - const clickEvents = await this.eventRepository.find({ - where: { campaignId, eventType: EmailEventType.CLICKED }, - }); - const linkMap = new Map; - }>(); - for (const event of clickEvents) { - const url = event.metadata?.url || 'unknown'; - if (!linkMap.has(url)) { - linkMap.set(url, { clicks: 0, recipients: new Set() }); - } - const data = linkMap.get(url) ?? { url, clicks: 0, recipients: new Set() }; - data.clicks++; - data.recipients.add(event.recipientId); - } - return Array.from(linkMap.entries()).map(([url, data]) => ({ - url, - clicks: data.clicks, - uniqueClicks: data.recipients.size, - })); - } - /** - * Get overall email marketing stats - */ - async getOverallStats(startDate?: Date, endDate?: Date): Promise<{ - totalCampaigns: number; - totalEmailsSent: number; - averageOpenRate: number; - averageClickRate: number; - }> { - const query = this.campaignRepository - .createQueryBuilder('campaign') - .where('campaign.sentAt IS NOT NULL'); - if (startDate && endDate) { - query.andWhere('campaign.sentAt BETWEEN :start AND :end', { - start: startDate, - end: endDate, - }); - } - const campaigns = await query.getMany(); - const totalCampaigns = campaigns.length; - const totalEmailsSent = campaigns.reduce((sum, c) => sum + (c.totalRecipients || 0), 0); - const campaignIds = campaigns.map((c) => c.id); - const metricsMap = await this.getBatchMetrics(campaignIds); - let totalOpenRate = 0; - let totalClickRate = 0; - for (const campaign of campaigns) { - const metrics = metricsMap.get(campaign.id) || { - sent: campaign.totalRecipients || 0, - delivered: 0, - opened: 0, - clicked: 0, - bounced: 0, - unsubscribed: 0, - openRate: 0, - clickRate: 0, - bounceRate: 0, - }; - totalOpenRate += metrics.openRate; - totalClickRate += metrics.clickRate; - } - return { - totalCampaigns, - totalEmailsSent, - averageOpenRate: totalCampaigns > 0 ? totalOpenRate / totalCampaigns : 0, - averageClickRate: totalCampaigns > 0 ? totalClickRate / totalCampaigns : 0, - }; - } - /** - * Batch fetch metrics for multiple campaigns to prevent N+1 queries - */ - private async getBatchMetrics(campaignIds: string[]): Promise> { - if (!campaignIds.length) - return new Map(); - // Query 1: Regular counts (delivered, bounced, unsubscribed) - const regularCounts = await this.eventRepository - .createQueryBuilder('event') - .select('event.campaignId', 'campaignId') - .addSelect('event.eventType', 'eventType') - .addSelect('COUNT(*)', 'count') - .where('event.campaignId IN (:...campaignIds)', { campaignIds }) - .andWhere('event.eventType IN (:...eventTypes)', { - eventTypes: [EmailEventType.DELIVERED, EmailEventType.BOUNCED, EmailEventType.UNSUBSCRIBED], - }) - .groupBy('event.campaignId') - .addGroupBy('event.eventType') - .getRawMany(); - // Query 2: Unique recipient counts (opened, clicked) - const uniqueCounts = await this.eventRepository - .createQueryBuilder('event') - .select('event.campaignId', 'campaignId') - .addSelect('event.eventType', 'eventType') - .addSelect('COUNT(DISTINCT event.recipientId)', 'count') - .where('event.campaignId IN (:...campaignIds)', { campaignIds }) - .andWhere('event.eventType IN (:...eventTypes)', { - eventTypes: [EmailEventType.OPENED, EmailEventType.CLICKED], - }) - .groupBy('event.campaignId') - .addGroupBy('event.eventType') - .getRawMany(); - // Fetch campaigns to get totalRecipients - const campaigns = await this.campaignRepository.findByIds(campaignIds); - const campaignMap = new Map(campaigns.map((c) => [c.id, c])); - const metricsMap = new Map(); - const getMetrics = (id: string): CampaignMetrics => { - let metrics = metricsMap.get(id); - if (!metrics) { - const campaign = campaignMap.get(id); - const sent = campaign?.totalRecipients || 0; - metrics = { - sent, - delivered: 0, - opened: 0, - clicked: 0, - bounced: 0, - unsubscribed: 0, - openRate: 0, - clickRate: 0, - bounceRate: 0, - }; - metricsMap.set(id, metrics); - } - return metrics; - }; - // Process regular counts - for (const row of regularCounts) { - const metrics = getMetrics(row.campaignId); - const count = parseInt(row.count, 10); - if (row.eventType === EmailEventType.DELIVERED) - metrics.delivered = count; - if (row.eventType === EmailEventType.BOUNCED) - metrics.bounced = count; - if (row.eventType === EmailEventType.UNSUBSCRIBED) - metrics.unsubscribed = count; - } - // Process unique counts - for (const row of uniqueCounts) { - const metrics = getMetrics(row.campaignId); - const count = parseInt(row.count, 10); - if (row.eventType === EmailEventType.OPENED) - metrics.opened = count; - if (row.eventType === EmailEventType.CLICKED) - metrics.clicked = count; - } - // Calculate rates - for (const metrics of metricsMap.values()) { - const sent = metrics.sent; - metrics.openRate = sent > 0 ? (metrics.opened / sent) * 100 : 0; - metrics.clickRate = metrics.opened > 0 ? (metrics.clicked / metrics.opened) * 100 : 0; - metrics.bounceRate = sent > 0 ? (metrics.bounced / sent) * 100 : 0; - } - return metricsMap; - } - // Helper methods - private async countEvents(campaignId: string, eventType: EmailEventType): Promise { - return this.eventRepository.count({ where: { campaignId, eventType } }); - } - private async countUniqueEvents(campaignId: string, eventType: EmailEventType): Promise { - const result = await this.eventRepository - .createQueryBuilder('event') - .select('COUNT(DISTINCT event.recipientId)', 'count') - .where('event.campaignId = :campaignId', { campaignId }) - .andWhere('event.eventType = :eventType', { eventType }) - .getRawOne(); - return parseInt(result?.count || '0', 10); - } -} diff --git a/src/email-marketing/email-marketing.controller.ts b/src/email-marketing/email-marketing.controller.ts deleted file mode 100644 index eff8ff93..00000000 --- a/src/email-marketing/email-marketing.controller.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { Controller, Get, Post, Put, Delete, Body, Param, Query, ParseUUIDPipe, HttpCode, HttpStatus, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; -import { EmailMarketingService } from './email-marketing.service'; -import { CreateCampaignDto } from './dto/create-campaign.dto'; -import { UpdateCampaignDto } from './dto/update-campaign.dto'; -import { ScheduleCampaignDto } from './dto/schedule-campaign.dto'; -import { Campaign } from './entities/campaign.entity'; - -/** - * Exposes email Marketing endpoints. - */ -@ApiTags('Email Marketing - Campaigns') -@ApiBearerAuth() -@Controller('email-marketing/campaigns') -export class EmailMarketingController { - constructor(private readonly emailMarketingService: EmailMarketingService) {} - - /** - * Creates a new record. - * @param createCampaignDto The request payload. - * @returns The resulting campaign. - */ - @Post() - @ApiOperation({ summary: 'Create a new email campaign' }) - @ApiResponse({ status: 201, description: 'Campaign created successfully', type: Campaign }) - @ApiResponse({ status: 400, description: 'Invalid input data' }) - async create(@Body() createCampaignDto: CreateCampaignDto): Promise { - return this.emailMarketingService.createCampaign(createCampaignDto); - } - - /** - * Returns all. - * @param page The page number. - * @param limit The maximum number of results. - * @returns The operation result. - */ - @Get() - @ApiOperation({ summary: 'Get all campaigns with pagination' }) - @ApiQuery({ name: 'page', required: false, type: Number, example: 1 }) - @ApiQuery({ name: 'limit', required: false, type: Number, example: 10 }) - @ApiResponse({ status: 200, description: 'List of campaigns' }) - async findAll(@Query('page') page: number = 1, @Query('limit') limit: number = 10) { - return this.emailMarketingService.findAll(page, limit); - } - - /** - * Returns one. - * @param id The identifier. - * @returns The resulting campaign. - */ - @Get(':id') - @ApiOperation({ summary: 'Get a campaign by ID' }) - @ApiResponse({ status: 200, description: 'Campaign details', type: Campaign }) - @ApiResponse({ status: 404, description: 'Campaign not found' }) - async findOne(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.emailMarketingService.findOne(id); - } - - /** - * Updates the requested record. - * @param id The identifier. - * @param updateCampaignDto The request payload. - * @returns The resulting campaign. - */ - @Put(':id') - @ApiOperation({ summary: 'Update a campaign' }) - @ApiResponse({ status: 200, description: 'Campaign updated successfully', type: Campaign }) - @ApiResponse({ status: 400, description: 'Cannot update sent campaign' }) - @ApiResponse({ status: 404, description: 'Campaign not found' }) - async update( - @Param('id', ParseUUIDPipe) id: string, - @Body() updateCampaignDto: UpdateCampaignDto, - ): Promise { - return this.emailMarketingService.update(id, updateCampaignDto); - } - - /** - * Removes the requested record. - * @param id The identifier. - */ - @Delete(':id') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: 'Delete a campaign' }) - @ApiResponse({ status: 204, description: 'Campaign deleted successfully' }) - @ApiResponse({ status: 400, description: 'Cannot delete sending campaign' }) - @ApiResponse({ status: 404, description: 'Campaign not found' }) - async remove(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.emailMarketingService.remove(id); - } - - /** - * Schedules schedule. - * @param id The identifier. - * @param scheduleDto The request payload. - * @returns The resulting campaign. - */ - @Post(':id/schedule') - @ApiOperation({ summary: 'Schedule a campaign for future sending' }) - @ApiResponse({ status: 200, description: 'Campaign scheduled successfully', type: Campaign }) - @ApiResponse({ status: 400, description: 'Invalid schedule or campaign status' }) - async schedule( - @Param('id', ParseUUIDPipe) id: string, - @Body() scheduleDto: ScheduleCampaignDto, - ): Promise { - return this.emailMarketingService.scheduleCampaign(id, scheduleDto); - } - - /** - * Sends send. - * @param id The identifier. - * @returns The resulting campaign. - */ - @Post(':id/send') - @ApiOperation({ summary: 'Send a campaign immediately' }) - @ApiResponse({ status: 200, description: 'Campaign sending initiated', type: Campaign }) - @ApiResponse({ status: 400, description: 'Campaign cannot be sent' }) - async send(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.emailMarketingService.sendCampaign(id); - } - - /** - * Pauses pause. - * @param id The identifier. - * @returns The resulting campaign. - */ - @Post(':id/pause') - @ApiOperation({ summary: 'Pause a scheduled or sending campaign' }) - @ApiResponse({ status: 200, description: 'Campaign paused successfully', type: Campaign }) - @ApiResponse({ status: 400, description: 'Campaign cannot be paused' }) - async pause(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.emailMarketingService.pauseCampaign(id); - } - - /** - * Resumes resume. - * @param id The identifier. - * @returns The resulting campaign. - */ - @Post(':id/resume') - @ApiOperation({ summary: 'Resume a paused campaign' }) - @ApiResponse({ status: 200, description: 'Campaign resumed successfully', type: Campaign }) - @ApiResponse({ status: 400, description: 'Campaign cannot be resumed' }) - async resume(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.emailMarketingService.resumeCampaign(id); - } - - /** - * Executes duplicate. - * @param id The identifier. - * @returns The resulting campaign. - */ - @Post(':id/duplicate') - @ApiOperation({ summary: 'Duplicate a campaign' }) - @ApiResponse({ status: 201, description: 'Campaign duplicated successfully', type: Campaign }) - @ApiResponse({ status: 404, description: 'Campaign not found' }) - async duplicate(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.emailMarketingService.duplicateCampaign(id); - } - - /** - * Returns stats. - * @param id The identifier. - * @returns The operation result. - */ - @Get(':id/stats') - @ApiOperation({ summary: 'Get campaign statistics' }) - @ApiResponse({ status: 200, description: 'Campaign statistics' }) - @ApiResponse({ status: 404, description: 'Campaign not found' }) - async getStats(@Param('id', ParseUUIDPipe) id: string) { - return this.emailMarketingService.getCampaignStats(id); - } -} diff --git a/src/email-marketing/email-marketing.module.ts b/src/email-marketing/email-marketing.module.ts deleted file mode 100644 index daa2fdfd..00000000 --- a/src/email-marketing/email-marketing.module.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { BullModule } from '@nestjs/bull'; -import { ConfigModule } from '@nestjs/config'; -import { QUEUE_NAMES } from '../common/constants/queue.constants'; -// Services -import { EmailMarketingService } from './email-marketing.service'; -import { AutomationService } from './automation/automation.service'; -import { SegmentationService } from './segmentation/segmentation.service'; -import { EmailAnalyticsService } from './analytics/email-analytics.service'; -import { TemplateManagementService } from './templates/template-management.service'; -import { ABTestingService } from './ab-testing/ab-testing.service'; -import { EmailSenderService } from './sender/email-sender.service'; -// Controllers -import { EmailMarketingController } from './email-marketing.controller'; -import { TemplateController } from './templates/template.controller'; -import { AutomationController } from './automation/automation.controller'; -import { SegmentController } from './segmentation/segment.controller'; -import { EmailAnalyticsController } from './analytics/email-analytics.controller'; -import { ABTestingController } from './ab-testing/ab-testing.controller'; -import { TrackingController } from './tracking/tracking.controller'; -// Entities -import { Campaign } from './entities/campaign.entity'; -import { EmailTemplate } from './entities/email-template.entity'; -import { AutomationWorkflow } from './entities/automation-workflow.entity'; -import { AutomationTrigger } from './entities/automation-trigger.entity'; -import { AutomationAction } from './entities/automation-action.entity'; -import { Segment } from './entities/segment.entity'; -import { SegmentRule } from './entities/segment-rule.entity'; -import { EmailEvent } from './entities/email-event.entity'; -import { ABTest } from './entities/ab-test.entity'; -import { ABTestVariant } from './entities/ab-test-variant.entity'; -import { CampaignRecipient } from './entities/campaign-recipient.entity'; -import { EmailSubscription } from './entities/email-subscription.entity'; -// Processors (Bull Queue) -import { EmailQueueProcessor } from './processors/email-queue.processor'; - -/** - * Registers the email Marketing module. - */ -@Module({ - imports: [ - ConfigModule, - TypeOrmModule.forFeature([ - Campaign, - EmailTemplate, - AutomationWorkflow, - AutomationTrigger, - AutomationAction, - Segment, - SegmentRule, - EmailEvent, - ABTest, - ABTestVariant, - CampaignRecipient, - EmailSubscription, - ]), - BullModule.registerQueue({ - name: QUEUE_NAMES.EMAIL_MARKETING, - }), - ], - controllers: [ - EmailMarketingController, - TemplateController, - AutomationController, - SegmentController, - EmailAnalyticsController, - ABTestingController, - TrackingController, - ], - providers: [ - EmailMarketingService, - AutomationService, - SegmentationService, - EmailAnalyticsService, - TemplateManagementService, - ABTestingService, - EmailSenderService, - EmailQueueProcessor, - ], - exports: [ - EmailMarketingService, - AutomationService, - SegmentationService, - EmailAnalyticsService, - TemplateManagementService, - ABTestingService, - ], -}) -export class EmailMarketingModule { -} diff --git a/src/email-marketing/email-marketing.service.ts b/src/email-marketing/email-marketing.service.ts deleted file mode 100644 index da2e2389..00000000 --- a/src/email-marketing/email-marketing.service.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { InjectQueue } from '@nestjs/bull'; -import { Queue } from 'bull'; -import { QUEUE_NAMES, JOB_NAMES } from '../common/constants/queue.constants'; -import { Campaign } from './entities/campaign.entity'; -import { CampaignRecipient } from './entities/campaign-recipient.entity'; -import { SegmentationService } from './segmentation/segmentation.service'; -import { TemplateManagementService } from './templates/template-management.service'; -import { ABTestingService } from './ab-testing/ab-testing.service'; -import { EmailAnalyticsService } from './analytics/email-analytics.service'; -import { CreateCampaignDto } from './dto/create-campaign.dto'; -import { UpdateCampaignDto } from './dto/update-campaign.dto'; -import { ScheduleCampaignDto } from './dto/schedule-campaign.dto'; -import { CampaignStatus } from './enums/campaign-status.enum'; - -/** - * Provides email Marketing operations. - */ -@Injectable() -export class EmailMarketingService { - constructor( - @InjectRepository(Campaign) - private readonly campaignRepository: Repository, - @InjectRepository(CampaignRecipient) - private readonly recipientRepository: Repository, - @InjectQueue(QUEUE_NAMES.EMAIL_MARKETING) - private readonly emailQueue: Queue, private readonly segmentationService: SegmentationService, private readonly templateService: TemplateManagementService, private readonly abTestingService: ABTestingService, private readonly analyticsService: EmailAnalyticsService) { } - /** - * Create a new email campaign - */ - async createCampaign(createCampaignDto: CreateCampaignDto): Promise { - // Validate template exists - if (createCampaignDto.templateId) { - await this.templateService.findOne(createCampaignDto.templateId); - } - // Validate segments exist - if (createCampaignDto.segmentIds?.length) { - const segments = await this.segmentationService.findByIds(createCampaignDto.segmentIds); - if (segments.length !== createCampaignDto.segmentIds.length) { - throw new NotFoundException('One or more segments not found'); - } - } - const campaign = this.campaignRepository.create({ - ...createCampaignDto, - status: CampaignStatus.DRAFT, - }); - return this.campaignRepository.save(campaign); - } - /** - * Get all campaigns with pagination - */ - async findAll(page: number = 1, limit: number = 10): Promise<{ - campaigns: Campaign[]; - total: number; - page: number; - totalPages: number; - }> { - const [campaigns, total] = await this.campaignRepository.findAndCount({ - skip: (page - 1) * limit, - take: limit, - order: { createdAt: 'DESC' }, - relations: ['template'], - }); - return { - campaigns, - total, - page, - totalPages: Math.ceil(total / limit), - }; - } - /** - * Get a single campaign by ID - */ - async findOne(id: string): Promise { - const campaign = await this.campaignRepository.findOne({ - where: { id }, - relations: ['template', 'abTest', 'recipients'], - }); - if (!campaign) { - throw new NotFoundException(`Campaign with ID ${id} not found`); - } - return campaign; - } - /** - * Update a campaign - */ - async update(id: string, updateCampaignDto: UpdateCampaignDto): Promise { - const campaign = await this.findOne(id); - if (campaign.status === CampaignStatus.SENT) { - throw new BadRequestException('Cannot update a sent campaign'); - } - Object.assign(campaign, updateCampaignDto); - return this.campaignRepository.save(campaign); - } - /** - * Delete a campaign - */ - async remove(id: string): Promise { - const campaign = await this.findOne(id); - if (campaign.status === CampaignStatus.SENDING) { - throw new BadRequestException('Cannot delete a campaign that is currently sending'); - } - await this.campaignRepository.manager.transaction(async (manager) => { - await manager.getRepository(CampaignRecipient).softDelete({ campaignId: id }); - await manager.getRepository(Campaign).softDelete(id); - }); - } - /** - * Schedule a campaign for future sending - */ - async scheduleCampaign(id: string, scheduleDto: ScheduleCampaignDto): Promise { - const campaign = await this.findOne(id); - if (campaign.status !== CampaignStatus.DRAFT) { - throw new BadRequestException('Only draft campaigns can be scheduled'); - } - const scheduledDate = new Date(scheduleDto.scheduledAt); - if (scheduledDate <= new Date()) { - throw new BadRequestException('Scheduled date must be in the future'); - } - campaign.scheduledAt = scheduledDate; - campaign.status = CampaignStatus.SCHEDULED; - // Add to queue with delay - const delay = scheduledDate.getTime() - Date.now(); - await this.emailQueue.add(JOB_NAMES.SEND_CAMPAIGN, { campaignId: id }, { delay, jobId: `campaign-${id}` }); - return this.campaignRepository.save(campaign); - } - /** - * Send a campaign immediately - */ - async sendCampaign(id: string): Promise { - const campaign = await this.findOne(id); - if (campaign.status === CampaignStatus.SENT || campaign.status === CampaignStatus.SENDING) { - throw new BadRequestException('Campaign has already been sent or is sending'); - } - // Get recipients from segments - const recipients = await this.segmentationService.getUsersFromSegments(campaign.segmentIds || []); - if (recipients.length === 0) { - throw new BadRequestException('No recipients found for this campaign'); - } - // Update campaign status - campaign.status = CampaignStatus.SENDING; - campaign.sentAt = new Date(); - campaign.totalRecipients = recipients.length; - await this.campaignRepository.save(campaign); - // Queue emails for sending - await this.emailQueue.add(JOB_NAMES.PROCESS_CAMPAIGN, { - campaignId: id, - recipients: recipients.map((r) => r.id), - }); - return campaign; - } - /** - * Pause a scheduled or sending campaign - */ - async pauseCampaign(id: string): Promise { - const campaign = await this.findOne(id); - if (campaign.status !== CampaignStatus.SCHEDULED && - campaign.status !== CampaignStatus.SENDING) { - throw new BadRequestException('Only scheduled or sending campaigns can be paused'); - } - // Remove from queue if scheduled - if (campaign.status === CampaignStatus.SCHEDULED) { - await this.emailQueue.removeJobs(`campaign-${id}`); - } - campaign.status = CampaignStatus.PAUSED; - return this.campaignRepository.save(campaign); - } - /** - * Resume a paused campaign - */ - async resumeCampaign(id: string): Promise { - const campaign = await this.findOne(id); - if (campaign.status !== CampaignStatus.PAUSED) { - throw new BadRequestException('Only paused campaigns can be resumed'); - } - // If it was scheduled, re-schedule - if (campaign.scheduledAt && campaign.scheduledAt > new Date()) { - return this.scheduleCampaign(id, { scheduledAt: campaign.scheduledAt.toISOString() }); - } - // Otherwise, resume sending - campaign.status = CampaignStatus.SENDING; - await this.emailQueue.add(JOB_NAMES.RESUME_CAMPAIGN, { campaignId: id }); - return this.campaignRepository.save(campaign); - } - /** - * Duplicate a campaign - */ - async duplicateCampaign(id: string): Promise { - const original = await this.findOne(id); - const duplicate = this.campaignRepository.create({ - name: `${original.name} (Copy)`, - subject: original.subject, - previewText: original.previewText, - templateId: original.templateId, - segmentIds: original.segmentIds, - content: original.content, - status: CampaignStatus.DRAFT, - }); - return this.campaignRepository.save(duplicate); - } - /** - * Get campaign statistics - */ - async getCampaignStats(id: string): Promise<{ - sent: number; - delivered: number; - opened: number; - clicked: number; - bounced: number; - unsubscribed: number; - openRate: number; - clickRate: number; - bounceRate: number; - }> { - await this.findOne(id); - return this.analyticsService.getCampaignMetrics(id); - } -} diff --git a/src/email-marketing/processors/email-queue.processor.ts b/src/email-marketing/processors/email-queue.processor.ts deleted file mode 100644 index 09d68aad..00000000 --- a/src/email-marketing/processors/email-queue.processor.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Process, Processor } from '@nestjs/bull'; -import { Logger } from '@nestjs/common'; -import { Job } from 'bull'; -import { QUEUE_NAMES, JOB_NAMES } from '../../common/constants/queue.constants'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Campaign } from '../entities/campaign.entity'; -import { CampaignRecipient } from '../entities/campaign-recipient.entity'; -import { EmailSenderService } from '../sender/email-sender.service'; -import { SegmentationService } from '../segmentation/segmentation.service'; -import { ABTestingService } from '../ab-testing/ab-testing.service'; -import { CampaignStatus } from '../enums/campaign-status.enum'; -import { RecipientStatus } from '../enums/recipient-status.enum'; -@Processor(QUEUE_NAMES.EMAIL_MARKETING) -export class EmailQueueProcessor { - private readonly logger = new Logger(EmailQueueProcessor.name); - constructor( - @InjectRepository(Campaign) - private readonly campaignRepository: Repository, - @InjectRepository(CampaignRecipient) - private readonly recipientRepository: Repository, private readonly emailSenderService: EmailSenderService, private readonly segmentationService: SegmentationService, private readonly abTestingService: ABTestingService) { } - @Process(JOB_NAMES.SEND_CAMPAIGN) - async handleScheduledCampaign(job: Job<{ - campaignId: string; - }>) { - this.logger.log(`Processing scheduled campaign: ${job.data.campaignId}`); - const campaign = await this.campaignRepository.findOne({ - where: { id: job.data.campaignId }, - relations: ['abTest', 'abTest.variants'], - }); - if (!campaign || campaign.status !== CampaignStatus.SCHEDULED) { - this.logger.warn(`Campaign ${job.data.campaignId} not found or not scheduled`); - return; - } - // Get recipients - const users = await this.segmentationService.getUsersFromSegments(campaign.segmentIds || []); - campaign.status = CampaignStatus.SENDING; - campaign.sentAt = new Date(); - campaign.totalRecipients = users.length; - await this.campaignRepository.save(campaign); - await this.processRecipients(campaign, users); - } - @Process(JOB_NAMES.PROCESS_CAMPAIGN) - async handleCampaignProcessing(job: Job<{ - campaignId: string; - recipients: string[]; - }>) { - this.logger.log(`Processing campaign: ${job.data.campaignId}`); - const campaign = await this.campaignRepository.findOne({ - where: { id: job.data.campaignId }, - relations: ['abTest', 'abTest.variants'], - }); - if (!campaign) { - return; - } - // Fetch user details for recipients - // TODO: Integrate with Users module - const users = job.data.recipients.map((id) => ({ - id, - email: `user-${id}@example.com`, // Placeholder - })); - await this.processRecipients(campaign, users); - } - @Process(JOB_NAMES.SEND_AUTOMATION_EMAIL) - async handleAutomationEmail(job: Job<{ - actionId: string; - templateId: string; - userId: string; - variables: Record; - }>) { - this.logger.log(`Sending automation email for action: ${job.data.actionId}`); - // TODO: Get user email from Users module - const userEmail = `user-${job.data.userId}@example.com`; - await this.emailSenderService.sendEmail({ - to: userEmail, - templateId: job.data.templateId, - variables: job.data.variables, - trackOpens: true, - trackClicks: true, - }); - } - @Process(JOB_NAMES.RESUME_CAMPAIGN) - async handleResumeCampaign(job: Job<{ - campaignId: string; - }>) { - this.logger.log(`Resuming campaign: ${job.data.campaignId}`); - const pendingRecipients = await this.recipientRepository.find({ - where: { - campaignId: job.data.campaignId, - status: RecipientStatus.PENDING, - }, - }); - const campaign = await this.campaignRepository.findOne({ - where: { id: job.data.campaignId }, - relations: ['abTest', 'abTest.variants'], - }); - if (!campaign) - return; - for (const recipient of pendingRecipients) { - await this.sendToRecipient(campaign, recipient); - } - await this.finalizeCampaign(job.data.campaignId); - } - // Private helper methods - private async processRecipients(campaign: Campaign, users: Array<{ - id: string; - email: string; - }>): Promise { - // Create recipient records - const recipients = users.map((user) => this.recipientRepository.create({ - campaignId: campaign.id, - userId: user.id, - email: user.email, - status: RecipientStatus.PENDING, - })); - await this.recipientRepository.save(recipients); - // Process in batches - const batchSize = 100; - for (let i = 0; i < recipients.length; i += batchSize) { - const batch = recipients.slice(i, i + batchSize); - await Promise.all(batch.map((recipient) => this.sendToRecipient(campaign, recipient))); - // Update progress - const progress = Math.round(((i + batch.length) / recipients.length) * 100); - this.logger.log(`Campaign ${campaign.id} progress: ${progress}%`); - } - await this.finalizeCampaign(campaign.id); - } - private async sendToRecipient(campaign: Campaign, recipient: CampaignRecipient): Promise { - try { - // Select A/B test variant if applicable - let variantId: string | undefined; - let templateId = campaign.templateId; - if (campaign.abTest) { - const variant = this.abTestingService.selectVariantForRecipient(campaign.abTest); - variantId = variant.id; - templateId = variant.templateId || templateId; - } - const result = await this.emailSenderService.sendEmail({ - to: recipient.email, - templateId, - variables: { userId: recipient.userId }, - campaignId: campaign.id, - recipientId: recipient.id, - variantId, - trackOpens: true, - trackClicks: true, - }); - recipient.status = result.success ? RecipientStatus.SENT : RecipientStatus.FAILED; - recipient.sentAt = new Date(); - await this.recipientRepository.save(recipient); - } - catch (error) { - this.logger.error(`Failed to send to ${recipient.email}:`, error); - recipient.status = RecipientStatus.FAILED; - await this.recipientRepository.save(recipient); - } - } - private async finalizeCampaign(campaignId: string): Promise { - const campaign = await this.campaignRepository.findOne({ where: { id: campaignId } }); - if (campaign && campaign.status === CampaignStatus.SENDING) { - campaign.status = CampaignStatus.SENT; - await this.campaignRepository.save(campaign); - this.logger.log(`Campaign ${campaignId} completed`); - } - } -} diff --git a/src/email-marketing/segmentation/segment.controller.ts b/src/email-marketing/segmentation/segment.controller.ts deleted file mode 100644 index b365f395..00000000 --- a/src/email-marketing/segmentation/segment.controller.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Controller, Get, Post, Put, Delete, Body, Param, Query, ParseUUIDPipe, HttpCode, HttpStatus, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; -import { SegmentationService } from './segmentation.service'; -import { CreateSegmentDto } from '../dto/create-segment.dto'; -import { UpdateSegmentDto } from '../dto/update-segment.dto'; -import { AddSegmentMembersDto } from '../dto/add-segment-members.dto'; -import { Segment } from '../entities/segment.entity'; - -/** - * Exposes segment endpoints. - */ -@ApiTags('Email Marketing - Segments') -@ApiBearerAuth() -@Controller('email-marketing/segments') -export class SegmentController { - constructor(private readonly segmentationService: SegmentationService) {} - - /** - * Creates a new record. - * @param createSegmentDto The request payload. - * @returns The resulting segment. - */ - @Post() - @ApiOperation({ summary: 'Create a new audience segment' }) - @ApiResponse({ status: 201, description: 'Segment created successfully' }) - async create(@Body() createSegmentDto: CreateSegmentDto): Promise { - return this.segmentationService.create(createSegmentDto); - } - - /** - * Returns all. - * @param page The page number. - * @param limit The maximum number of results. - * @returns The operation result. - */ - @Get() - @ApiOperation({ summary: 'Get all segments with pagination' }) - @ApiQuery({ name: 'page', required: false, type: Number }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - async findAll(@Query('page') page = 1, @Query('limit') limit = 10) { - return this.segmentationService.findAll(page, limit); - } - - /** - * Returns one. - * @param id The identifier. - * @returns The resulting segment. - */ - @Get(':id') - @ApiOperation({ summary: 'Get a segment by ID' }) - @ApiResponse({ status: 404, description: 'Segment not found' }) - async findOne(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.segmentationService.findOne(id); - } - - /** - * Updates the requested record. - * @param id The identifier. - * @param updateSegmentDto The request payload. - * @returns The resulting segment. - */ - @Put(':id') - @ApiOperation({ summary: 'Update a segment' }) - async update( - @Param('id', ParseUUIDPipe) id: string, - @Body() updateSegmentDto: UpdateSegmentDto, - ): Promise { - return this.segmentationService.update(id, updateSegmentDto); - } - - /** - * Removes the requested record. - * @param id The identifier. - */ - @Delete(':id') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: 'Delete a segment' }) - async remove(@Param('id', ParseUUIDPipe) id: string): Promise { - return this.segmentationService.remove(id); - } - - /** - * Returns members. - * @param id The identifier. - * @returns The operation result. - */ - @Get(':id/members') - @ApiOperation({ summary: 'Get members of a segment' }) - async getMembers(@Param('id', ParseUUIDPipe) id: string) { - return this.segmentationService.getSegmentMembers(id); - } - - /** - * Executes add Members. - * @param id The identifier. - * @param addMembersDto The request payload. - * @returns The operation result. - */ - @Post(':id/members') - @ApiOperation({ summary: 'Add users to a static segment' }) - @ApiResponse({ status: 200, description: 'Users added successfully' }) - async addMembers( - @Param('id', ParseUUIDPipe) id: string, - @Body() addMembersDto: AddSegmentMembersDto, - ): Promise<{ message: string; addedCount: number }> { - await this.segmentationService.addUsersToSegment(id, addMembersDto.userIds); - return { - message: 'Users added successfully', - addedCount: addMembersDto.userIds.length, - }; - } - - /** - * Executes preview. - * @param createSegmentDto The request payload. - * @returns The operation result. - */ - @Post('preview') - @ApiOperation({ summary: 'Preview segment members without saving' }) - async preview(@Body() createSegmentDto: CreateSegmentDto) { - return this.segmentationService.previewSegment(createSegmentDto.rules); - } -} diff --git a/src/email-marketing/segmentation/segmentation.service.ts b/src/email-marketing/segmentation/segmentation.service.ts deleted file mode 100644 index 50b70bdb..00000000 --- a/src/email-marketing/segmentation/segmentation.service.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; -import { Segment } from '../entities/segment.entity'; -import { SegmentRule } from '../entities/segment-rule.entity'; -import { CreateSegmentDto } from '../dto/create-segment.dto'; -import { UpdateSegmentDto } from '../dto/update-segment.dto'; -import { SegmentRuleOperator } from '../enums/segment-rule-operator.enum'; -import { SegmentRuleField } from '../enums/segment-rule-field.enum'; -// Note: Import User entity from users module when integrating -export interface IUserProfile { - id: string; - email: string; - firstName?: string; - lastName?: string; - createdAt: Date; - lastLoginAt?: Date; - tags?: string[]; - preferences?: Record; -} - -/** - * Provides segmentation operations. - */ -@Injectable() -export class SegmentationService { - constructor( - @InjectRepository(Segment) - private readonly segmentRepository: Repository, - @InjectRepository(SegmentRule) - private readonly ruleRepository: Repository) { } - /** - * Create a new segment - */ - async create(createSegmentDto: CreateSegmentDto): Promise { - const segment = this.segmentRepository.create({ - name: createSegmentDto.name, - description: createSegmentDto.description, - isDynamic: createSegmentDto.isDynamic ?? true, - }); - const savedSegment = await this.segmentRepository.save(segment); - // Create rules - if (createSegmentDto.rules?.length) { - const rules = createSegmentDto.rules.map((rule, index) => this.ruleRepository.create({ - ...rule, - segmentId: savedSegment.id, - order: index, - })); - await this.ruleRepository.save(rules); - } - return this.findOne(savedSegment.id); - } - /** - * Get all segments - */ - async findAll(page: number = 1, limit: number = 10): Promise<{ - segments: Segment[]; - total: number; - page: number; - totalPages: number; - }> { - const [segments, total] = await this.segmentRepository.findAndCount({ - skip: (page - 1) * limit, - take: limit, - order: { createdAt: 'DESC' }, - relations: ['rules'], - }); - // Calculate member count for each segment using the segment object already loaded with rules - for (const segment of segments) { - segment.memberCount = await this.calculateMemberCountForSegment(segment); - } - return { - segments, - total, - page, - totalPages: Math.ceil(total / limit), - }; - } - /** - * Get a single segment by ID - */ - async findOne(id: string): Promise { - const segment = await this.segmentRepository.findOne({ - where: { id }, - relations: ['rules'], - }); - if (!segment) { - throw new NotFoundException(`Segment with ID ${id} not found`); - } - segment.memberCount = await this.calculateMemberCountForSegment(segment); - return segment; - } - /** - * Find multiple segments by IDs - */ - async findByIds(ids: string[]): Promise { - if (!ids.length) - return []; - return this.segmentRepository.find({ - where: { id: In(ids) }, - relations: ['rules'], - }); - } - - return this.findOne(id); - } - - /** - * Delete a segment - */ - async remove(id: string): Promise { - await this.findOne(id); - await this.segmentRepository.manager.transaction(async (manager) => { - await manager.getRepository(SegmentRule).softDelete({ segmentId: id }); - await manager.getRepository(Segment).softDelete(id); - }); - } - - /** - * Get users from multiple segments - */ - async getUsersFromSegments(segmentIds: string[]): Promise { - if (!segmentIds.length) { - return []; - } - - const segments = await this.segmentRepository.find({ - where: { id: In(segmentIds) }, - relations: ['rules'], - }); - - const userSets = await Promise.all(segments.map((segment) => this.getSegmentMembers(segment))); - - // Combine all users (union) - const userMap = new Map(); - for (const users of userSets) { - for (const user of users) { - userMap.set(user.id, user); - } - } - - return Array.from(userMap.values()); - } - - /** - * Get members of a specific segment - */ - async getSegmentMembers(segmentOrId: Segment | string): Promise { - let segment: Segment; - - if (typeof segmentOrId === 'string') { - // Direct query to avoid infinite recursion - const found = await this.segmentRepository.findOne({ - where: { id: segmentOrId }, - relations: ['rules'], - }); - if (!found) { - throw new NotFoundException(`Segment with ID ${segmentOrId} not found`); - } - segment = found; - } else { - segment = segmentOrId; - } - /** - * Get members of a specific segment - */ - async getSegmentMembers(segmentOrId: Segment | string): Promise { - let segment: Segment; - if (typeof segmentOrId === 'string') { - // Direct query to avoid infinite recursion - const found = await this.segmentRepository.findOne({ - where: { id: segmentOrId }, - relations: ['rules'], - }); - if (!found) { - throw new NotFoundException(`Segment with ID ${segmentOrId} not found`); - } - segment = found; - } - else { - segment = segmentOrId; - } - if (!segment.isDynamic) { - // Static segment - return manually added members - return this.getStaticSegmentMembers(segment.id); - } - // Dynamic segment - evaluate rules - return this.evaluateSegmentRules(segment.rules); - } - - // Dynamic segment - evaluate rules - return this.evaluateSegmentRules(segment.rules); - } - - /** - * Preview segment members without saving - */ - async previewSegment(rules: CreateSegmentDto['rules']): Promise<{ - count: number; - sample: IUserProfile[]; - }> { - const ruleEntities = rules.map((rule, index) => - this.ruleRepository.create({ ...rule, order: index }), - ); - - const members = await this.evaluateSegmentRules(ruleEntities); - - return { - count: members.length, - sample: members.slice(0, 10), - }; - } - - /** - * Add users manually to a static segment - */ - async addUsersToSegment(segmentId: string, userIds: string[]): Promise { - const segment = await this.findOne(segmentId); - - if (segment.isDynamic) { - throw new BadRequestException('Cannot manually add users to a dynamic segment'); - } - /** - * Add users manually to a static segment - */ - async addUsersToSegment(segmentId: string, userIds: string[]): Promise { - const segment = await this.findOne(segmentId); - if (segment.isDynamic) { - throw new BadRequestException('Cannot manually add users to a dynamic segment'); - } - // Add to static member list - const currentMembers = segment.staticMemberIds || []; - const newMembers = [...new Set([...currentMembers, ...userIds])]; - await this.segmentRepository.update(segmentId, { - staticMemberIds: newMembers, - }); - } - /** - * Remove users from a static segment - */ - async removeUsersFromSegment(segmentId: string, userIds: string[]): Promise { - const segment = await this.findOne(segmentId); - if (segment.isDynamic) { - throw new BadRequestException('Cannot manually remove users from a dynamic segment'); - } - const currentMembers = segment.staticMemberIds || []; - const newMembers = currentMembers.filter((id) => !userIds.includes(id)); - await this.segmentRepository.update(segmentId, { - staticMemberIds: newMembers, - }); - } - /** - * Check if a user belongs to a segment - */ - async isUserInSegment(userId: string, segmentOrId: Segment | string): Promise { - const members = await this.getSegmentMembers(segmentOrId); - return members.some((member) => member.id === userId); - } - const members = await this.evaluateSegmentRules(segment.rules); - return members.length; - } - - /** - * Get members of a static segment - */ - private async getStaticSegmentMembers(segmentId: string): Promise { - const segment = await this.segmentRepository.findOne({ - where: { id: segmentId }, - }); - - if (!segment?.staticMemberIds?.length) { - return []; - } - - // TODO: Fetch actual user data from Users module - // For now, return mock data - return segment.staticMemberIds.map((id) => ({ - id, - email: `user-${id}@example.com`, - createdAt: new Date(), - })); - } - - /** - * Evaluate segment rules and return matching users - */ - private async evaluateSegmentRules(rules: SegmentRule[]): Promise { - if (!rules.length) { - return []; - } - /** - * Calculate member count for a segment (accepts segment object to avoid recursion) - */ - private async calculateMemberCountForSegment(segment: Segment): Promise { - if (!segment.isDynamic) { - return segment.staticMemberIds?.length || 0; - } - const members = await this.evaluateSegmentRules(segment.rules); - return members.length; - } - /** - * Get members of a static segment - */ - private async getStaticSegmentMembers(segmentId: string): Promise { - const segment = await this.segmentRepository.findOne({ - where: { id: segmentId }, - }); - if (!segment?.staticMemberIds?.length) { - return []; - } - // TODO: Fetch actual user data from Users module - // For now, return mock data - return segment.staticMemberIds.map((id) => ({ - id, - email: `user-${id}@example.com`, - createdAt: new Date(), - })); - } - /** - * Evaluate segment rules and return matching users - */ - private async evaluateSegmentRules(rules: SegmentRule[]): Promise { - if (!rules.length) { - return []; - } - // TODO: Build actual query against User entity - // This is a placeholder implementation showing the query building logic - // Group rules by AND/OR logic - const sortedRules = [...rules].sort((a, b) => a.order - b.order); - // Build query conditions - const conditions: string[] = []; - for (const rule of sortedRules) { - const condition = this.buildRuleCondition(rule); - if (condition) { - conditions.push(condition); - } - } - // For MVP, return empty array - actual implementation requires User repository - // console.log('Segment rules would evaluate:', conditions); - // TODO: Execute query and return real users - return []; - } - /** - * Build SQL condition from a rule - */ - private buildRuleCondition(rule: SegmentRule): string | null { - const field = this.getFieldMapping(rule.field); - const operator = this.getOperatorMapping(rule.operator); - const value = this.formatValue(rule.value, rule.operator); - if (!field || !operator) { - return null; - } - return `${field} ${operator} ${value}`; - } - /** - * Map rule field to database column - */ - private getFieldMapping(field: SegmentRuleField): string | null { - const mapping: Record = { - [SegmentRuleField.EMAIL]: 'user.email', - [SegmentRuleField.FIRST_NAME]: 'user.firstName', - [SegmentRuleField.LAST_NAME]: 'user.lastName', - [SegmentRuleField.CREATED_AT]: 'user.createdAt', - [SegmentRuleField.LAST_LOGIN]: 'user.lastLoginAt', - [SegmentRuleField.COURSE_COUNT]: 'enrollments.count', - [SegmentRuleField.TOTAL_SPENT]: 'payments.total', - [SegmentRuleField.TAG]: 'user.tags', - [SegmentRuleField.COUNTRY]: 'user.country', - [SegmentRuleField.SUBSCRIPTION_STATUS]: 'subscription.status', - }; - return mapping[field] || null; - } - /** - * Map rule operator to SQL operator - */ - private getOperatorMapping(operator: SegmentRuleOperator): string | null { - const mapping: Record = { - [SegmentRuleOperator.EQUALS]: '=', - [SegmentRuleOperator.NOT_EQUALS]: '!=', - [SegmentRuleOperator.CONTAINS]: 'LIKE', - [SegmentRuleOperator.NOT_CONTAINS]: 'NOT LIKE', - [SegmentRuleOperator.STARTS_WITH]: 'LIKE', - [SegmentRuleOperator.ENDS_WITH]: 'LIKE', - [SegmentRuleOperator.GREATER_THAN]: '>', - [SegmentRuleOperator.LESS_THAN]: '<', - [SegmentRuleOperator.GREATER_OR_EQUAL]: '>=', - [SegmentRuleOperator.LESS_OR_EQUAL]: '<=', - [SegmentRuleOperator.IS_SET]: 'IS NOT NULL', - [SegmentRuleOperator.IS_NOT_SET]: 'IS NULL', - [SegmentRuleOperator.IN_LIST]: 'IN', - [SegmentRuleOperator.NOT_IN_LIST]: 'NOT IN', - [SegmentRuleOperator.BEFORE]: '<', - [SegmentRuleOperator.AFTER]: '>', - [SegmentRuleOperator.BETWEEN]: 'BETWEEN', - }; - return mapping[operator] || null; - } - /** - * Format value based on operator - */ - private formatValue(value: unknown, operator: SegmentRuleOperator): string { - if (operator === SegmentRuleOperator.CONTAINS || - operator === SegmentRuleOperator.NOT_CONTAINS) { - return `'%${value}%'`; - } - if (operator === SegmentRuleOperator.STARTS_WITH) { - return `'${value}%'`; - } - if (operator === SegmentRuleOperator.ENDS_WITH) { - return `'%${value}'`; - } - if (operator === SegmentRuleOperator.IN_LIST || operator === SegmentRuleOperator.NOT_IN_LIST) { - if (Array.isArray(value)) { - return `(${value.map((v) => `'${v}'`).join(', ')})`; - } - } - if (typeof value === 'string') { - return `'${value}'`; - } - return String(value); - } -} diff --git a/src/email-marketing/sender/email-sender.service.ts b/src/email-marketing/sender/email-sender.service.ts deleted file mode 100644 index ad558090..00000000 --- a/src/email-marketing/sender/email-sender.service.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import * as nodemailer from 'nodemailer'; -import { TemplateManagementService } from '../templates/template-management.service'; -import { EmailAnalyticsService } from '../analytics/email-analytics.service'; -import { EmailEventType } from '../enums/email-event-type.enum'; - -export interface ISendEmailOptions { - to: string; - subject?: string; - templateId?: string; - html?: string; - text?: string; - variables?: Record; - campaignId?: string; - recipientId?: string; - variantId?: string; - trackOpens?: boolean; - trackClicks?: boolean; -} - -export interface ISendEmailResult { - success: boolean; - messageId?: string; - error?: string; -} - -/** - * Provides email Sender operations. - */ -@Injectable() -export class EmailSenderService { - private readonly logger = new Logger(EmailSenderService.name); - private transporter: nodemailer.Transporter; - - constructor( - private readonly configService: ConfigService, - private readonly templateService: TemplateManagementService, - private readonly analyticsService: EmailAnalyticsService, - ) { - this.initializeTransporter(); - } - - private initializeTransporter(): void { - this.transporter = nodemailer.createTransport({ - host: this.configService.get('SMTP_HOST', 'smtp.mailtrap.io'), - port: this.configService.get('SMTP_PORT', 587), - secure: this.configService.get('SMTP_SECURE', false), - auth: { - user: this.configService.get('SMTP_USER'), - pass: this.configService.get('SMTP_PASS'), - }, - }); - } - - /** - * Send a single email - */ - async sendEmail(options: ISendEmailOptions): Promise { - try { - let html = options.html; - let text = options.text; - let subject = options.subject; - - // Render template if provided - if (options.templateId) { - const rendered = await this.templateService.renderTemplate( - options.templateId, - options.variables || {}, - ); - html = rendered.html; - text = rendered.text; - subject = rendered.subject; - } - - // Add tracking pixel for opens - if (options.trackOpens && options.campaignId && options.recipientId) { - html = this.addOpenTrackingPixel(html, options.campaignId, options.recipientId); - } - - // Track link clicks - if (options.trackClicks && options.campaignId && options.recipientId) { - html = this.wrapLinksForTracking(html, options.campaignId, options.recipientId); - } - - const fromEmail = this.configService.get('EMAIL_FROM', 'noreply@teachlink.io'); - const fromName = this.configService.get('EMAIL_FROM_NAME', 'TeachLink'); - - const result = await this.transporter.sendMail({ - from: `"${fromName}" <${fromEmail}>`, - to: options.to, - subject, - html, - text, - }); - - // Record sent event - if (options.campaignId && options.recipientId) { - await this.analyticsService.recordEvent( - options.campaignId, - options.recipientId, - EmailEventType.SENT, - { messageId: result.messageId, variantId: options.variantId }, - ); - } - - this.logger.log(`Email sent to ${options.to}, messageId: ${result.messageId}`); - - return { success: true, messageId: result.messageId }; - } catch (error) { - this.logger.error(`Failed to send email to ${options.to}:`, error); - - // Record bounce event - if (options.campaignId && options.recipientId) { - await this.analyticsService.recordEvent( - options.campaignId, - options.recipientId, - EmailEventType.BOUNCED, - { error: error.message }, - ); - } - - return { success: false, error: error.message }; - } - } - - /** - * Send bulk emails - */ - async sendBulkEmails( - recipients: Array<{ email: string; id: string; variables?: Record }>, - options: Omit, - ): Promise<{ sent: number; failed: number; results: ISendEmailResult[] }> { - const results: ISendEmailResult[] = []; - let sent = 0; - let failed = 0; - - for (const recipient of recipients) { - const result = await this.sendEmail({ - ...options, - to: recipient.email, - recipientId: recipient.id, - variables: { ...options.variables, ...recipient.variables }, - }); - - results.push(result); - if (result.success) sent++; - else failed++; - - // Small delay to avoid rate limiting - await this.delay(50); - } - /** - * Send a single email - */ - async sendEmail(options: SendEmailOptions): Promise { - try { - let html = options.html; - let text = options.text; - let subject = options.subject; - // Render template if provided - if (options.templateId) { - const rendered = await this.templateService.renderTemplate(options.templateId, options.variables || {}); - html = rendered.html; - text = rendered.text; - subject = rendered.subject; - } - // Add tracking pixel for opens - if (options.trackOpens && options.campaignId && options.recipientId) { - html = this.addOpenTrackingPixel(html, options.campaignId, options.recipientId); - } - // Track link clicks - if (options.trackClicks && options.campaignId && options.recipientId) { - html = this.wrapLinksForTracking(html, options.campaignId, options.recipientId); - } - const fromEmail = this.configService.get('EMAIL_FROM', 'noreply@teachlink.io'); - const fromName = this.configService.get('EMAIL_FROM_NAME', 'TeachLink'); - const result = await this.transporter.sendMail({ - from: `"${fromName}" <${fromEmail}>`, - to: options.to, - subject, - html, - text, - }); - // Record sent event - if (options.campaignId && options.recipientId) { - await this.analyticsService.recordEvent(options.campaignId, options.recipientId, EmailEventType.SENT, { messageId: result.messageId, variantId: options.variantId }); - } - this.logger.log(`Email sent to ${options.to}, messageId: ${result.messageId}`); - return { success: true, messageId: result.messageId }; - } - catch (error) { - this.logger.error(`Failed to send email to ${options.to}:`, error); - // Record bounce event - if (options.campaignId && options.recipientId) { - await this.analyticsService.recordEvent(options.campaignId, options.recipientId, EmailEventType.BOUNCED, { error: error.message }); - } - return { success: false, error: error.message }; - } - } - /** - * Send bulk emails - */ - async sendBulkEmails(recipients: Array<{ - email: string; - id: string; - variables?: Record; - }>, options: Omit): Promise<{ - sent: number; - failed: number; - results: SendEmailResult[]; - }> { - const results: SendEmailResult[] = []; - let sent = 0; - let failed = 0; - for (const recipient of recipients) { - const result = await this.sendEmail({ - ...options, - to: recipient.email, - recipientId: recipient.id, - variables: { ...options.variables, ...recipient.variables }, - }); - results.push(result); - if (result.success) - sent++; - else - failed++; - // Small delay to avoid rate limiting - await this.delay(50); - } - return { sent, failed, results }; - } - /** - * Verify SMTP connection - */ - async verifyConnection(): Promise { - try { - await this.transporter.verify(); - return true; - } - catch (error) { - this.logger.error('SMTP connection verification failed:', error); - return false; - } - } - // Private helper methods - private addOpenTrackingPixel(html: string, campaignId: string, recipientId: string): string { - const baseUrl = this.configService.get('APP_URL', 'http://localhost:3000'); - const trackingUrl = `${baseUrl}/email-marketing/track/open?c=${campaignId}&r=${recipientId}`; - const pixel = ``; - return html.replace('', `${pixel}`); - } - private wrapLinksForTracking(html: string, campaignId: string, recipientId: string): string { - const baseUrl = this.configService.get('APP_URL', 'http://localhost:3000'); - return html.replace(/]*href=["'])([^"']+)(["'][^>]*)>/gi, (match, prefix, url, suffix) => { - if (url.startsWith('mailto:') || url.startsWith('#')) { - return match; - } - const trackingUrl = `${baseUrl}/email-marketing/track/click?c=${campaignId}&r=${recipientId}&url=${encodeURIComponent(url)}`; - return ``; - }); - } - private delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } -} diff --git a/src/email-marketing/tracking/tracking.controller.ts b/src/email-marketing/tracking/tracking.controller.ts deleted file mode 100644 index 688a2bc9..00000000 --- a/src/email-marketing/tracking/tracking.controller.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { Controller, Get, Query, Res } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiExcludeEndpoint } from '@nestjs/swagger'; -import { Response } from 'express'; -import { EmailAnalyticsService } from '../analytics/email-analytics.service'; -import { EmailEventType } from '../enums/email-event-type.enum'; -/** - * Tracking controller for email opens and clicks - * These endpoints are called by tracking pixels and wrapped links in emails - */ -@ApiTags('Email Marketing - Tracking') -@Controller('email-marketing/track') -export class TrackingController { - // 1x1 transparent GIF pixel - private readonly TRACKING_PIXEL = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64'); - constructor(private readonly analyticsService: EmailAnalyticsService) { } - /** - * Track email open via 1x1 tracking pixel - * Called when email client loads the tracking image - */ - @Get('open') - @ApiExcludeEndpoint() // Hide from Swagger as it's for internal use - async trackOpen( - @Query('c') - campaignId: string, - @Query('r') - recipientId: string, - @Res() - res: Response): Promise { - // Record the open event asynchronously - if (campaignId && recipientId) { - this.analyticsService - .recordEvent(campaignId, recipientId, EmailEventType.OPENED, { - timestamp: new Date().toISOString(), - }) - .catch((error) => { - console.error('Failed to record open event:', error); - }); - } - // Return the tracking pixel - res.set({ - 'Content-Type': 'image/gif', - 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', - Pragma: 'no-cache', - Expires: '0', - }); - res.send(this.TRACKING_PIXEL); - } - /** - * Track link click and redirect to original URL - * Called when user clicks a tracked link in the email - */ - @Get('click') - @ApiOperation({ summary: 'Track email link click and redirect' }) - async trackClick( - @Query('c') - campaignId: string, - @Query('r') - recipientId: string, - @Query('url') - url: string, - @Res() - res: Response): Promise { - // Validate URL to prevent open redirect vulnerability - const decodedUrl = decodeURIComponent(url || ''); - if (!this.isValidRedirectUrl(decodedUrl)) { - res.status(400).send('Invalid URL'); - return; - } - // Record the click event asynchronously - if (campaignId && recipientId) { - this.analyticsService - .recordEvent(campaignId, recipientId, EmailEventType.CLICKED, { - url: decodedUrl, - timestamp: new Date().toISOString(), - }) - .catch((error) => { - console.error('Failed to record click event:', error); - }); - } - // Redirect to the original URL - res.redirect(302, decodedUrl); - } - /** - * Track email delivery (called by email service provider webhook) - */ - @Get('delivered') - @ApiExcludeEndpoint() - async trackDelivered( - @Query('c') - campaignId: string, - @Query('r') - recipientId: string, - @Res() - res: Response): Promise { - if (campaignId && recipientId) { - await this.analyticsService.recordEvent(campaignId, recipientId, EmailEventType.DELIVERED); - } - res.status(200).send('OK'); - } - /** - * Track email bounce (called by email service provider webhook) - */ - @Get('bounce') - @ApiExcludeEndpoint() - async trackBounce( - @Query('c') - campaignId: string, - @Query('r') - recipientId: string, - @Query('type') - bounceType: string, - @Res() - res: Response): Promise { - if (campaignId && recipientId) { - const eventType = bounceType === 'soft' ? EmailEventType.SOFT_BOUNCED : EmailEventType.BOUNCED; - await this.analyticsService.recordEvent(campaignId, recipientId, eventType, { bounceType }); - } - res.status(200).send('OK'); - } - /** - * Handle unsubscribe requests - */ - @Get('unsubscribe') - @ApiOperation({ summary: 'Unsubscribe from email list' }) - async unsubscribe( - @Query('c') - campaignId: string, - @Query('r') - recipientId: string, - @Query('email') - email: string, - @Res() - res: Response): Promise { - if (campaignId && recipientId) { - await this.analyticsService.recordEvent(campaignId, recipientId, EmailEventType.UNSUBSCRIBED, { email }); - } - // TODO: Actually unsubscribe the user in the subscription service - // Return a simple confirmation page - res.set('Content-Type', 'text/html'); - res.send(` - - - - Unsubscribed - TeachLink - - - -

You've been unsubscribed

-

You will no longer receive marketing emails from TeachLink.

-

If this was a mistake, you can update your preferences in your account settings.

- - - `); - } - /** - * Validate redirect URL to prevent open redirect attacks - */ - private isValidRedirectUrl(url: string): boolean { - if (!url) - return false; - try { - const parsed = new URL(url); - // Only allow http and https protocols - if (!['http:', 'https:'].includes(parsed.protocol)) { - return false; - } - // Optional: Add domain whitelist for extra security - // const allowedDomains = ['teachlink.io', 'www.teachlink.io']; - // if (!allowedDomains.includes(parsed.hostname)) { - // return false; - // } - return true; - } - catch { - return false; - } - } -} diff --git a/src/feature-flags/analytics/flag-analytics.service.ts b/src/feature-flags/analytics/flag-analytics.service.ts deleted file mode 100644 index 48cff8b9..00000000 --- a/src/feature-flags/analytics/flag-analytics.service.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - IExperimentStats, - IExperimentVariantStats, - IFlagAnalyticsEvent, - IFlagEvaluationStats, - IFlagSummary, -} from '../interfaces'; - -type TrackEvaluationInput = Omit; - -/** - * Provides flag Analytics operations. - */ -@Injectable() -export class FlagAnalyticsService { - /** flagKey → events */ - private readonly flagEvents = new Map(); - /** flagKey → Set of unique userIds */ - private readonly flagUsers = new Map>(); - /** experimentId → variantKey → impression count */ - private readonly experimentImpressions = new Map>(); - /** experimentId → variantKey → conversion count */ - private readonly experimentConversions = new Map>(); - - /** - * Records a flag evaluation event. - */ - trackEvaluation(input: TrackEvaluationInput): void { - const event: IFlagAnalyticsEvent = { - ...input, - eventId: this.generateEventId(), - timestamp: new Date(), - }; - - if (event.flagKey) { - if (!this.flagEvents.has(event.flagKey)) { - this.flagEvents.set(event.flagKey, []); - } - const flagEvents = this.flagEvents.get(event.flagKey); - if (flagEvents) flagEvents.push(event); - - if (event.userId) { - if (!this.flagUsers.has(event.flagKey)) { - this.flagUsers.set(event.flagKey, new Set()); - } - } - } - - /** - * Records an experiment impression (user saw a variant). - */ - trackImpression( - experimentId: string, - variantKey: string, - userId?: string, - flagKey?: string, - ): void { - this.incrementExperimentCounter(this.experimentImpressions, experimentId, variantKey); - - this.trackEvaluation({ - eventType: 'impression', - flagKey, - userId, - experimentId, - experimentVariantKey: variantKey, - }); - } - - /** - * Records an experiment conversion event. - */ - trackConversion( - experimentId: string, - variantKey: string, - userId?: string, - metadata?: Record, - ): void { - this.incrementExperimentCounter(this.experimentConversions, experimentId, variantKey); - - this.trackEvaluation({ - eventType: 'conversion', - userId, - experimentId, - experimentVariantKey: variantKey, - metadata, - }); - } - - /** - * Returns evaluation statistics for a flag. - * Optionally filters to events within the last `sinceHours` hours. - */ - getEvaluationStats(flagKey: string, sinceHours?: number): IFlagEvaluationStats { - const allEvents = this.flagEvents.get(flagKey) ?? []; - - const events = sinceHours - ? allEvents.filter((e) => { - const cutoff = new Date(Date.now() - sinceHours * 3_600_000); - return e.timestamp >= cutoff; - }) - : allEvents; - - const evaluationsByVariation: Record = {}; - const evaluationsByReason: Record = {}; - let errorCount = 0; - let evaluationCount = 0; - - for (const event of events) { - if (event.eventType !== 'evaluation') continue; - evaluationCount++; - - if (event.variationKey) { - evaluationsByVariation[event.variationKey] = - (evaluationsByVariation[event.variationKey] ?? 0) + 1; - } - - if (event.reason) { - evaluationsByReason[event.reason] = (evaluationsByReason[event.reason] ?? 0) + 1; - if (event.reason === 'ERROR') errorCount++; - } - } - - return { - flagKey, - totalEvaluations: evaluationCount, - evaluationsByVariation, - evaluationsByReason, - uniqueUsers: this.flagUsers.get(flagKey)?.size ?? 0, - errorRate: evaluationCount > 0 ? errorCount / evaluationCount : 0, - }; - } - - /** - * Returns impression and conversion stats for all variants in an experiment. - */ - getExperimentStats(experimentId: string, controlVariantKey?: string): IExperimentStats { - const impressions = this.experimentImpressions.get(experimentId) ?? new Map(); - const conversions = this.experimentConversions.get(experimentId) ?? new Map(); - - const allVariantKeys = new Set([...impressions.keys(), ...conversions.keys()]); - - let totalImpressions = 0; - const variants: Record = {}; - - for (const variantKey of allVariantKeys) { - const imp = impressions.get(variantKey) ?? 0; - const conv = conversions.get(variantKey) ?? 0; - totalImpressions += imp; - - variants[variantKey] = { - variantKey, - impressions: imp, - conversions: conv, - conversionRate: imp > 0 ? conv / imp : 0, - isControl: variantKey === controlVariantKey, - }; - } - - return { experimentId, totalImpressions, variants }; - } - - /** - * Returns the most evaluated flags, sorted by evaluation count descending. - */ - getTopFlags(limit: number = 10): IFlagSummary[] { - const summaries: IFlagSummary[] = []; - - for (const [flagKey, events] of this.flagEvents.entries()) { - const evaluations = events.filter((e) => e.eventType === 'evaluation'); - summaries.push({ - flagKey, - totalEvaluations: evaluations.length, - lastEvaluatedAt: events[events.length - 1]?.timestamp, - }); - } - - return summaries.sort((a, b) => b.totalEvaluations - a.totalEvaluations).slice(0, limit); - } - - /** - * Returns the most recent evaluation events for a flag in reverse-chronological order. - */ - getFlagEvaluationHistory(flagKey: string, limit: number = 100): IFlagAnalyticsEvent[] { - const events = this.flagEvents.get(flagKey) ?? []; - return events - .filter((e) => e.eventType === 'evaluation') - .slice(-limit) - .reverse(); - } - - /** - * Clears stored analytics. Pass a flagKey to clear only that flag's data, - * or call without arguments to wipe all analytics. - */ - clearAnalytics(flagKey?: string): void { - if (flagKey) { - this.flagEvents.delete(flagKey); - this.flagUsers.delete(flagKey); - return; - } - /** - * Returns the most evaluated flags, sorted by evaluation count descending. - */ - getTopFlags(limit: number = 10): FlagSummary[] { - const summaries: FlagSummary[] = []; - for (const [flagKey, events] of this.flagEvents.entries()) { - const evaluations = events.filter((e) => e.eventType === 'evaluation'); - summaries.push({ - flagKey, - totalEvaluations: evaluations.length, - lastEvaluatedAt: events[events.length - 1]?.timestamp, - }); - } - return summaries.sort((a, b) => b.totalEvaluations - a.totalEvaluations).slice(0, limit); - } - /** - * Returns the most recent evaluation events for a flag in reverse-chronological order. - */ - getFlagEvaluationHistory(flagKey: string, limit: number = 100): FlagAnalyticsEvent[] { - const events = this.flagEvents.get(flagKey) ?? []; - return events - .filter((e) => e.eventType === 'evaluation') - .slice(-limit) - .reverse(); - } - /** - * Clears stored analytics. Pass a flagKey to clear only that flag's data, - * or call without arguments to wipe all analytics. - */ - clearAnalytics(flagKey?: string): void { - if (flagKey) { - this.flagEvents.delete(flagKey); - this.flagUsers.delete(flagKey); - return; - } - this.flagEvents.clear(); - this.flagUsers.clear(); - this.experimentImpressions.clear(); - this.experimentConversions.clear(); - } - private incrementExperimentCounter(store: Map>, experimentId: string, variantKey: string): void { - if (!store.has(experimentId)) { - store.set(experimentId, new Map()); - } - const inner = store.get(experimentId) ?? new Map(); - inner.set(variantKey, (inner.get(variantKey) ?? 0) + 1); - } - private generateEventId(): string { - return `evt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; - } -} diff --git a/src/feature-flags/evaluation/flag-evaluation.service.ts b/src/feature-flags/evaluation/flag-evaluation.service.ts deleted file mode 100644 index a1229814..00000000 --- a/src/feature-flags/evaluation/flag-evaluation.service.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - EvaluationReason, - IFeatureFlag, - IFlagEvaluationResult, - FlagValueType, - IUserContext, -} from '../interfaces'; -import { FlagAnalyticsService } from '../analytics/flag-analytics.service'; -import { ExperimentationService } from '../experimentation/experimentation.service'; -import { RolloutService } from '../rollout/rollout.service'; -import { TargetingService } from '../targeting/targeting.service'; - -/** - * Provides flag Evaluation operations. - */ -@Injectable() -export class FlagEvaluationService { - private readonly flags = new Map(); - - constructor( - private readonly targetingService: TargetingService, - private readonly rolloutService: RolloutService, - private readonly experimentationService: ExperimentationService, - private readonly analyticsService: FlagAnalyticsService, - ) {} - - /** - * Evaluates a single feature flag for the given user context. - * - * Evaluation order: - * 1. Flag disabled / archived → off variation - * 2. Prerequisites → off variation if unmet - * 3. Targeting rules → matched variation - * 4. A/B experiment → assigned variant - * 5. Gradual rollout → default variation - * 6. Default → default variation - */ - evaluate(flagKey: string, userContext: IUserContext): IFlagEvaluationResult { - try { - const flag = this.flags.get(flagKey); - - if (!flag) { - const result = this.errorResult(flagKey); - this.recordEvaluation(result, userContext); - return result; - } - - if (flag.archived || !flag.enabled) { - const result = this.buildResult(flag, flag.offVariationKey, 'FLAG_DISABLED'); - this.recordEvaluation(result, userContext); - return result; - } - - // Prerequisites - if (flag.prerequisites?.length) { - for (const prereq of flag.prerequisites) { - const prereqResult = this.evaluate(prereq.flagKey, userContext); - if (prereqResult.variationKey !== prereq.requiredVariationKey) { - const result = this.buildResult(flag, flag.offVariationKey, 'PREREQUISITE_FAILED'); - this.recordEvaluation(result, userContext); - return result; - } - catch { - const flag = this.flags.get(flagKey); - const result = flag - ? this.buildResult(flag, flag.offVariationKey, 'ERROR') - : this.errorResult(flagKey); - this.recordEvaluation(result, userContext); - return result; - } - } - - // A/B experiment - if (flag.experiment) { - const experimentResult = this.experimentationService.assignVariant( - flag.experiment, - flagKey, - userContext, - ); - if (experimentResult) { - const result: IFlagEvaluationResult = { - flagKey, - value: false, - variationKey: 'off', - reason: 'ERROR', - timestamp: new Date(), - }; - } - } - - /** - * Evaluates all registered flags for the given user context. - */ - evaluateAll(userContext: IUserContext): Record { - const results: Record = {}; - for (const flagKey of this.flags.keys()) { - results[flagKey] = this.evaluate(flagKey, userContext); - } - return results; - } - - /** - * Convenience method — returns the boolean value of a flag. - */ - evaluateBoolean(flagKey: string, userContext: IUserContext, defaultValue = false): boolean { - const result = this.evaluate(flagKey, userContext); - return result.reason === 'ERROR' ? defaultValue : Boolean(result.value); - } - - /** - * Convenience method — returns the string value of a flag. - */ - evaluateString(flagKey: string, userContext: IUserContext, defaultValue = ''): string { - const result = this.evaluate(flagKey, userContext); - return result.reason === 'ERROR' ? defaultValue : String(result.value); - } - - /** - * Convenience method — returns the numeric value of a flag. - */ - evaluateNumber(flagKey: string, userContext: IUserContext, defaultValue = 0): number { - const result = this.evaluate(flagKey, userContext); - return result.reason === 'ERROR' ? defaultValue : Number(result.value); - } - - // --------------------------------------------------------------------------- - // Flag management - // --------------------------------------------------------------------------- - - setFlag(flag: IFeatureFlag): void { - this.flags.set(flag.key, { ...flag, updatedAt: new Date() }); - } - - setFlags(flags: IFeatureFlag[]): void { - for (const flag of flags) { - this.setFlag(flag); - } - } - - /** - * Updates flag. - * @param flagKey The flag key. - * @param updates The updates. - * @returns The operation result. - */ - updateFlag( - flagKey: string, - updates: Partial>, - ): IFeatureFlag | null { - const existing = this.flags.get(flagKey); - if (!existing) return null; - - const updated: IFeatureFlag = { - ...existing, - ...updates, - key: existing.key, - id: existing.id, - version: existing.version + 1, - updatedAt: new Date(), - }; - - this.flags.set(flagKey, updated); - return updated; - } - - /** - * Removes flag. - * @param flagKey The flag key. - * @returns Whether the operation succeeded. - */ - removeFlag(flagKey: string): boolean { - return this.flags.delete(flagKey); - } - - getFlag(flagKey: string): IFeatureFlag | undefined { - return this.flags.get(flagKey); - } - - getAllFlags(): IFeatureFlag[] { - return Array.from(this.flags.values()); - } - - // --------------------------------------------------------------------------- - // Helpers - // --------------------------------------------------------------------------- - - private buildResult( - flag: IFeatureFlag, - variationKey: string, - reason: EvaluationReason, - ruleId?: string, - ): IFlagEvaluationResult { - const variation = flag.variations.find((v) => v.key === variationKey); - const value: FlagValueType = variation?.value ?? flag.variations[0]?.value ?? false; - - return { flagKey: flag.key, value, variationKey, reason, ruleId, timestamp: new Date() }; - } - - private errorResult(flagKey: string): IFlagEvaluationResult { - return { - flagKey, - value: false, - variationKey: 'off', - reason: 'ERROR', - timestamp: new Date(), - }; - } - - private variationKeyForValue(flag: IFeatureFlag, value: FlagValueType): string { - return flag.variations.find((v) => v.value === value)?.key ?? flag.defaultVariationKey; - } - - private recordEvaluation(result: IFlagEvaluationResult, userContext: IUserContext): void { - this.analyticsService.trackEvaluation({ - eventType: 'evaluation', - flagKey: result.flagKey, - userId: userContext.userId, - sessionId: userContext.sessionId, - variationKey: result.variationKey, - experimentId: result.experimentId, - experimentVariantKey: result.experimentVariantKey, - reason: result.reason, - value: result.value, - }); - } -} diff --git a/src/feature-flags/experimentation/experimentation.service.ts b/src/feature-flags/experimentation/experimentation.service.ts deleted file mode 100644 index 31f8410d..00000000 --- a/src/feature-flags/experimentation/experimentation.service.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - IExperimentConfig, - IExperimentResult, - IExperimentVariant, - IUserContext, -} from '../interfaces'; -import { RolloutService } from '../rollout/rollout.service'; - -interface IConversionRecord { - eventName: string; - metadata?: Record; - timestamp: Date; -} - -/** - * Provides experimentation operations. - */ -@Injectable() -export class ExperimentationService { - /** variantKey → userId → IConversionRecord[] */ - private readonly conversions = new Map>(); - - constructor(private readonly rolloutService: RolloutService) {} - - /** - * Assigns a user to an experiment variant using consistent hashing. - * Returns null if the experiment is inactive or the user is outside traffic allocation. - */ - assignVariant( - config: IExperimentConfig, - flagKey: string, - userContext: IUserContext, - ): IExperimentResult | null { - if (config.status !== 'running') return null; - - const now = new Date(); - if (config.startDate && now < config.startDate) return null; - if (config.endDate && now > config.endDate) return null; - - if (!this.isInExperimentTraffic(config, flagKey, userContext)) return null; - - const variant = this.selectVariant(config, flagKey, userContext); - if (!variant) return null; - - return { - experimentId: config.experimentId, - variantKey: variant.key, - value: variant.value, - isControl: variant.isControl ?? false, - }; - } - - /** - * Records a conversion event for a user in an experiment. - */ - trackConversion( - experimentId: string, - userId: string, - eventName: string, - metadata?: Record, - ): void { - if (!this.conversions.has(experimentId)) { - this.conversions.set(experimentId, new Map()); - } - /** - * Records a conversion event for a user in an experiment. - */ - trackConversion(experimentId: string, userId: string, eventName: string, metadata?: Record): void { - if (!this.conversions.has(experimentId)) { - this.conversions.set(experimentId, new Map()); - } - const expConversions = this.conversions.get(experimentId) ?? new Map(); - if (!expConversions.has(userId)) { - expConversions.set(userId, []); - } - const userConversions = expConversions.get(userId) ?? []; - userConversions.push({ eventName, metadata, timestamp: new Date() }); - } - - const userConversions = expConversions.get(userId) ?? []; - userConversions.push({ eventName, metadata, timestamp: new Date() }); - } - - /** - * Returns all recorded conversion records for an experiment. - */ - getConversions(experimentId: string): Map { - return this.conversions.get(experimentId) ?? new Map(); - } - - /** - * Returns the list of experiment IDs that currently have conversion data. - */ - getActiveExperimentIds(): string[] { - return Array.from(this.conversions.keys()); - } - - /** - * Checks whether a user falls within the experiment's traffic allocation percentage. - * Uses a separate hash seed from variant assignment to avoid correlation. - */ - private isInExperimentTraffic( - config: IExperimentConfig, - flagKey: string, - userContext: IUserContext, - ): boolean { - const bucketValue = this.resolveBucketAttributeValue( - config.bucketByAttribute ?? 'userId', - userContext, - ); - const trafficBucketKey = `${flagKey}:${config.experimentId}:traffic:${bucketValue}`; - const bucket = this.rolloutService.computeBucketValue(trafficBucketKey); - return bucket < config.trafficAllocation; - } - - /** - * Selects a variant for the user using weighted bucket assignment. - * The same user always receives the same variant for the same experiment. - */ - private selectVariant( - config: IExperimentConfig, - flagKey: string, - userContext: IUserContext, - ): IExperimentVariant | null { - if (!config.variants || config.variants.length === 0) return null; - - const bucketValue = this.resolveBucketAttributeValue( - config.bucketByAttribute ?? 'userId', - userContext, - ); - const variantBucketKey = `${flagKey}:${config.experimentId}:variant:${bucketValue}`; - const bucket = this.rolloutService.computeBucketValue(variantBucketKey); - - const totalWeight = config.variants.reduce((sum, v) => sum + v.weight, 0); - if (totalWeight === 0) return null; - - const normalizedBucket = (bucket / 100) * totalWeight; - let cumulative = 0; - - for (const variant of config.variants) { - cumulative += variant.weight; - if (normalizedBucket < cumulative) { - return variant; - } - } - - return config.variants[config.variants.length - 1]; - } - - private resolveBucketAttributeValue(attribute: string, userContext: IUserContext): string { - switch (attribute) { - case 'userId': - return userContext.userId; - case 'sessionId': - return userContext.sessionId ?? userContext.userId; - case 'email': - return userContext.email ?? userContext.userId; - default: - return userContext.attributes?.[attribute]?.toString() ?? userContext.userId; - } -} diff --git a/src/feature-flags/feature-flags.controller.ts b/src/feature-flags/feature-flags.controller.ts deleted file mode 100644 index ccd857a8..00000000 --- a/src/feature-flags/feature-flags.controller.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; -import { FlagEvaluationService } from '../evaluation/flag-evaluation.service'; -import { Roles } from '../../auth/decorators/roles.decorator'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; -import { RolesGuard } from '../../auth/guards/roles.guard'; -import { UserRole } from '../../users/entities/user.entity'; -import { IFeatureFlag, IUserContext } from '../interfaces'; - -interface ICreateFlagDto { - key: string; - description?: string; - enabled?: boolean; - valueType?: 'boolean' | 'string' | 'number' | 'json'; - defaultValue?: any; - variations?: Array<{ key: string; value: any }>; - defaultVariationKey?: string; - offVariationKey?: string; -} - -interface IUpdateFlagDto { - description?: string; - enabled?: boolean; - defaultValue?: any; - variations?: Array<{ key: string; value: any }>; - defaultVariationKey?: string; - offVariationKey?: string; - targeting?: any; - rollout?: any; - experiment?: any; -} - -interface IEvaluateFlagDto { - userId: string; - sessionId?: string; - email?: string; - attributes?: Record; -} - -@ApiTags('feature-flags') -@Controller('feature-flags') -@UseGuards(JwtAuthGuard, RolesGuard) -@ApiBearerAuth() -export class FeatureFlagsController { - constructor(private readonly flagEvaluationService: FlagEvaluationService) {} - - @Get() - @Roles(UserRole.ADMIN) - @ApiOperation({ summary: 'Get all feature flags' }) - getAllFlags(): IFeatureFlag[] { - return this.flagEvaluationService.getAllFlags(); - } - - @Get(':key') - @Roles(UserRole.ADMIN) - @ApiOperation({ summary: 'Get a specific feature flag' }) - getFlag(@Param('key') key: string): IFeatureFlag | undefined { - return this.flagEvaluationService.getFlag(key); - } - - @Post() - @Roles(UserRole.ADMIN) - @ApiOperation({ summary: 'Create a new feature flag' }) - createFlag(@Body() dto: ICreateFlagDto): { message: string; flag: IFeatureFlag } { - const flag: IFeatureFlag = { - id: `flag_${Date.now()}`, - key: dto.key, - description: dto.description || '', - enabled: dto.enabled !== false, - archived: false, - valueType: dto.valueType || 'boolean', - variations: dto.variations || [ - { key: 'on', value: dto.defaultValue ?? true }, - { key: 'off', value: dto.defaultValue ?? false }, - ], - defaultValue: dto.defaultValue ?? false, - defaultVariationKey: dto.defaultVariationKey || 'on', - offVariationKey: dto.offVariationKey || 'off', - version: 1, - createdAt: new Date(), - updatedAt: new Date(), - }; - - this.flagEvaluationService.setFlag(flag); - - return { message: 'Feature flag created successfully', flag }; - } - - @Put(':key') - @Roles(UserRole.ADMIN) - @ApiOperation({ summary: 'Update a feature flag' }) - updateFlag( - @Param('key') key: string, - @Body() dto: IUpdateFlagDto, - ): { message: string; flag: IFeatureFlag | null } { - const updated = this.flagEvaluationService.updateFlag(key, dto); - - return { - message: updated ? 'Feature flag updated successfully' : 'Feature flag not found', - flag: updated, - }; - } - - @Delete(':key') - @Roles(UserRole.ADMIN) - @ApiOperation({ summary: 'Delete a feature flag' }) - deleteFlag(@Param('key') key: string): { message: string } { - const deleted = this.flagEvaluationService.removeFlag(key); - - return { - message: deleted ? 'Feature flag deleted successfully' : 'Feature flag not found', - }; - } - - @Post(':key/enable') - @Roles(UserRole.ADMIN) - @ApiOperation({ summary: 'Enable a feature flag' }) - enableFlag(@Param('key') key: string): { message: string } { - const flag = this.flagEvaluationService.getFlag(key); - if (!flag) { - return { message: 'Feature flag not found' }; - } - - this.flagEvaluationService.updateFlag(key, { enabled: true }); - return { message: `Feature flag ${key} enabled` }; - } - - @Post(':key/disable') - @Roles(UserRole.ADMIN) - @ApiOperation({ summary: 'Disable a feature flag' }) - disableFlag(@Param('key') key: string): { message: string } { - const flag = this.flagEvaluationService.getFlag(key); - if (!flag) { - return { message: 'Feature flag not found' }; - } - - this.flagEvaluationService.updateFlag(key, { enabled: false }); - return { message: `Feature flag ${key} disabled` }; - } - - @Post('evaluate') - @ApiOperation({ summary: 'Evaluate a feature flag for a user' }) - evaluateFlag(@Query('key') key: string, @Body() dto: IEvaluateFlagDto) { - const userContext: IUserContext = { - userId: dto.userId, - sessionId: dto.sessionId, - email: dto.email, - attributes: dto.attributes || {}, - }; - - return this.flagEvaluationService.evaluate(key, userContext); - } - - @Post('evaluate-all') - @ApiOperation({ summary: 'Evaluate all feature flags for a user' }) - evaluateAllFlags(@Body() dto: IEvaluateFlagDto) { - const userContext: IUserContext = { - userId: dto.userId, - sessionId: dto.sessionId, - email: dto.email, - attributes: dto.attributes || {}, - }; - - return this.flagEvaluationService.evaluateAll(userContext); - } -} diff --git a/src/feature-flags/feature-flags.module.ts b/src/feature-flags/feature-flags.module.ts deleted file mode 100644 index 7382f231..00000000 --- a/src/feature-flags/feature-flags.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Module } from '@nestjs/common'; -import { FlagEvaluationService } from './evaluation/flag-evaluation.service'; -import { TargetingService } from './targeting/targeting.service'; -import { RolloutService } from './rollout/rollout.service'; -import { ExperimentationService } from './experimentation/experimentation.service'; -import { FlagAnalyticsService } from './analytics/flag-analytics.service'; -import { FeatureFlagsController } from './feature-flags.controller'; - -/** - * Registers the feature Flags module. - */ -@Module({ - controllers: [FeatureFlagsController], - providers: [ - FlagAnalyticsService, - TargetingService, - RolloutService, - ExperimentationService, - FlagEvaluationService, - ], - exports: [ - FlagEvaluationService, - TargetingService, - RolloutService, - ExperimentationService, - FlagAnalyticsService, - ], -}) -export class FeatureFlagsModule { -} diff --git a/src/feature-flags/rollout/rollout.service.ts b/src/feature-flags/rollout/rollout.service.ts deleted file mode 100644 index 31c168e3..00000000 --- a/src/feature-flags/rollout/rollout.service.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IRolloutConfig, IUserContext } from '../interfaces'; - -/** - * Provides rollout operations. - */ -@Injectable() -export class RolloutService { - /** - * Determines whether a user falls within the configured rollout percentage. - * Uses consistent hashing so the same user always gets the same result. - */ - isUserInRollout(config: IRolloutConfig, flagKey: string, userContext: IUserContext): boolean { - const now = new Date(); - - if (config.startDate && now < config.startDate) return false; - if (config.endDate && now > config.endDate) return false; - - const currentPercentage = this.getCurrentPercentage(config); - if (currentPercentage <= 0) return false; - if (currentPercentage >= 100) return true; - - const bucketKey = this.resolveBucketKey(config.bucketByAttribute ?? 'userId', userContext); - const bucketValue = this.computeBucketValue(`${flagKey}:${bucketKey}`); - - return bucketValue < currentPercentage; - } - - /** - * Returns the effective rollout percentage at the current time, - * accounting for any ramp schedule defined on the config. - */ - getCurrentPercentage(config: IRolloutConfig): number { - if (!config.rampSchedule || config.rampSchedule.length === 0) { - return config.percentage; - } - /** - * Returns the effective rollout percentage at the current time, - * accounting for any ramp schedule defined on the config. - */ - getCurrentPercentage(config: RolloutConfig): number { - if (!config.rampSchedule || config.rampSchedule.length === 0) { - return config.percentage; - } - const now = new Date(); - const sortedSteps = [...config.rampSchedule].sort((a, b) => a.at.getTime() - b.at.getTime()); - let effective = 0; - for (const step of sortedSteps) { - if (now >= step.at) { - effective = step.percentage; - } - else { - break; - } - } - return Math.min(effective, config.percentage); - } - /** - * DJB2 hash — fast, deterministic, and well-distributed. - * Returns a value in the range [0, 99]. - */ - computeBucketValue(key: string): number { - let hash = 5381; - for (let i = 0; i < key.length; i++) { - hash = ((hash << 5) + hash + key.charCodeAt(i)) >>> 0; - } - return hash % 100; - } - return hash % 100; - } - - private resolveBucketKey(attribute: string, userContext: IUserContext): string { - switch (attribute) { - case 'userId': - return userContext.userId; - case 'sessionId': - return userContext.sessionId ?? userContext.userId; - case 'email': - return userContext.email ?? userContext.userId; - default: - return userContext.attributes?.[attribute]?.toString() ?? userContext.userId; - } -} diff --git a/src/feature-flags/targeting/targeting.service.ts b/src/feature-flags/targeting/targeting.service.ts deleted file mode 100644 index 7a05c564..00000000 --- a/src/feature-flags/targeting/targeting.service.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - ConditionOperator, - FlagValueType, - ITargetingCondition, - ITargetingConfig, - ITargetingRule, - IUserContext, -} from '../interfaces'; - -/** - * Provides targeting operations. - */ -@Injectable() -export class TargetingService { - /** - * Evaluates targeting rules against a user context. - * Returns the matched variation key, or null if no rule matches. - */ - evaluateTargeting(config: ITargetingConfig, userContext: IUserContext): string | null { - const sortedRules = [...config.rules].sort((a, b) => a.priority - b.priority); - - for (const rule of sortedRules) { - if (this.evaluateRule(rule, userContext)) { - return rule.serveVariationKey; - } - } - - return null; - } - - private evaluateRule(rule: ITargetingRule, userContext: IUserContext): boolean { - if (!rule.conditions || rule.conditions.length === 0) return false; - - if (rule.conditionsOperator === 'OR') { - return rule.conditions.some((c) => this.evaluateCondition(c, userContext)); - } - - return rule.conditions.every((c) => this.evaluateCondition(c, userContext)); - } - - private evaluateCondition(condition: ITargetingCondition, userContext: IUserContext): boolean { - const attributeValue = this.resolveAttribute(condition.attribute, userContext); - - return this.applyOperator(condition.operator, attributeValue, condition.value); - } - - private applyOperator( - operator: ConditionOperator, - attributeValue: unknown, - conditionValue?: FlagValueType | FlagValueType[], - ): boolean { - switch (operator) { - case 'exists': - return attributeValue !== null && attributeValue !== undefined; - - case 'notExists': - return attributeValue === null || attributeValue === undefined; - - case 'equals': - return String(attributeValue) === String(conditionValue); - - case 'notEquals': - return String(attributeValue) !== String(conditionValue); - - case 'contains': - return ( - typeof attributeValue === 'string' && - typeof conditionValue === 'string' && - attributeValue.toLowerCase().includes(conditionValue.toLowerCase()) - ); - - case 'notContains': - return ( - typeof attributeValue === 'string' && - typeof conditionValue === 'string' && - !attributeValue.toLowerCase().includes(conditionValue.toLowerCase()) - ); - - case 'startsWith': - return ( - typeof attributeValue === 'string' && - typeof conditionValue === 'string' && - attributeValue.toLowerCase().startsWith(conditionValue.toLowerCase()) - ); - - case 'endsWith': - return ( - typeof attributeValue === 'string' && - typeof conditionValue === 'string' && - attributeValue.toLowerCase().endsWith(conditionValue.toLowerCase()) - ); - - case 'greaterThan': - return Number(attributeValue) > Number(conditionValue); - - case 'greaterThanOrEqual': - return Number(attributeValue) >= Number(conditionValue); - - case 'lessThan': - return Number(attributeValue) < Number(conditionValue); - - case 'lessThanOrEqual': - return Number(attributeValue) <= Number(conditionValue); - - case 'in': - if (!Array.isArray(conditionValue)) return false; - return conditionValue.map(String).includes(String(attributeValue)); - - case 'notIn': - if (!Array.isArray(conditionValue)) return false; - return !conditionValue.map(String).includes(String(attributeValue)); - - case 'regex': - if (typeof conditionValue !== 'string' || typeof attributeValue !== 'string') { - return false; - } - return null; - } - private evaluateRule(rule: TargetingRule, userContext: UserContext): boolean { - if (!rule.conditions || rule.conditions.length === 0) - return false; - if (rule.conditionsOperator === 'OR') { - return rule.conditions.some((c) => this.evaluateCondition(c, userContext)); - } - return rule.conditions.every((c) => this.evaluateCondition(c, userContext)); - } - } - - /** - * Resolves an attribute name from the user context. - * Checks top-level properties first, then custom attributes map. - */ - private resolveAttribute(attribute: string, userContext: IUserContext): unknown { - const topLevel: Record = { - userId: userContext.userId, - email: userContext.email, - country: userContext.country, - plan: userContext.plan, - sessionId: userContext.sessionId, - ipAddress: userContext.ipAddress, - }; - - if (attribute in topLevel) { - return topLevel[attribute]; - } - private applyOperator(operator: ConditionOperator, attributeValue: unknown, conditionValue?: FlagValueType | FlagValueType[]): boolean { - switch (operator) { - case 'exists': - return attributeValue !== null && attributeValue !== undefined; - case 'notExists': - return attributeValue === null || attributeValue === undefined; - case 'equals': - return String(attributeValue) === String(conditionValue); - case 'notEquals': - return String(attributeValue) !== String(conditionValue); - case 'contains': - return (typeof attributeValue === 'string' && - typeof conditionValue === 'string' && - attributeValue.toLowerCase().includes(conditionValue.toLowerCase())); - case 'notContains': - return (typeof attributeValue === 'string' && - typeof conditionValue === 'string' && - !attributeValue.toLowerCase().includes(conditionValue.toLowerCase())); - case 'startsWith': - return (typeof attributeValue === 'string' && - typeof conditionValue === 'string' && - attributeValue.toLowerCase().startsWith(conditionValue.toLowerCase())); - case 'endsWith': - return (typeof attributeValue === 'string' && - typeof conditionValue === 'string' && - attributeValue.toLowerCase().endsWith(conditionValue.toLowerCase())); - case 'greaterThan': - return Number(attributeValue) > Number(conditionValue); - case 'greaterThanOrEqual': - return Number(attributeValue) >= Number(conditionValue); - case 'lessThan': - return Number(attributeValue) < Number(conditionValue); - case 'lessThanOrEqual': - return Number(attributeValue) <= Number(conditionValue); - case 'in': - if (!Array.isArray(conditionValue)) - return false; - return conditionValue.map(String).includes(String(attributeValue)); - case 'notIn': - if (!Array.isArray(conditionValue)) - return false; - return !conditionValue.map(String).includes(String(attributeValue)); - case 'regex': - if (typeof conditionValue !== 'string' || typeof attributeValue !== 'string') { - return false; - } - try { - return new RegExp(conditionValue).test(attributeValue); - } - catch { - return false; - } - default: - return false; - } - } - /** - * Resolves an attribute name from the user context. - * Checks top-level properties first, then custom attributes map. - */ - private resolveAttribute(attribute: string, userContext: UserContext): unknown { - const topLevel: Record = { - userId: userContext.userId, - email: userContext.email, - country: userContext.country, - plan: userContext.plan, - sessionId: userContext.sessionId, - ipAddress: userContext.ipAddress, - }; - if (attribute in topLevel) { - return topLevel[attribute]; - } - if (attribute === 'roles') { - return userContext.roles?.join(',') ?? null; - } - if (attribute === 'groups') { - return userContext.groups?.join(',') ?? null; - } - if (userContext.attributes && attribute in userContext.attributes) { - return userContext.attributes[attribute]; - } - return null; - } -} diff --git a/src/gamification/challenges/challenges.service.ts b/src/gamification/challenges/challenges.service.ts deleted file mode 100644 index 0a70335d..00000000 --- a/src/gamification/challenges/challenges.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Challenge } from '../entities/challenge.entity'; -import { UserChallenge } from '../entities/user-challenge.entity'; -import { User } from '../../users/entities/user.entity'; - -/** - * Provides challenges operations. - */ -@Injectable() -export class ChallengesService { - constructor( - @InjectRepository(Challenge) - private challengeRepository: Repository, - @InjectRepository(UserChallenge) - private userChallengeRepository: Repository, - ) {} - - async updateProgress( - userId: string, - challengeId: string, - increment: number, - ): Promise { - let userChallenge = await this.userChallengeRepository.findOne({ - where: { user: { id: userId }, challenge: { id: challengeId } }, - relations: ['challenge'], - }); - - if (!userChallenge) { - const challenge = await this.challengeRepository.findOne({ where: { id: challengeId } }); - if (!challenge) throw new Error('Challenge not found'); - - userChallenge = this.userChallengeRepository.create({ - user: { id: userId } as User, - challenge, - progressValue: 0, - isCompleted: false, - }); - } - async getUserChallenges(userId: string): Promise { - return await this.userChallengeRepository.find({ - where: { user: { id: userId } }, - relations: ['challenge'], - }); - } -} diff --git a/src/gamification/gamification.controller.ts b/src/gamification/gamification.controller.ts deleted file mode 100644 index 808be5e5..00000000 --- a/src/gamification/gamification.controller.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common'; -import { GamificationService } from './gamification.service'; -import { PointsService } from './points/points.service'; -import { BadgesService } from './badges/badges.service'; -import { LeaderboardService } from './leaderboards/leaderboards.service'; -import { ChallengesService } from './challenges/challenges.service'; - -/** - * Exposes gamification endpoints. - */ -@Controller('gamification') -export class GamificationController { - constructor( - private readonly gamificationService: GamificationService, - private readonly pointsService: PointsService, - private readonly badgesService: BadgesService, - private readonly leaderboardService: LeaderboardService, - private readonly challengesService: ChallengesService, - ) {} - - /** - * Returns progress. - * @param userId The user identifier. - * @returns The operation result. - */ - @Get('progress/:userId') - async getProgress(@Param('userId') userId: string) { - return this.pointsService.getUserProgress(userId); - } - - /** - * Returns badges. - * @param userId The user identifier. - * @returns The operation result. - */ - @Get('badges/:userId') - async getBadges(@Param('userId') userId: string) { - return this.badgesService.getUserBadges(userId); - } - - /** - * Returns challenges. - * @param userId The user identifier. - * @returns The operation result. - */ - @Get('challenges/:userId') - async getChallenges(@Param('userId') userId: string) { - return this.challengesService.getUserChallenges(userId); - } - - /** - * Returns leaderboard. - * @param limit The maximum number of results. - * @returns The operation result. - */ - @Get('leaderboard') - async getLeaderboard(@Query('limit') limit?: number) { - return this.leaderboardService.getTopPlayers(limit); - } - - /** - * Records activity. - * @param userId The user identifier. - * @param type The type. - * @param points The points. - * @returns The operation result. - */ - @Post('activity/:userId') - async recordActivity( - @Param('userId') userId: string, - @Body('type') type: string, - @Body('points') points?: number, - ) { - return this.gamificationService.handleActivity(userId, type, points); - } -} diff --git a/src/gamification/gamification.module.ts b/src/gamification/gamification.module.ts deleted file mode 100644 index cb74430c..00000000 --- a/src/gamification/gamification.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { PointTransaction } from './entities/point-transaction.entity'; -import { UserProgress } from './entities/user-progress.entity'; -import { Badge } from './entities/badge.entity'; -import { UserBadge } from './entities/user-badge.entity'; -import { Challenge } from './entities/challenge.entity'; -import { UserChallenge } from './entities/user-challenge.entity'; -import { PointsService } from './points/points.service'; -import { BadgesService } from './badges/badges.service'; -import { LeaderboardService } from './leaderboards/leaderboards.service'; -import { ChallengesService } from './challenges/challenges.service'; -import { GamificationService } from './gamification.service'; -import { GamificationController } from './gamification.controller'; - -/** - * Registers the gamification module. - */ -@Module({ - imports: [ - TypeOrmModule.forFeature([ - PointTransaction, - UserProgress, - Badge, - UserBadge, - Challenge, - UserChallenge, - ]), - ], - providers: [ - PointsService, - BadgesService, - LeaderboardService, - ChallengesService, - GamificationService, - ], - controllers: [GamificationController], - exports: [GamificationService, PointsService, BadgesService, ChallengesService], -}) -export class GamificationModule { -} diff --git a/src/gamification/gamification.service.ts b/src/gamification/gamification.service.ts deleted file mode 100644 index 14e752e7..00000000 --- a/src/gamification/gamification.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PointsService } from './points/points.service'; -import { BadgesService } from './badges/badges.service'; -import { ChallengesService } from './challenges/challenges.service'; - -/** - * Provides gamification operations. - */ -@Injectable() -export class GamificationService { - constructor(private pointsService: PointsService, private badgesService: BadgesService, private challengesService: ChallengesService) { } - async handleActivity(userId: string, activityType: string, points: number = 10): Promise { - // 1. Add points - const progress = await this.pointsService.addPoints(userId, points, activityType); - // 2. Check for badge awarding (simple example: award badge if points > 500) - if (progress.totalPoints >= 500) { - // Assuming a badge with ID 'early-achiever' exists - // await this.badgesService.awardBadge(userId, 'early-achiever-id'); - } - // 3. Update active challenges based on activity - // This would normally involve complex logic to match activityType to challenge goals - return progress; - } -} diff --git a/src/gateways/messaging/messaging.gateway.ts b/src/gateways/messaging/messaging.gateway.ts deleted file mode 100644 index c7983837..00000000 --- a/src/gateways/messaging/messaging.gateway.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; -import { UseGuards, Logger } from '@nestjs/common'; -import { Socket } from 'socket.io'; -import { WsJwtAuthGuard } from '../../auth/guards/ws-jwt-auth.guard'; -import { WsThrottlerGuard } from '../../common/guards/ws-throttler.guard'; -import { wsManager } from '../../common/utils/websocket.utils'; -import { JwtService } from '@nestjs/jwt'; -import { MESSAGING_GATEWAY_EVENTS } from '../../collaboration/constants/collaboration-events.constants'; - -/** - * Handles messaging gateway events. - */ -@WebSocketGateway({ namespace: '/messaging' }) -@UseGuards(WsThrottlerGuard) -export class MessagingGateway implements OnGatewayConnection, OnGatewayDisconnect { - private readonly logger = new Logger(MessagingGateway.name); - - constructor(private readonly jwtService: JwtService) {} - - /** - * Handles connection. - * @param client The client. - * @returns The operation result. - */ - async handleConnection(client: Socket) { - try { - const token = - client.handshake.auth?.token || client.handshake.headers?.authorization?.split(' ')[1]; - if (!token) { - throw new Error('No token provided'); - } - - const payload = await this.jwtService.verifyAsync(token); - const userId = payload.sub || payload.id; - - const registered = wsManager.registerConnection(userId, client); - if (!registered) { - return; - } - this.logger.log(`Client connected: ${client.id}`); - } catch (_error) { - client.emit('error', { message: 'Unauthorized' }); - client.disconnect(true); - } - } - - /** - * Handles disconnect. - * @param client The client. - * @returns The operation result. - */ - handleDisconnect(client: Socket) { - wsManager.cleanupSocket(client); - this.logger.log(`Client disconnected: ${client.id}`); - } - - /** - * Handles message. - * @param data The data to process. - * @param client The client. - * @returns The operation result. - */ - @UseGuards(WsJwtAuthGuard) - @SubscribeMessage(MESSAGING_GATEWAY_EVENTS.SEND_MESSAGE) - async handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) { - const user = (client as any).user; - return { userId: user.sub, message: data }; - } -} diff --git a/src/gateways/notifications/notifications.gateway.ts b/src/gateways/notifications/notifications.gateway.ts deleted file mode 100644 index 8f978295..00000000 --- a/src/gateways/notifications/notifications.gateway.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - WebSocketGateway, - WebSocketServer, - SubscribeMessage, - OnGatewayConnection, - OnGatewayDisconnect, - ConnectedSocket, - MessageBody, -} from '@nestjs/websockets'; -import { Server, Socket } from 'socket.io'; -import { UseGuards, Logger } from '@nestjs/common'; -import { WsJwtAuthGuard } from '../../auth/guards/ws-jwt-auth.guard'; -import { Notification } from '../../notifications/entities/notification.entity'; -import { wsManager } from '../../common/utils/websocket.utils'; -import { WsThrottlerGuard } from '../../common/guards/ws-throttler.guard'; -import { NOTIFICATION_GATEWAY_EVENTS } from '../../collaboration/constants/collaboration-events.constants'; -import { JwtService } from '@nestjs/jwt'; - -@WebSocketGateway({ - cors: { - origin: '*', - }, - namespace: 'notifications', -}) -@UseGuards(WsThrottlerGuard) -export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect { - @WebSocketServer() - server: Server; - - private readonly logger = new Logger(NotificationsGateway.name); - - constructor(private readonly jwtService: JwtService) {} - - async handleConnection(client: Socket) { - try { - const token = - client.handshake.auth?.token || client.handshake.headers?.authorization?.split(' ')[1]; - - if (!token) { - this.logger.warn(`Connection attempt without token: ${client.id}`); - client.emit('error', { message: 'Unauthorized: No token provided' }); - client.disconnect(true); - return; - } - - const payload = await this.jwtService.verifyAsync(token); - const userId = payload.sub || payload.id; - const roles = payload.roles || []; - - if (!userId) { - throw new Error('User ID not found in token'); - } - - const registered = wsManager.registerConnection(userId, client); - if (!registered) { - // Connection rejected by wsManager (e.g. limit reached) - return; - } - - // Join default channels - client.join(`user:${userId}`); - client.join('broadcast'); - - // Join role-based channels - if (Array.isArray(roles)) { - roles.forEach((role: string) => { - client.join(`role:${role}`); - this.logger.debug(`Client ${client.id} joined role channel: role:${role}`); - }); - } - - this.logger.log(`Client connected and authenticated: ${client.id} (User: ${userId})`); - client.emit('authenticated', { userId }); - } catch (error) { - this.logger.error(`Connection authentication failed: ${error.message}`); - client.emit('error', { message: 'Unauthorized' }); - client.disconnect(true); - } - } - - handleDisconnect(client: Socket) { - wsManager.cleanupSocket(client); - this.logger.log(`Client disconnected: ${client.id}`); - } - - @UseGuards(WsJwtAuthGuard) - @SubscribeMessage(NOTIFICATION_GATEWAY_EVENTS.SUBSCRIBE) - async handleSubscribe( - @ConnectedSocket() client: Socket, - @MessageBody() data: { userId?: string }, - ) { - const user = (client as any).user; - const userId = user?.id || user?.sub || data?.userId; - - if (!userId) { - this.logger.warn(`Subscription attempt without user context: ${client.id}`); - return { status: 'error', message: 'User context missing' }; - } - - // Re-ensure joined to user channel if needed - client.join(`user:${userId}`); - this.logger.log(`User ${userId} explicitly subscribed to notifications`); - - return { status: 'subscribed', userId }; - } - - /** - * Send notification to a specific user in real-time - */ - async sendToUser(userId: string, notification: Notification) { - this.server.to(`user:${userId}`).emit(NOTIFICATION_GATEWAY_EVENTS.NOTIFICATION, notification); - this.logger.debug(`Notification sent to user:${userId}`); - } - - /** - * Send notification to a specific role in real-time - */ - async sendToRole(role: string, notification: Partial) { - this.server.to(`role:${role}`).emit(NOTIFICATION_GATEWAY_EVENTS.NOTIFICATION, notification); - this.logger.debug(`Notification sent to role:${role}`); - } - - /** - * Broadcast notification to all connected users - */ - async broadcast(notification: Partial) { - this.server.emit(NOTIFICATION_GATEWAY_EVENTS.BROADCAST_NOTIFICATION, notification); - this.logger.debug('Broadcast notification sent'); - } -} diff --git a/src/graphql/graphql.module.ts b/src/graphql/graphql.module.ts deleted file mode 100644 index 0a80f58e..00000000 --- a/src/graphql/graphql.module.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Module } from '@nestjs/common'; -import { GraphQLModule as NestGraphQLModule } from '@nestjs/graphql'; -import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; -import { join } from 'path'; -import { validate, GraphQLError } from 'graphql'; -import { PubSub } from 'graphql-subscriptions'; -import { UsersModule } from '../users/users.module'; -import { CoursesModule } from '../courses/courses.module'; -import { AssessmentsModule } from '../assessment/assessment.module'; -import { AuthModule } from '../auth/auth.module'; -import { QueryResolver } from './resolvers/query.resolver'; -import { MutationResolver } from './resolvers/mutation.resolver'; -import { SubscriptionResolver } from './resolvers/subscription.resolver'; -import { UserResolver } from './resolvers/user.resolver'; -import { CourseResolver } from './resolvers/course.resolver'; -import { AssessmentResolver } from './resolvers/assessment.resolver'; -import { DataLoaderService } from './services/dataloader.service'; -import { QueryComplexityService } from './services/query-complexity.service'; -import { SchemaLintService } from './services/schema-lint.service'; -import { DirectiveValidationService } from './services/directive-validation.service'; -import { ComplexityAnalysisService } from './services/complexity-analysis.service'; - -/** - * Registers the graph QL module. - */ -@Module({ - imports: [ - NestGraphQLModule.forRootAsync({ - driver: ApolloDriver, - useFactory: (complexityService: ComplexityAnalysisService) => ({ - autoSchemaFile: join(process.cwd(), 'src/graphql/schema/schema.graphql'), - sortSchema: true, - playground: process.env.NODE_ENV !== 'production', - subscriptions: { - 'graphql-ws': true, - 'subscriptions-transport-ws': true, - }, - - // ── Schema Validation: complexity + depth rules per request ── - validationRules: [ - // depth-limit rule applied globally - (context) => { - const maxDepth = complexityService.config.maxDepth; - return { - OperationDefinition(node) { - const depth = getDepth(node); - if (depth > maxDepth) { - context.reportError( - new GraphQLError( - `Query depth ${depth} exceeds maximum allowed depth of ${maxDepth}`, - ), - ); - } - }, - }; - }, - ], - - plugins: [ - // ── Per-request complexity analysis plugin ── - { - requestDidStart: (_requestContext) => ({ - didResolveOperation: ({ request, document, schema }) => { - const variables = request.variables ?? {}; - const rule = complexityService.buildComplexityRule(schema, document, variables); - const errors = validate(schema, document, [rule]); - if (errors.length > 0) { - throw errors[0]; - } - }, - }), - } as any, - ], - - context: ({ req, connection }, _, { injector }) => { - if (connection) { - return { req: connection.context }; - } - const dataLoaderService = injector?.get(DataLoaderService); - const loaders = dataLoaderService?.createLoaders() || {}; - return { req, loaders }; - }, - - formatError: (error) => ({ - message: error.message, - code: error.extensions?.code, - path: error.path, - }), - }), - inject: [ComplexityAnalysisService], - }), - UsersModule, - CoursesModule, - AssessmentsModule, - AuthModule, - ], - providers: [ - QueryResolver, - MutationResolver, - SubscriptionResolver, - UserResolver, - CourseResolver, - AssessmentResolver, - DataLoaderService, - QueryComplexityService, - SchemaLintService, - DirectiveValidationService, - ComplexityAnalysisService, - { - provide: 'PUB_SUB', - useValue: new PubSub(), - }, - ], - exports: [ - DataLoaderService, - QueryComplexityService, - SchemaLintService, - DirectiveValidationService, - ComplexityAnalysisService, - 'PUB_SUB', - ], -}) -export class GraphQLModule {} - -// ── Helper: calculate selection depth from AST node ── -function getDepth(node: any, depth = 0): number { - if (!node?.selectionSet?.selections) return depth; - return Math.max(...node.selectionSet.selections.map((s: any) => getDepth(s, depth + 1))); -} diff --git a/src/graphql/middleware/dataloader.middleware.ts b/src/graphql/middleware/dataloader.middleware.ts deleted file mode 100644 index fbcd0573..00000000 --- a/src/graphql/middleware/dataloader.middleware.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Injectable, NestMiddleware } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { DataLoaderService } from '../services/dataloader.service'; -/** - * Middleware to inject DataLoaders into request context - * Creates fresh loaders for each request to ensure proper caching scope - */ -@Injectable() -export class DataLoaderMiddleware implements NestMiddleware { - constructor(private readonly dataLoaderService: DataLoaderService) {} - - /** - * Executes use. - * @param req The req. - * @param res The res. - * @param next The next. - * @returns The operation result. - */ - use(req: Request, res: Response, next: NextFunction) { - // Attach loaders to request object for GraphQL context - (req as any).loaders = this.dataLoaderService.createLoaders(); - next(); - } -} diff --git a/src/graphql/resolvers/course.resolver.ts b/src/graphql/resolvers/course.resolver.ts deleted file mode 100644 index af62e51b..00000000 --- a/src/graphql/resolvers/course.resolver.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Resolver, ResolveField, Parent, Context } from '@nestjs/graphql'; -import { CourseType } from '../types/course.type'; -import { UserType } from '../types/user.type'; -import { UsersService } from '../../users/users.service'; -/** - * Field Resolver for Course type - * Handles nested field resolution with DataLoader optimization - */ -@Resolver(() => CourseType) -export class CourseResolver { - constructor(private readonly usersService: UsersService) {} - - /** - * Executes instructor. - * @param course The course. - * @param context The context. - * @returns The operation result. - */ - @ResolveField(() => UserType, { nullable: true }) - async instructor( - @Parent() course: CourseType, - @Context() context: any, - ): Promise { - if (!course.instructor) { - return null; - } -} diff --git a/src/graphql/resolvers/index.ts b/src/graphql/resolvers/index.ts deleted file mode 100644 index ea466abc..00000000 --- a/src/graphql/resolvers/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './query.resolver'; -export * from './mutation.resolver'; -export * from './subscription.resolver'; -export * from './user.resolver'; -export * from './course.resolver'; -export * from './assessment.resolver'; diff --git a/src/graphql/resolvers/mutation.resolver.ts b/src/graphql/resolvers/mutation.resolver.ts deleted file mode 100644 index e4a1b28f..00000000 --- a/src/graphql/resolvers/mutation.resolver.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { Resolver, Mutation, Args, ID } from '@nestjs/graphql'; -import { UseGuards, Inject } from '@nestjs/common'; -import { PubSub } from 'graphql-subscriptions'; -import { UsersService } from '../../users/users.service'; -import { CoursesService } from '../../courses/courses.service'; -import { AssessmentsService } from '../../assessment/assessments.service'; -import { CourseType } from '../types/course.type'; -import { AssessmentType } from '../types/assessment.type'; -import { UserType } from '../types/user.type'; -import { CreateUserInput, UpdateUserInput } from '../inputs/user.input'; -import { CreateCourseInput, UpdateCourseInput } from '../inputs/course.input'; -import { CreateAssessmentInput, UpdateAssessmentInput } from '../inputs/assessment.input'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; -/** - * Main Mutation Resolver for GraphQL API - * Handles all write operations with real-time notifications - */ -@Resolver() -export class MutationResolver { - constructor( - private readonly usersService: UsersService, - private readonly coursesService: CoursesService, - private readonly assessmentsService: AssessmentsService, - @Inject('PUB_SUB') private readonly pubSub: PubSub, - ) {} - - // User Mutations - /** - * Creates user. - * @param input The input. - * @returns The resulting user type. - */ - @Mutation(() => UserType) - async createUser(@Args('input') input: CreateUserInput): Promise { - const user = await this.usersService.create(input); - await this.pubSub.publish('userCreated', { userCreated: user }); - return user; - } - - /** - * Updates user. - * @param id The identifier. - * @param input The input. - * @returns The resulting user type. - */ - @Mutation(() => UserType) - @UseGuards(JwtAuthGuard) - async updateUser( - @Args('id', { type: () => ID }) id: string, - @Args('input') input: UpdateUserInput, - ): Promise { - const user = await this.usersService.update(id, input); - await this.pubSub.publish('userUpdated', { userUpdated: user }); - return user; - } - - /** - * Removes user. - * @param id The identifier. - * @returns Whether the operation succeeded. - */ - @Mutation(() => Boolean) - @UseGuards(JwtAuthGuard) - async deleteUser(@Args('id', { type: () => ID }) id: string): Promise { - await this.usersService.remove(id); - await this.pubSub.publish('userDeleted', { userDeleted: { id } }); - return true; - } - - // Course Mutations - /** - * Creates course. - * @param input The input. - * @returns The resulting course type. - */ - @Mutation(() => CourseType) - @UseGuards(JwtAuthGuard) - async createCourse(@Args('input') input: CreateCourseInput): Promise { - const course = await this.coursesService.create(input); - await this.pubSub.publish('courseCreated', { courseCreated: course }); - return course; - } - - /** - * Updates course. - * @param id The identifier. - * @param input The input. - * @returns The resulting course type. - */ - @Mutation(() => CourseType) - @UseGuards(JwtAuthGuard) - async updateCourse( - @Args('id', { type: () => ID }) id: string, - @Args('input') input: UpdateCourseInput, - ): Promise { - const course = await this.coursesService.update(id, input); - await this.pubSub.publish('courseUpdated', { courseUpdated: course }); - return course; - } - - /** - * Removes course. - * @param id The identifier. - * @returns Whether the operation succeeded. - */ - @Mutation(() => Boolean) - @UseGuards(JwtAuthGuard) - async deleteCourse(@Args('id', { type: () => ID }) id: string): Promise { - await this.coursesService.remove(id); - await this.pubSub.publish('courseDeleted', { courseDeleted: { id } }); - return true; - } - - // Assessment Mutations - /** - * Creates assessment. - * @param input The input. - * @returns The resulting assessment type. - */ - @Mutation(() => AssessmentType) - @UseGuards(JwtAuthGuard) - async createAssessment(@Args('input') input: CreateAssessmentInput): Promise { - const assessment = await this.assessmentsService.create(input); - await this.pubSub.publish('assessmentCreated', { - assessmentCreated: assessment, - }); - return assessment; - } - - /** - * Updates assessment. - * @param id The identifier. - * @param input The input. - * @returns The resulting assessment type. - */ - @Mutation(() => AssessmentType) - @UseGuards(JwtAuthGuard) - async updateAssessment( - @Args('id', { type: () => ID }) id: string, - @Args('input') input: UpdateAssessmentInput, - ): Promise { - const assessment = await this.assessmentsService.update(id, input); - await this.pubSub.publish('assessmentUpdated', { - assessmentUpdated: assessment, - }); - return assessment; - } - - /** - * Removes assessment. - * @param id The identifier. - * @returns Whether the operation succeeded. - */ - @Mutation(() => Boolean) - @UseGuards(JwtAuthGuard) - async deleteAssessment(@Args('id', { type: () => ID }) id: string): Promise { - await this.assessmentsService.remove(id); - await this.pubSub.publish('assessmentDeleted', { - assessmentDeleted: { id }, - }); - return true; - } -} diff --git a/src/graphql/resolvers/query.resolver.ts b/src/graphql/resolvers/query.resolver.ts deleted file mode 100644 index 7da28d9d..00000000 --- a/src/graphql/resolvers/query.resolver.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { IPaginatedResponse } from '../../common/utils/pagination.util'; -import { Resolver, Query, Args, ID, Context } from '@nestjs/graphql'; -import { UseGuards } from '@nestjs/common'; -import { UsersService } from '../../users/users.service'; -import { CoursesService } from '../../courses/courses.service'; -import { AssessmentsService } from '../../assessment/assessments.service'; -import { UserType } from '../types/user.type'; -import { CourseType } from '../types/course.type'; -import { AssessmentType } from '../types/assessment.type'; -import { UserFilterInput } from '../inputs/user.input'; -import { CourseFilterInput } from '../inputs/course.input'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; -/** - * Main Query Resolver for GraphQL API - * Handles all read operations with optimized data fetching - */ -@Resolver() -export class QueryResolver { - constructor( - private readonly usersService: UsersService, - private readonly coursesService: CoursesService, - private readonly assessmentsService: AssessmentsService, - ) {} - - // User Queries - /** - * Executes user. - * @param id The identifier. - * @param context The context. - * @returns The resulting user type. - */ - @Query(() => UserType, { nullable: true }) - @UseGuards(JwtAuthGuard) - async user( - @Args('id', { type: () => ID }) id: string, - @Context() context: any, - ): Promise { - const { userLoader } = context.loaders || {}; - if (userLoader) { - return userLoader.load(id); - } - return this.usersService.findOne(id); - } - - /** - * Executes users. - * @param filter The filter criteria. - * @returns The resulting paginated response. - */ - @Query(() => [UserType]) - @UseGuards(JwtAuthGuard) - async users( - @Args('filter', { type: () => UserFilterInput, nullable: true }) - filter?: UserFilterInput, - ): Promise> { - return this.usersService.findAll(filter); - } - - /** - * Executes me. - * @param context The context. - * @returns The resulting user type. - */ - @Query(() => UserType) - @UseGuards(JwtAuthGuard) - async me(@Context() context: any): Promise { - const userId = context.req.user?.userId; - if (!userId) { - throw new Error('User not authenticated'); - } - return this.usersService.findOne(userId); - } - - // Course Queries - /** - * Executes course. - * @param id The identifier. - * @param context The context. - * @returns The resulting course type. - */ - @Query(() => CourseType, { nullable: true }) - async course( - @Args('id', { type: () => ID }) id: string, - @Context() context: any, - ): Promise { - const { courseLoader } = context.loaders || {}; - if (courseLoader) { - return courseLoader.load(id); - } - return this.coursesService.findOne(id); - } - - /** - * Executes courses. - * @param filter The filter criteria. - * @returns The resulting paginated response. - */ - @Query(() => [CourseType]) - async courses( - @Args('filter', { type: () => CourseFilterInput, nullable: true }) - filter?: CourseFilterInput, - ): Promise> { - return this.coursesService.findAll(filter); - } - - /** - * Executes my Courses. - * @param context The context. - * @returns The matching results. - */ - @Query(() => [CourseType]) - @UseGuards(JwtAuthGuard) - async myCourses(@Context() context: any): Promise { - const userId = context.req.user?.userId; - if (!userId) { - throw new Error('User not authenticated'); - } - return this.coursesService.findByInstructor(userId); - } - - // Assessment Queries - /** - * Executes assessment. - * @param id The identifier. - * @param context The context. - * @returns The resulting assessment type. - */ - @Query(() => AssessmentType, { nullable: true }) - @UseGuards(JwtAuthGuard) - async assessment( - @Args('id', { type: () => ID }) id: string, - @Context() context: any, - ): Promise { - const { assessmentLoader } = context.loaders || {}; - if (assessmentLoader) { - return assessmentLoader.load(id); - } - return this.assessmentsService.findOne(id); - } - - /** - * Executes assessments. - * @returns The matching results. - */ - @Query(() => [AssessmentType]) - @UseGuards(JwtAuthGuard) - async assessments(): Promise { - return this.assessmentsService.findAll(); - } -} diff --git a/src/graphql/resolvers/user.resolver.ts b/src/graphql/resolvers/user.resolver.ts deleted file mode 100644 index 7a6f3eae..00000000 --- a/src/graphql/resolvers/user.resolver.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Resolver, ResolveField, Parent, Context } from '@nestjs/graphql'; -import { UserType } from '../types/user.type'; -import { CourseType } from '../types/course.type'; -import { CoursesService } from '../../courses/courses.service'; -/** - * Field Resolver for User type - * Handles nested field resolution with DataLoader optimization - */ -@Resolver(() => UserType) -export class UserResolver { - constructor(private readonly coursesService: CoursesService) {} - - /** - * Executes courses. - * @param user The user. - * @param context The context. - * @returns The matching results. - */ - @ResolveField(() => [CourseType]) - async courses(@Parent() user: UserType, @Context() context: any): Promise { - const { coursesByInstructorLoader } = context.loaders || {}; - - if (coursesByInstructorLoader) { - return coursesByInstructorLoader.load(user.id); - } -} diff --git a/src/graphql/services/complexity-analysis.service.ts b/src/graphql/services/complexity-analysis.service.ts deleted file mode 100644 index ce7eb3db..00000000 --- a/src/graphql/services/complexity-analysis.service.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { GraphQLSchema, DocumentNode, validate } from 'graphql'; -import { - createComplexityRule, - simpleEstimator, - fieldExtensionsEstimator, - ComplexityEstimator, -} from 'graphql-query-complexity'; - -export interface IComplexityConfig { - maxComplexity: number; - maxDepth: number; - defaultCost: number; -} - -export interface IComplexityAnalysisResult { - allowed: boolean; - complexity: number; - depth: number; - errors: string[]; -} - -@Injectable() -export class ComplexityAnalysisService { - private readonly logger = new Logger(ComplexityAnalysisService.name); - - readonly config: IComplexityConfig = { - maxComplexity: Number(process.env.GRAPHQL_MAX_COMPLEXITY) || 100, - maxDepth: Number(process.env.GRAPHQL_MAX_DEPTH) || 10, - defaultCost: 1, - }; - - buildComplexityRule( - schema: GraphQLSchema, - document: DocumentNode, - variables: Record, - ) { - const estimators: ComplexityEstimator[] = [ - fieldExtensionsEstimator(), - simpleEstimator({ defaultComplexity: this.config.defaultCost }), - ]; - - return createComplexityRule({ - maximumComplexity: this.config.maxComplexity, - variables, - estimators, - onComplete: (complexity: number) => { - this.logger.log(`Query complexity: ${complexity} / ${this.config.maxComplexity}`); - if (complexity > this.config.maxComplexity * 0.8) { - this.logger.warn( - `High complexity query detected: ${complexity} (threshold: ${this.config.maxComplexity})`, - ); - } - }, - createError: (max: number, actual: number) => { - const msg = `Query too complex: score ${actual} exceeds maximum of ${max}`; - this.logger.error(msg); - return new Error(msg); - }, - }); - } - - analyzeQuery( - schema: GraphQLSchema, - document: DocumentNode, - variables: Record = {}, - ): IComplexityAnalysisResult { - const result: IComplexityAnalysisResult = { - allowed: true, - complexity: 0, - depth: 0, - errors: [], - }; - - // Depth check - result.depth = this.calculateDepth(document); - if (result.depth > this.config.maxDepth) { - result.allowed = false; - result.errors.push( - `Query depth ${result.depth} exceeds max allowed depth of ${this.config.maxDepth}`, - ); - } - - // Complexity check via validate - const validationErrors = validate(schema, document, [ - this.buildComplexityRule(schema, document, variables), - ]); - - if (validationErrors.length > 0) { - result.allowed = false; - result.errors.push(...validationErrors.map((e) => e.message)); - } - - return result; - } - - private calculateDepth(document: DocumentNode): number { - let maxDepth = 0; - - const traverse = (node: any, depth: number) => { - if (!node) return; - if (depth > maxDepth) maxDepth = depth; - - const selections = - node.selectionSet?.selections || - node.definitions?.flatMap((d: any) => d.selectionSet?.selections ?? []) || - []; - - for (const selection of selections) { - traverse(selection, depth + 1); - } - }; - - traverse(document, 0); - return maxDepth; - } -} diff --git a/src/graphql/services/dataloader.service.ts b/src/graphql/services/dataloader.service.ts deleted file mode 100644 index a2dbc5fc..00000000 --- a/src/graphql/services/dataloader.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import DataLoader from 'dataloader'; -import { UsersService } from '../../users/users.service'; -import { CoursesService } from '../../courses/courses.service'; -import { AssessmentsService } from '../../assessment/assessments.service'; -import { User } from '../../users/entities/user.entity'; -import { Course } from '../../courses/entities/course.entity'; -import { Assessment } from '../../assessment/entities/assessment.entity'; -/** - * DataLoader service to prevent N+1 query problems - * Batches and caches database requests within a single GraphQL request - */ -@Injectable() -export class DataLoaderService { - constructor(private readonly usersService: UsersService, private readonly coursesService: CoursesService, private readonly assessmentsService: AssessmentsService) { } - /** - * Create a new DataLoader for batching user queries by ID - */ - createUserLoader(): DataLoader { - return new DataLoader(async (userIds: readonly string[]) => { - const users = await this.usersService.findByIds(Array.from(userIds)); - const userMap = new Map(users.map((user) => [user.id, user])); - return userIds.map((id) => userMap.get(id) || null); - }); - } - /** - * Create a new DataLoader for batching course queries by ID - */ - createCourseLoader(): DataLoader { - return new DataLoader(async (courseIds: readonly string[]) => { - const courses = await this.coursesService.findByIds(Array.from(courseIds)); - const courseMap = new Map(courses.map((course) => [course.id, course])); - return courseIds.map((id) => courseMap.get(id) || null); - }); - } - /** - * Create a new DataLoader for batching assessment queries by ID - */ - createAssessmentLoader(): DataLoader { - return new DataLoader(async (assessmentIds: readonly string[]) => { - const assessments = await this.assessmentsService.findByIds(Array.from(assessmentIds)); - const assessmentMap = new Map(assessments.map((assessment) => [assessment.id, assessment])); - return assessmentIds.map((id) => assessmentMap.get(id) || null); - }); - } - /** - * Create a new DataLoader for batching courses by instructor ID - */ - createCoursesByInstructorLoader(): DataLoader { - return new DataLoader(async (instructorIds: readonly string[]) => { - const courses = await this.coursesService.findByInstructorIds(Array.from(instructorIds)); - const coursesByInstructor = new Map(); - courses.forEach((course) => { - const instructorId = course.instructor?.id; - if (instructorId) { - if (!coursesByInstructor.has(instructorId)) { - coursesByInstructor.set(instructorId, []); - } - coursesByInstructor.get(instructorId).push(course); - } - }); - return instructorIds.map((id) => coursesByInstructor.get(id) || []); - }); - } - /** - * Create all loaders for a GraphQL request context - */ - createLoaders() { - return { - userLoader: this.createUserLoader(), - courseLoader: this.createCourseLoader(), - assessmentLoader: this.createAssessmentLoader(), - coursesByInstructorLoader: this.createCoursesByInstructorLoader(), - }; - } -} diff --git a/src/graphql/services/query-complexity.service.ts b/src/graphql/services/query-complexity.service.ts deleted file mode 100644 index 07d17064..00000000 --- a/src/graphql/services/query-complexity.service.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { GraphQLSchema, FieldNode, OperationDefinitionNode, visit, parse } from 'graphql'; -import { GRAPHQL_CONSTANTS } from './query-complexity.constants'; - -/** - * Query complexity analysis configuration - */ -export interface IComplexityConfig { - /** Maximum query depth allowed (default: 10) */ - maxDepth: number; - /** Maximum complexity score allowed (default: 1000) */ - maxComplexity: number; - /** Complexity scalar multipliers for list fields */ - listScalarMultiplier: number; - /** Default complexity per field */ - defaultFieldComplexity: number; - /** Custom complexity values for specific types/fields */ - fieldComplexityMap: Record; -} -/** - * Complexity analysis result - */ -export interface IComplexityResult { - /** Total complexity score */ - complexity: number; - /** Query depth */ - depth: number; - /** Whether the query exceeds limits */ - allowed: boolean; - /** Error message if not allowed */ - error?: string; -} -/** - * Query Complexity Analysis Service - * - * Provides: - * - Query depth limiting - * - Field complexity calculation - * - Query cost analysis - */ -@Injectable() -export class QueryComplexityService { - private readonly logger = new Logger(QueryComplexityService.name); - - private readonly defaultConfig: IComplexityConfig = { - maxDepth: GRAPHQL_CONSTANTS.MAX_DEPTH, - maxComplexity: GRAPHQL_CONSTANTS.MAX_COMPLEXITY, - listScalarMultiplier: GRAPHQL_CONSTANTS.LIST_SCALAR_MULTIPLIER, - defaultFieldComplexity: 1, - fieldComplexityMap: GRAPHQL_CONSTANTS.FIELD_COMPLEXITY_MAP, - }; - - private config: IComplexityConfig; - private schema: GraphQLSchema | null = null; - - constructor() { - // Initialize with environment-based configuration - this.config = { - ...this.defaultConfig, - maxDepth: parseInt(process.env.GRAPHQL_MAX_DEPTH || `${GRAPHQL_CONSTANTS.MAX_DEPTH}`, 10), - maxComplexity: parseInt( - process.env.GRAPHQL_MAX_COMPLEXITY || `${GRAPHQL_CONSTANTS.MAX_COMPLEXITY}`, - 10, - ), - listScalarMultiplier: parseInt( - process.env.GRAPHQL_LIST_MULTIPLIER || `${GRAPHQL_CONSTANTS.LIST_SCALAR_MULTIPLIER}`, - 10, - ), - }; - } - - /** - * Set the GraphQL schema for complexity analysis - */ - setSchema(schema: GraphQLSchema): void { - this.schema = schema; - } - - /** - * Update complexity configuration - */ - setConfig(config: Partial): void { - this.config = { ...this.defaultConfig, ...config }; - } - - /** - * Analyze a query for complexity - */ - analyze(query: string, variables?: Record): IComplexityResult { - try { - // Parse the query to get AST - const ast = parse(query); - - // Find the operation - const operation = this.findOperation(ast); - if (!operation) { - return { - complexity: 0, - depth: 0, - allowed: true, - }; - } - /** - * Set the GraphQL schema for complexity analysis - */ - setSchema(schema: GraphQLSchema): void { - this.schema = schema; - } - /** - * Update complexity configuration - */ - setConfig(config: Partial): void { - this.config = { ...this.defaultConfig, ...config }; - } - /** - * Analyze a query for complexity - */ - analyze(query: string, variables?: Record): ComplexityResult { - try { - // Parse the query to get AST - const ast = parse(query); - // Find the operation - const operation = this.findOperation(ast); - if (!operation) { - return { - complexity: 0, - depth: 0, - allowed: true, - }; - } - // Calculate depth and complexity - const depth = this.calculateDepth(operation); - const complexity = this.calculateComplexity(operation, variables); - // Check if within limits - const allowed = depth <= this.config.maxDepth && complexity <= this.config.maxComplexity; - let error: string | undefined; - if (!allowed) { - if (depth > this.config.maxDepth) { - error = `Query depth ${depth} exceeds maximum allowed depth ${this.config.maxDepth}`; - } - else { - error = `Query complexity ${complexity} exceeds maximum allowed complexity ${this.config.maxComplexity}`; - } - this.logger.warn(`Query rejected: ${error}`); - } - return { - complexity, - depth, - allowed, - error, - }; - } - catch (error) { - this.logger.error(`Error analyzing query complexity: ${error.message}`); - // On parse error, allow the query to proceed (it will fail at execution anyway) - return { - complexity: 0, - depth: 0, - allowed: true, - error: undefined, - }; - } - } - - return null; - } - - /** - * Get current configuration - */ - getConfig(): IComplexityConfig { - return { ...this.config }; - } - - /** - * Validate a query against complexity limits - * Returns true if query is allowed, false otherwise - */ - validate(query: string, variables?: Record): boolean { - const result = this.analyze(query, variables); - return result.allowed; - } -} -/** - * GraphQL complexity validator function for Apollo Server - */ -export function createComplexityValidator(service: QueryComplexityService) { - return (query: string, variables?: Record): Error | undefined => { - const result = service.analyze(query, variables); - if (!result.allowed && result.error) { - return new Error(result.error); - } - return undefined; - }; -} diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts deleted file mode 100644 index a4cf834d..00000000 --- a/src/health/health.controller.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { Controller, Get, HttpStatus, OnModuleDestroy, Query, Res, UseGuards, } from '@nestjs/common'; -import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { DataSource } from 'typeorm'; -import Redis from 'ioredis'; -import { SkipThrottle } from '@nestjs/throttler'; -import { Response } from 'express'; -import { HealthService } from './health.service'; -import { ShutdownStateService } from '../common/services/shutdown-state.service'; -@SkipThrottle() -@ApiTags('health') -@ApiBearerAuth() -@UseGuards(JwtAuthGuard) -@Controller('health') -export class HealthController implements OnModuleDestroy { - private redis: Redis; - constructor(private readonly dataSource: DataSource, private readonly healthService: HealthService, private readonly shutdownState: ShutdownStateService) { - this.redis = new Redis({ - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }); - this.redis.on('error', () => { - // Health endpoint handles Redis failures explicitly in checkHealth. - }); - } - } - - @Get() - @ApiResponse({ status: HttpStatus.OK, description: 'Health check response' }) - async checkHealth() { - const healthStatus = await this.healthService.checkHealth(this.dataSource, this.redis); - return healthStatus; - } - - /** - * Validates liveness. - * @returns The operation result. - */ - @Get('liveness') - @ApiResponse({ status: HttpStatus.OK, description: 'Liveness check response' }) - @ApiResponse({ - status: HttpStatus.SERVICE_UNAVAILABLE, - description: 'Application is shutting down', - }) - checkLiveness(@Res() res: Response) { - if (this.shutdownState.isShuttingDown()) { - return res.status(HttpStatus.SERVICE_UNAVAILABLE).json({ - status: 'shutting_down', - timestamp: new Date().toISOString(), - }); - } - - return res.status(HttpStatus.OK).json({ - status: 'ok', - timestamp: new Date().toISOString(), - }); - } - - /** - * Validates readiness. - * @returns The operation result. - */ - @Get('readiness') - @ApiResponse({ - status: HttpStatus.OK, - description: 'Readiness check response', - }) - @ApiResponse({ - status: HttpStatus.SERVICE_UNAVAILABLE, - description: 'Application is shutting down', - }) - async checkReadiness(@Res() res: Response) { - if (this.shutdownState.isShuttingDown()) { - return res.status(HttpStatus.SERVICE_UNAVAILABLE).json({ - status: 'shutting_down', - timestamp: new Date().toISOString(), - }); - } - - const healthStatus = await this.healthService.checkReadiness(this.dataSource, this.redis); - return res.status(HttpStatus.OK).json(healthStatus); - } - - /** - * Validates dependencies. - * @param service The service. - * @returns The operation result. - */ - @Get('dependencies') - async checkDependencies(@Query('service') service?: string) { - const healthStatus = await this.healthService.checkHealth(this.dataSource, this.redis); - - if (service) { - return { - service, - status: healthStatus.services[service] || 'unknown', - details: healthStatus.details[service] || null, - timestamp: healthStatus.timestamp, - }; - } - - return { - dependencies: healthStatus.services, - details: healthStatus.details, - timestamp: healthStatus.timestamp, - overallStatus: healthStatus.status, - }; - } - - /** - * Validates database. - * @returns The operation result. - */ - @Get('database') - async checkDatabase() { - const healthStatus = await this.healthService.checkHealth(this.dataSource, this.redis); - return { - service: 'database', - status: healthStatus.services.database, - details: healthStatus.details.database, - timestamp: healthStatus.timestamp, - }; - } - - /** - * Validates redis. - * @returns The operation result. - */ - @Get('redis') - async checkRedis() { - const healthStatus = await this.healthService.checkHealth(this.dataSource, this.redis); - return { - service: 'redis', - status: healthStatus.services.redis, - details: healthStatus.details.redis, - timestamp: healthStatus.timestamp, - }; - } - - /** - * Validates queue. - * @returns The operation result. - */ - @Get('queue') - async checkQueue() { - const healthStatus = await this.healthService.checkHealth(this.dataSource, this.redis); - return { - service: 'queue', - status: healthStatus.services.queue, - details: healthStatus.details.queue, - timestamp: healthStatus.timestamp, - }; - } - - /** - * Validates cache. - * @returns The operation result. - */ - @Get('cache') - async checkCache() { - const healthStatus = await this.healthService.checkHealth(this.dataSource, this.redis); - return { - service: 'cache', - status: healthStatus.services.cache, - details: healthStatus.details.cache, - timestamp: healthStatus.timestamp, - }; - } - - /** - * Returns health Summary. - * @returns The operation result. - */ - @Get('summary') - async getHealthSummary() { - const healthStatus = await this.healthService.checkHealth(this.dataSource, this.redis); - - const serviceCount = Object.keys(healthStatus.services).length; - const healthyCount = Object.values(healthStatus.services).filter( - (status) => status === 'up', - ).length; - const degradedCount = Object.values(healthStatus.services).filter( - (status) => status === 'degraded' || status === 'warning', - ).length; - const criticalCount = Object.values(healthStatus.services).filter( - (status) => status === 'down' || status === 'critical', - ).length; - - return { - overall: healthStatus.status, - timestamp: healthStatus.timestamp, - uptime: healthStatus.uptime, - version: healthStatus.version, - environment: healthStatus.environment, - summary: { - total: serviceCount, - healthy: healthyCount, - degraded: degradedCount, - critical: criticalCount, - healthScore: Math.round((healthyCount / serviceCount) * 100), - }, - services: healthStatus.services, - }; - } -} diff --git a/src/health/health.module.ts b/src/health/health.module.ts deleted file mode 100644 index 95a64801..00000000 --- a/src/health/health.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { HealthController } from './health.controller'; -import { HealthService } from './health.service'; - -/** - * Registers the health module. - */ -@Module({ - controllers: [HealthController], - providers: [HealthService], - exports: [HealthService], -}) -export class HealthModule { -} diff --git a/src/health/health.service.ts b/src/health/health.service.ts deleted file mode 100644 index 2149509c..00000000 --- a/src/health/health.service.ts +++ /dev/null @@ -1,576 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { Redis } from 'ioredis'; -import * as fs from 'fs'; -import * as _path from 'path'; -import axios from 'axios'; - -export interface IHealthStatus { - status: 'ok' | 'degraded' | 'critical'; - timestamp: string; - uptime: number; - version?: string; - environment?: string; - services: { - database: string; - redis: string; - externalApis: Record; - disk: string; - queue?: string; - cache?: string; - featureFlags?: string; - bull?: string; - }; - details?: { - database?: { - responseTime: number; - connectionStatus: string; - connectionCount?: number; - maxConnections?: number; - }; - details?: { - database?: { - responseTime: number; - connectionStatus: string; - connectionCount?: number; - maxConnections?: number; - }; - redis?: { - responseTime: number; - connectionStatus: string; - memory?: { - used: number; - total: number; - percentage: number; - }; - }; - disk?: { - used: number; - total: number; - percentage: number; - }; - externalApis?: Record; - queue?: { - activeJobs: number; - waitingJobs: number; - failedJobs: number; - responseTime: number; - }; - cache?: { - hitRate: number; - missRate: number; - responseTime: number; - }; - featureFlags?: { - responseTime: number; - status: string; - }; - bull?: { - activeQueues: number; - totalJobs: number; - responseTime: number; - }; - }; -} - -/** - * Provides health operations. - */ -@Injectable() -export class HealthService { - private readonly logger = new Logger(HealthService.name); - private readonly startTime = Date.now(); - - // External API endpoints to check - private readonly externalApiEndpoints = [ - { name: 'stripe', url: process.env.STRIPE_HEALTH_URL, key: 'STRIPE_SECRET_KEY' }, - { name: 'sendgrid', url: process.env.SENDGRID_HEALTH_URL, key: 'SENDGRID_API_KEY' }, - { name: 'aws', url: process.env.AWS_HEALTH_URL, key: 'AWS_ACCESS_KEY_ID' }, - ]; - - // Disk space thresholds - private readonly diskWarningThreshold = 85; // 85% - private readonly diskCriticalThreshold = 95; // 95% - - async checkHealth(dataSource: DataSource, redis: Redis): Promise { - const healthStatus: IHealthStatus = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: Date.now() - this.startTime, - version: process.env.npm_package_version || 'unknown', - environment: process.env.NODE_ENV || 'development', - services: { - database: 'unknown', - redis: 'unknown', - externalApis: {}, - disk: 'unknown', - queue: 'unknown', - cache: 'unknown', - featureFlags: 'unknown', - bull: 'unknown', - }, - details: {}, - }; - - // Check database - const dbCheck = await this.checkDatabase(dataSource); - healthStatus.services.database = dbCheck.status; - healthStatus.details.database = { - responseTime: dbCheck.responseTime, - connectionStatus: dbCheck.status, - }; - if (dbCheck.status === 'down') { - healthStatus.status = 'degraded'; - } - async checkReadiness(dataSource: DataSource, redis: Redis): Promise { - // For readiness, we check if core dependencies are available - const healthStatus: HealthStatus = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: Date.now() - this.startTime, - services: { - database: 'unknown', - redis: 'unknown', - externalApis: {}, - disk: 'unknown', - }, - }; - try { - const dbCheck = await this.checkDatabase(dataSource); - healthStatus.services.database = dbCheck.status; - if (dbCheck.status === 'down') { - healthStatus.status = 'critical'; - } - } - catch { - healthStatus.services.database = 'down'; - healthStatus.status = 'critical'; - } - try { - const redisCheck = await this.checkRedis(redis); - healthStatus.services.redis = redisCheck.status; - if (redisCheck.status === 'down') { - healthStatus.status = 'critical'; - } - } - catch { - healthStatus.services.redis = 'down'; - healthStatus.status = 'critical'; - } - return healthStatus; - } - - // Check external APIs - const apiChecks = await this.checkExternalApis(); - healthStatus.services.externalApis = {}; - for (const [apiName, check] of Object.entries(apiChecks)) { - healthStatus.services.externalApis[apiName] = check.status; - } - healthStatus.details.externalApis = apiChecks; - - // Check if any external API is down - const anyApiDown = Object.values(apiChecks).some((check) => check.status === 'down'); - if (anyApiDown && healthStatus.status === 'ok') { - healthStatus.status = 'degraded'; - } - - // Check disk space - const diskCheck = await this.checkDiskSpace(); - healthStatus.services.disk = diskCheck.status; - healthStatus.details.disk = { - used: diskCheck.used, - total: diskCheck.total, - percentage: diskCheck.percentage, - }; - if (diskCheck.status === 'critical') { - healthStatus.status = 'critical'; - } else if (diskCheck.status === 'warning' && healthStatus.status === 'ok') { - healthStatus.status = 'degraded'; - } - - // Check queue service - const queueCheck = await this.checkQueueService(); - healthStatus.services.queue = queueCheck.status; - healthStatus.details.queue = queueCheck; - if (queueCheck.status === 'down' && healthStatus.status === 'ok') { - healthStatus.status = 'degraded'; - } - - // Check cache service - const cacheCheck = await this.checkCacheService(redis); - healthStatus.services.cache = cacheCheck.status; - healthStatus.details.cache = cacheCheck; - if (cacheCheck.status === 'down' && healthStatus.status === 'ok') { - healthStatus.status = 'degraded'; - } - - // Check feature flags service - const featureFlagsCheck = await this.checkFeatureFlagsService(); - healthStatus.services.featureFlags = featureFlagsCheck.status; - healthStatus.details.featureFlags = featureFlagsCheck; - if (featureFlagsCheck.status === 'down' && healthStatus.status === 'ok') { - healthStatus.status = 'degraded'; - } - - // Check Bull queue service - const bullCheck = await this.checkBullService(); - healthStatus.services.bull = bullCheck.status; - healthStatus.details.bull = bullCheck; - if (bullCheck.status === 'down' && healthStatus.status === 'ok') { - healthStatus.status = 'degraded'; - } - - return healthStatus; - } - - async checkReadiness(dataSource: DataSource, redis: Redis): Promise { - // For readiness, we check if core dependencies are available - const healthStatus: IHealthStatus = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: Date.now() - this.startTime, - services: { - database: 'unknown', - redis: 'unknown', - externalApis: {}, - disk: 'unknown', - }, - }; - - try { - const dbCheck = await this.checkDatabase(dataSource); - healthStatus.services.database = dbCheck.status; - if (dbCheck.status === 'down') { - healthStatus.status = 'critical'; - } - } catch { - healthStatus.services.database = 'down'; - healthStatus.status = 'critical'; - } - - try { - const redisCheck = await this.checkRedis(redis); - healthStatus.services.redis = redisCheck.status; - if (redisCheck.status === 'down') { - healthStatus.status = 'critical'; - } - } catch { - healthStatus.services.redis = 'down'; - healthStatus.status = 'critical'; - } - - return healthStatus; - } - - private async checkDatabase(dataSource: DataSource): Promise<{ - status: string; - responseTime: number; - connectionCount?: number; - maxConnections?: number; - }> { - const startTime = Date.now(); - try { - await dataSource.query('SELECT 1'); - const responseTime = Date.now() - startTime; - - // Get connection pool stats - let connectionCount = 0; - let maxConnections = 0; - try { - const poolStats = await dataSource.query(` - SELECT count(*) as active_connections - FROM pg_stat_activity - WHERE state = 'active' - `); - connectionCount = parseInt(poolStats[0]?.active_connections || '0'); - maxConnections = parseInt(process.env.DATABASE_POOL_MAX || '30'); - } - catch (poolError) { - this.logger.warn(`Failed to get connection pool stats: ${poolError.message}`); - } - return { - status: responseTime < 1000 ? 'up' : 'degraded', - responseTime, - connectionCount, - maxConnections, - }; - } - catch (error) { - this.logger.error(`Database health check failed: ${error.message}`); - return { - status: 'down', - responseTime: Date.now() - startTime, - }; - } - } - private async checkRedis(redis: Redis): Promise<{ - status: string; - responseTime: number; - memory?: { - used: number; - total: number; - percentage: number; - }; - }> { - const startTime = Date.now(); - try { - const pong = await (redis as unknown).ping(); - const responseTime = Date.now() - startTime; - // Get Redis memory info - let memoryInfo = undefined; - try { - const info = await redis.info('memory'); - const lines = info.split('\r\n'); - const usedMemory = lines.find((line) => line.startsWith('used_memory:'))?.split(':')[1]; - const maxMemory = lines.find((line) => line.startsWith('maxmemory:'))?.split(':')[1]; - if (usedMemory) { - const used = parseInt(usedMemory); - const total = maxMemory ? parseInt(maxMemory) : used * 2; // Estimate if max not set - memoryInfo = { - used, - total, - percentage: Math.round((used / total) * 100), - }; - } - } - catch (memError) { - this.logger.warn(`Failed to get Redis memory info: ${memError.message}`); - } - return { - status: pong === 'PONG' && responseTime < 500 ? 'up' : 'degraded', - responseTime, - memory: memoryInfo, - }; - } - catch (error) { - this.logger.error(`Redis health check failed: ${error.message}`); - return { - status: 'down', - responseTime: Date.now() - startTime, - }; - } - } - private async checkExternalApis(): Promise> { - const results: Record = {}; - for (const endpoint of this.externalApiEndpoints) { - // Skip if API key is not configured - if (!process.env[endpoint.key]) { - results[endpoint.name] = { - status: 'not_configured', - responseTime: 0, - }; - continue; - } - // Skip if URL is not configured - if (!endpoint.url) { - results[endpoint.name] = { - status: 'not_configured', - responseTime: 0, - }; - continue; - } - const startTime = Date.now(); - try { - const response = await axios.get(endpoint.url, { - timeout: 5000, - validateStatus: (status) => status < 500, - }); - const responseTime = Date.now() - startTime; - results[endpoint.name] = { - status: response.status < 400 ? 'up' : 'down', - responseTime, - }; - } - catch (error) { - const responseTime = Date.now() - startTime; - this.logger.warn(`External API health check failed for ${endpoint.name}: ${error.message}`); - results[endpoint.name] = { - status: 'down', - responseTime, - error: error.message, - }; - } - } - return results; - } - private async checkDiskSpace(): Promise<{ - status: string; - used: number; - total: number; - percentage: number; - }> { - try { - // Get disk space using Node.js fs module - const stats = await fs.promises.statfs(process.cwd()); - const total = stats.bsize * stats.blocks; - const free = stats.bsize * stats.bfree; - const used = total - free; - const percentage = Math.round((used / total) * 100); - let status = 'up'; - if (percentage >= this.diskCriticalThreshold) { - status = 'critical'; - } - else if (percentage >= this.diskWarningThreshold) { - status = 'warning'; - } - return { - status, - used, - total, - percentage, - }; - } - catch (error) { - this.logger.error(`Disk space check failed: ${error.message}`); - return { - status: 'unknown', - used: 0, - total: 0, - percentage: 0, - }; - } - } - private async checkQueueService(): Promise<{ - status: string; - activeJobs: number; - waitingJobs: number; - failedJobs: number; - responseTime: number; - }> { - const startTime = Date.now(); - try { - // Simulate queue service check - in real implementation, this would query actual queue service - const responseTime = Date.now() - startTime; - return { - status: responseTime < 1000 ? 'up' : 'degraded', - activeJobs: Math.floor(Math.random() * 10), - waitingJobs: Math.floor(Math.random() * 50), - failedJobs: Math.floor(Math.random() * 5), - responseTime, - }; - } - catch (error) { - this.logger.error(`Queue service health check failed: ${error.message}`); - return { - status: 'down', - activeJobs: 0, - waitingJobs: 0, - failedJobs: 0, - responseTime: Date.now() - startTime, - }; - } - } - private async checkCacheService(redis: Redis): Promise<{ - status: string; - hitRate: number; - missRate: number; - responseTime: number; - }> { - const startTime = Date.now(); - try { - // Test cache with a simple get/set operation - const testKey = 'health_check_test'; - await redis.set(testKey, 'test', 'EX', 10); - const value = await redis.get(testKey); - const responseTime = Date.now() - startTime; - // Get cache stats - let hitRate = 0; - let missRate = 0; - try { - const info = await redis.info('stats'); - const lines = info.split('\r\n'); - const hits = lines.find((line) => line.startsWith('keyspace_hits:'))?.split(':')[1]; - const misses = lines.find((line) => line.startsWith('keyspace_misses:'))?.split(':')[1]; - if (hits && misses) { - const totalHits = parseInt(hits); - const totalMisses = parseInt(misses); - const total = totalHits + totalMisses; - if (total > 0) { - hitRate = Math.round((totalHits / total) * 100); - missRate = Math.round((totalMisses / total) * 100); - } - } - } - catch (statsError) { - this.logger.warn(`Failed to get cache stats: ${statsError.message}`); - } - return { - status: value === 'test' && responseTime < 500 ? 'up' : 'degraded', - hitRate, - missRate, - responseTime, - }; - } - catch (error) { - this.logger.error(`Cache service health check failed: ${error.message}`); - return { - status: 'down', - hitRate: 0, - missRate: 0, - responseTime: Date.now() - startTime, - }; - } - } - private async checkFeatureFlagsService(): Promise<{ - status: string; - responseTime: number; - }> { - const startTime = Date.now(); - try { - // Simulate feature flags service check - // In real implementation, this would check the actual feature flags service - const responseTime = Date.now() - startTime; - return { - status: responseTime < 500 ? 'up' : 'degraded', - responseTime, - }; - } - catch (error) { - this.logger.error(`Feature flags service health check failed: ${error.message}`); - return { - status: 'down', - responseTime: Date.now() - startTime, - }; - } - } - private async checkBullService(): Promise<{ - status: string; - activeQueues: number; - totalJobs: number; - responseTime: number; - }> { - const startTime = Date.now(); - try { - // Simulate Bull queue service check - // In real implementation, this would check actual Bull queues - const responseTime = Date.now() - startTime; - return { - status: responseTime < 1000 ? 'up' : 'degraded', - activeQueues: Math.floor(Math.random() * 5) + 1, - totalJobs: Math.floor(Math.random() * 1000), - responseTime, - }; - } - catch (error) { - this.logger.error(`Bull service health check failed: ${error.message}`); - return { - status: 'down', - activeQueues: 0, - totalJobs: 0, - responseTime: Date.now() - startTime, - }; - } - } -} diff --git a/src/learning-paths/learning-paths.controller.ts b/src/learning-paths/learning-paths.controller.ts deleted file mode 100644 index d34b7a5d..00000000 --- a/src/learning-paths/learning-paths.controller.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Controller, Post, Body } from '@nestjs/common'; -import { LearningPathsService } from './learning-paths.service'; - -/** - * Exposes learning Paths endpoints. - */ -@Controller('learning-paths') -export class LearningPathsController { - constructor(private readonly learningPathsService: LearningPathsService) {} - - /** - * Generates learning Path. - * @param payload The payload to process. - * @returns The operation result. - */ - @Post('generate') - generateLearningPath(@Body() payload: any) { - return this.learningPathsService.generateLearningPath(payload); - } -} diff --git a/src/learning-paths/learning-paths.module.ts b/src/learning-paths/learning-paths.module.ts deleted file mode 100644 index aa4c4311..00000000 --- a/src/learning-paths/learning-paths.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Module } from '@nestjs/common'; -import { LearningPathsController } from './learning-paths.controller'; -import { LearningPathsService } from './learning-paths.service'; -import { SkillAssessmentService } from './services/skill-assessment.service'; -import { PathGenerationService } from './services/path-generation.service'; -import { MilestoneTrackingService } from './services/milestone-tracking.service'; - -/** - * Registers the learning Paths module. - */ -@Module({ - controllers: [LearningPathsController], - providers: [ - LearningPathsService, - SkillAssessmentService, - PathGenerationService, - MilestoneTrackingService, - ], - exports: [LearningPathsService], -}) -export class LearningPathsModule { -} diff --git a/src/learning-paths/learning-paths.service.ts b/src/learning-paths/learning-paths.service.ts deleted file mode 100644 index b1d2e4cc..00000000 --- a/src/learning-paths/learning-paths.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { SkillAssessmentService } from './services/skill-assessment.service'; -import { PathGenerationService } from './services/path-generation.service'; -import { MilestoneTrackingService } from './services/milestone-tracking.service'; - -/** - * Provides learning Paths operations. - */ -@Injectable() -export class LearningPathsService { - constructor( - private readonly skillAssessmentService: SkillAssessmentService, - private readonly pathGenerationService: PathGenerationService, - private readonly milestoneTrackingService: MilestoneTrackingService, - ) {} - - /** - * Generates learning Path. - * @param input The input. - * @returns The operation result. - */ - generateLearningPath(input: any) { - const assessment = this.skillAssessmentService.assess(input); - const path = this.pathGenerationService.generate(assessment); - return this.milestoneTrackingService.initialize(path); - } -} diff --git a/src/learning-paths/services/path-generation.service.ts b/src/learning-paths/services/path-generation.service.ts deleted file mode 100644 index 1f8fd071..00000000 --- a/src/learning-paths/services/path-generation.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -/** - * Provides path Generation operations. - */ -@Injectable() -export class PathGenerationService { - /** - * Generates generate. - * @param assessment The assessment. - * @returns The operation result. - */ - generate(assessment: { level: string; goal: string }) { - const milestones = []; - - if (assessment.level === 'beginner') { - milestones.push('Fundamentals'); - } -} diff --git a/src/localization/language-detection.service.spec.ts b/src/localization/language-detection.service.spec.ts deleted file mode 100644 index 3c91fe06..00000000 --- a/src/localization/language-detection.service.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigService } from '@nestjs/config'; -import { LanguageDetectionService } from './language-detection.service'; -function mockReq(headers: Record = {}, query?: Record) { - return { - headers, - query: query ?? {}, - } as import('express').Request; -} -describe('LanguageDetectionService', () => { - let service: LanguageDetectionService; - async function createModule(config: Record) { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - LanguageDetectionService, - { - provide: ConfigService, - useValue: { - get: (key: string) => config[key], - }, - }, - ], - }).compile(); - return module.get(LanguageDetectionService); - } - beforeEach(async () => { - service = await createModule({ - I18N_DEFAULT_LOCALE: 'en', - I18N_SUPPORTED_LOCALES: 'en,fr,de', - }); - }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); - describe('pickSupported', () => { - it('returns exact tag when listed', () => { - expect(service.pickSupported('fr')).toBe('fr'); - expect(service.pickSupported('FR')).toBe('fr'); - }); - it('maps region subtag to primary when primary is supported', () => { - expect(service.pickSupported('fr-CA')).toBe('fr'); - }); - it('returns null when nothing matches', async () => { - const s = await createModule({ I18N_DEFAULT_LOCALE: 'en', I18N_SUPPORTED_LOCALES: 'en' }); - expect(s.pickSupported('zz')).toBeNull(); - }); - }); - describe('resolveWithSource', () => { - it('uses query lang when supported', () => { - const req = mockReq({ 'accept-language': 'en-US,en;q=0.9' }); - expect(service.resolveWithSource(req, 'de')).toEqual({ locale: 'de', source: 'query' }); - }); - it('ignores unsupported query lang and falls through to header', () => { - const req = mockReq({ 'accept-language': 'fr-FR,fr;q=0.9,en;q=0.8' }); - expect(service.resolveWithSource(req, 'xx')).toEqual({ locale: 'fr', source: 'header' }); - }); - it('respects q-values on Accept-Language', () => { - const req = mockReq({ 'accept-language': 'de;q=0.8,fr;q=0.9' }); - expect(service.resolveWithSource(req)).toEqual({ locale: 'fr', source: 'header' }); - }); - it('uses default when header missing or unmatched', async () => { - const s = await createModule({ I18N_DEFAULT_LOCALE: 'en', I18N_SUPPORTED_LOCALES: 'en' }); - expect(s.resolveWithSource(mockReq({}))).toEqual({ locale: 'en', source: 'default' }); - expect(s.resolveWithSource(mockReq({ 'accept-language': 'ja,zh-CN;q=0.9' }))).toEqual({ - locale: 'en', - source: 'default', - }); - }); - }); -}); diff --git a/src/localization/language-detection.service.ts b/src/localization/language-detection.service.ts deleted file mode 100644 index ac60533e..00000000 --- a/src/localization/language-detection.service.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { Request } from 'express'; -export type LocaleResolutionSource = 'query' | 'header' | 'default'; - -export interface IResolvedLocale { - locale: string; - source: LocaleResolutionSource; -} - -/** - * Provides language Detection operations. - */ -@Injectable() -export class LanguageDetectionService { - constructor(private readonly configService: ConfigService) {} - - /** - * Retrieves default Locale. - * @returns The resulting string value. - */ - getDefaultLocale(): string { - return this.configService.get('I18N_DEFAULT_LOCALE')?.trim().toLowerCase() || 'en'; - } - - /** - * Retrieves supported Locales. - * @returns The matching results. - */ - getSupportedLocales(): string[] { - const raw = - this.configService.get('I18N_SUPPORTED_LOCALES') || - this.configService.get('I18N_DEFAULT_LOCALE') || - 'en'; - return raw - .split(',') - .map((s) => this.normalizeLocaleTag(s)) - .filter(Boolean); - } - - /** - * Executes normalize Locale Tag. - * @param tag The tag. - * @returns The resulting string value. - */ - normalizeLocaleTag(tag: string): string { - if (!tag) return ''; - return tag.trim().toLowerCase().replace(/_/g, '-'); - } - - /** First supported locale that matches the tag (exact or primary subtag). */ - pickSupported(tag: string): string | null { - const normalized = this.normalizeLocaleTag(tag); - if (!normalized) return null; - const supported = this.getSupportedLocales(); - if (supported.includes(normalized)) return normalized; - const primary = normalized.split('-')[0]; - if (supported.includes(primary)) return primary; - for (const s of supported) { - if (s.startsWith(`${primary}-`) || primary === s.split('-')[0]) { - return s; - } - } - return null; - } - - resolveWithSource(req: Request, queryLang?: string): IResolvedLocale { - const defaultLocale = this.getDefaultLocale(); - - if (queryLang) { - const picked = this.pickSupported(queryLang); - if (picked) { - return { locale: picked, source: 'query' }; - } - } - normalizeLocaleTag(tag: string): string { - if (!tag) - return ''; - return tag.trim().toLowerCase().replace(/_/g, '-'); - } - - return { locale: defaultLocale, source: 'default' }; - } - - /** - * Resolves locale. - * @param req The req. - * @param queryLang The query value. - * @returns The resulting string value. - */ - resolveLocale(req: Request, queryLang?: string): string { - return this.resolveWithSource(req, queryLang).locale; - } - - /** - * RFC 7231-style Accept-Language: pick highest-q language that we support. - */ - private parseAcceptLanguage(header: string): string | null { - const parts = header.split(','); - const candidates: Array<{ tag: string; q: number }> = []; - for (const part of parts) { - const [langPart, ...params] = part.trim().split(';'); - const tag = this.normalizeLocaleTag(langPart); - if (!tag || tag === '*') continue; - let q = 1; - for (const p of params) { - const [k, v] = p.trim().split('='); - if (k?.toLowerCase() === 'q' && v) { - const n = parseFloat(v); - if (!Number.isNaN(n)) q = n; - } - return null; - } - resolveWithSource(req: Request, queryLang?: string): ResolvedLocale { - const defaultLocale = this.getDefaultLocale(); - if (queryLang) { - const picked = this.pickSupported(queryLang); - if (picked) { - return { locale: picked, source: 'query' }; - } - } - const header = req.headers['accept-language']; - if (header && typeof header === 'string') { - const fromHeader = this.parseAcceptLanguage(header); - if (fromHeader) { - return { locale: fromHeader, source: 'header' }; - } - } - return { locale: defaultLocale, source: 'default' }; - } - resolveLocale(req: Request, queryLang?: string): string { - return this.resolveWithSource(req, queryLang).locale; - } - /** - * RFC 7231-style Accept-Language: pick highest-q language that we support. - */ - private parseAcceptLanguage(header: string): string | null { - const parts = header.split(','); - const candidates: Array<{ - tag: string; - q: number; - }> = []; - for (const part of parts) { - const [langPart, ...params] = part.trim().split(';'); - const tag = this.normalizeLocaleTag(langPart); - if (!tag || tag === '*') - continue; - let q = 1; - for (const p of params) { - const [k, v] = p.trim().split('='); - if (k?.toLowerCase() === 'q' && v) { - const n = parseFloat(v); - if (!Number.isNaN(n)) - q = n; - } - } - candidates.push({ tag, q }); - } - candidates.sort((a, b) => b.q - a.q); - for (const { tag } of candidates) { - const picked = this.pickSupported(tag); - if (picked) - return picked; - } - return null; - } -} diff --git a/src/localization/language.middleware.ts b/src/localization/language.middleware.ts deleted file mode 100644 index cde40850..00000000 --- a/src/localization/language.middleware.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable, NestMiddleware } from '@nestjs/common'; -import { NextFunction, Response } from 'express'; -import { RequestWithLocale } from '../common/types/request-with-locale'; -import { LanguageDetectionService } from './language-detection.service'; - -/** - * Applies language middleware behavior. - */ -@Injectable() -export class LanguageMiddleware implements NestMiddleware { - constructor(private readonly languageDetection: LanguageDetectionService) {} - - /** - * Executes use. - * @param req The req. - * @param _res The res. - * @param next The next. - */ - use(req: RequestWithLocale, _res: Response, next: NextFunction): void { - const raw = req.query?.lang; - const queryLang = - typeof raw === 'string' - ? raw - : Array.isArray(raw) && typeof raw[0] === 'string' - ? raw[0] - : undefined; - req.resolvedLocale = this.languageDetection.resolveLocale(req, queryLang); - next(); - } -} diff --git a/src/localization/localization.controller.ts b/src/localization/localization.controller.ts deleted file mode 100644 index 21e2dbe1..00000000 --- a/src/localization/localization.controller.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { BadRequestException, Body, Controller, Delete, Get, Param, Patch, Post, Query, Req, Res, UseGuards, } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiProduces, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { Response } from 'express'; -import { RequestWithLocale } from '../common/types/request-with-locale'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { RolesGuard } from '../auth/guards/roles.guard'; -import { Roles } from '../auth/decorators/roles.decorator'; -import { UserRole } from '../users/entities/user.entity'; -import { BundleQueryDto } from './dto/bundle-query.dto'; -import { CreateTranslationDto } from './dto/create-translation.dto'; -import { ExportQueryDto } from './dto/export-query.dto'; -import { ImportTranslationsDto } from './dto/import-translations.dto'; -import { ListTranslationsQueryDto } from './dto/list-translations-query.dto'; -import { UpdateTranslationDto } from './dto/update-translation.dto'; -import { LanguageDetectionService } from './language-detection.service'; -import { LocalizationService } from './localization.service'; - -/** - * Exposes localization endpoints. - */ -@ApiTags('localization') -@Controller('localization') -export class LocalizationController { - constructor( - private readonly localizationService: LocalizationService, - private readonly languageDetection: LanguageDetectionService, - ) {} - - /** - * Returns bundle. - * @param query The query value. - * @param req The req. - * @returns The operation result. - */ - @Get('bundle') - @ApiOperation({ summary: 'Get merged translation bundle for a namespace' }) - async getBundle(@Query() query: BundleQueryDto, @Req() req: RequestWithLocale) { - const locale = - query.locale?.trim() || req.resolvedLocale || this.languageDetection.getDefaultLocale(); - return this.localizationService.getBundleForApi(query.namespace, locale); - } - - /** - * Executes detect. - * @param req The req. - * @param lang The lang. - * @returns The operation result. - */ - @Get('detect') - @ApiOperation({ summary: 'Show how the request locale was resolved' }) - @ApiQuery({ name: 'lang', required: false }) - detect(@Req() req: RequestWithLocale, @Query('lang') lang?: string) { - return this.languageDetection.resolveWithSource(req, lang); - } - - /** - * Returns admin. - * @param query The query value. - * @returns The operation result. - */ - @Get('admin/translations') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'List translations (admin)' }) - listAdmin(@Query() query: ListTranslationsQueryDto) { - return this.localizationService.findAll(query); - } - - /** - * Returns one. - * @param id The identifier. - * @returns The operation result. - */ - @Get('admin/translations/:id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get one translation (admin)' }) - findOne(@Param('id') id: string) { - return this.localizationService.findOne(id); - } - - /** - * Creates a new record. - * @param dto The dto. - * @returns The operation result. - */ - @Post('admin/translations') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Create translation (admin)' }) - create(@Body() dto: CreateTranslationDto) { - return this.localizationService.create(dto); - } - - /** - * Updates the requested record. - * @param id The identifier. - * @param dto The dto. - * @returns The operation result. - */ - @Patch('admin/translations/:id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Update translation (admin)' }) - update(@Param('id') id: string, @Body() dto: UpdateTranslationDto) { - return this.localizationService.update(id, dto); - } - - /** - * Removes the requested record. - * @param id The identifier. - * @returns The operation result. - */ - @Delete('admin/translations/:id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Delete translation (admin)' }) - async remove(@Param('id') id: string) { - await this.localizationService.remove(id); - return { deleted: true }; - } - - /** - * Imports import. - * @param body The body. - * @returns The operation result. - */ - @Post('admin/import') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Upsert translations from JSON (admin)' }) - import(@Body() body: ImportTranslationsDto) { - const rows = body.translations ?? body.rows; - if (!rows?.length) { - throw new BadRequestException('Provide translations or rows array with at least one item'); - } - return this.localizationService.importRows(rows); - } - - /** - * Exports export. - * @param query The query value. - * @param res The res. - * @returns The operation result. - */ - @Get('admin/export') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Export translations as JSON or CSV (admin)' }) - @ApiProduces('application/json', 'text/csv') - async export(@Query() query: ExportQueryDto, @Res({ passthrough: true }) res: Response) { - const rows = await this.localizationService.exportRows(query.namespace, query.locale); - const format = query.format ?? 'json'; - if (format === 'csv') { - const csv = LocalizationService.toCsv(rows); - res.setHeader('Content-Type', 'text/csv; charset=utf-8'); - res.setHeader( - 'Content-Disposition', - `attachment; filename="translations-${query.namespace}.csv"`, - ); - return csv; - } -} diff --git a/src/localization/localization.module.ts b/src/localization/localization.module.ts deleted file mode 100644 index 8c16511d..00000000 --- a/src/localization/localization.module.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { RolesGuard } from '../auth/guards/roles.guard'; -import { Translation } from './entities/translation.entity'; -import { LanguageDetectionService } from './language-detection.service'; -import { LanguageMiddleware } from './language.middleware'; -import { LocalizationController } from './localization.controller'; -import { LocalizationService } from './localization.service'; - -/** - * Registers the localization module. - */ -@Module({ - imports: [TypeOrmModule.forFeature([Translation])], - controllers: [LocalizationController], - providers: [ - LocalizationService, - LanguageDetectionService, - LanguageMiddleware, - JwtAuthGuard, - RolesGuard, - ], - exports: [LocalizationService, LanguageDetectionService], -}) -export class LocalizationModule implements NestModule { - /** - * Executes configure. - * @param consumer The consumer. - * @returns The operation result. - */ - configure(consumer: MiddlewareConsumer) { - consumer.apply(LanguageMiddleware).forRoutes(LocalizationController); - } -} diff --git a/src/localization/localization.service.spec.ts b/src/localization/localization.service.spec.ts deleted file mode 100644 index 25a2d627..00000000 --- a/src/localization/localization.service.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { ConfigService } from '@nestjs/config'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { LocalizationService } from './localization.service'; -import { LanguageDetectionService } from './language-detection.service'; -import { Translation } from './entities/translation.entity'; -import { bundleCacheKey } from './localization.constants'; -describe('LocalizationService', () => { - let service: LocalizationService; - let cacheManager: { - get: jest.Mock; - set: jest.Mock; - del: jest.Mock; - }; - let translationRepo: { - find: jest.Mock; - findOne: jest.Mock; - save: jest.Mock; - remove: jest.Mock; - create: jest.Mock; - upsert: jest.Mock; - createQueryBuilder: jest.Mock; - }; - beforeEach(async () => { - cacheManager = { - get: jest.fn().mockResolvedValue(undefined), - set: jest.fn().mockResolvedValue(undefined), - del: jest.fn().mockResolvedValue(undefined), - }; - translationRepo = { - find: jest.fn(), - findOne: jest.fn(), - save: jest.fn(), - remove: jest.fn(), - create: jest.fn((x) => ({ ...x, id: 'new-id' })), - upsert: jest.fn().mockResolvedValue(undefined), - createQueryBuilder: jest.fn(), - }; - const module: TestingModule = await Test.createTestingModule({ - providers: [ - LocalizationService, - LanguageDetectionService, - { - provide: ConfigService, - useValue: { - get: (key: string) => { - if (key === 'I18N_DEFAULT_LOCALE') - return 'en'; - if (key === 'I18N_SUPPORTED_LOCALES') - return 'en,fr'; - if (key === 'I18N_CACHE_TTL_SECONDS') - return '300'; - return undefined; - }, - }, - }, - { provide: CACHE_MANAGER, useValue: cacheManager }, - { provide: getRepositoryToken(Translation), useValue: translationRepo }, - ], - }).compile(); - service = module.get(LocalizationService); - }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); - describe('interpolate', () => { - it('replaces {{name}} placeholders', () => { - expect(service.interpolate('Hello {{name}}', { name: 'Ada' })).toBe('Hello Ada'); - }); - it('leaves unknown tokens', () => { - expect(service.interpolate('{{a}}', {})).toBe('{{a}}'); - }); - }); - describe('translate', () => { - beforeEach(() => { - translationRepo.find.mockImplementation((opts: { - where: { - namespace: string; - locale: string; - }; - }) => { - const { namespace, locale } = opts.where; - if (namespace === 'app' && locale === 'fr') { - return Promise.resolve([{ translationKey: 'b', value: 'deux' }]); - } - if (namespace === 'app' && locale === 'en') { - return Promise.resolve([ - { translationKey: 'a', value: 'one' }, - { translationKey: 'b', value: 'two' }, - { translationKey: 'greet', value: 'Hi {{name}}' }, - ]); - } - return Promise.resolve([]); - }); - }); - it('interpolates variables', async () => { - const t = await service.translate('app', 'greet', 'en', { name: 'Bo' }); - expect(t).toBe('Hi Bo'); - }); - it('falls back to default locale for missing key in requested locale', async () => { - const t = await service.translate('app', 'a', 'fr'); - expect(t).toBe('one'); - }); - it('uses primary locale when key exists there', async () => { - const t = await service.translate('app', 'b', 'fr'); - expect(t).toBe('deux'); - }); - it('returns namespace.key when missing everywhere', async () => { - const t = await service.translate('app', 'missing', 'en'); - expect(t).toBe('app.missing'); - }); - }); - describe('invalidateBundles', () => { - it('calls cache del for unique namespace/locale pairs', async () => { - await service.invalidateBundles([ - { namespace: 'n', locale: 'en' }, - { namespace: 'n', locale: 'en' }, - { namespace: 'n', locale: 'fr' }, - ]); - expect(cacheManager.del).toHaveBeenCalledWith(bundleCacheKey('n', 'en')); - expect(cacheManager.del).toHaveBeenCalledWith(bundleCacheKey('n', 'fr')); - expect(cacheManager.del).toHaveBeenCalledTimes(2); - }); - }); - describe('create', () => { - it('invalidates bundle cache after save', async () => { - const saved = { - id: '1', - namespace: 'errors', - translationKey: 'x', - locale: 'en', - value: 'v', - createdAt: new Date(), - updatedAt: new Date(), - }; - translationRepo.save.mockResolvedValue(saved); - await service.create({ - namespace: 'errors', - key: 'x', - locale: 'en', - value: 'v', - }); - expect(cacheManager.del).toHaveBeenCalledWith(bundleCacheKey('errors', 'en')); - }); - }); - describe('toCsv', () => { - it('escapes commas and quotes', () => { - const csv = LocalizationService.toCsv([ - { namespace: 'n', key: 'k', locale: 'en', value: 'say "hi", ok' }, - ]); - expect(csv).toContain('"say ""hi"", ok"'); - }); - }); -}); diff --git a/src/localization/localization.service.ts b/src/localization/localization.service.ts deleted file mode 100644 index b304c939..00000000 --- a/src/localization/localization.service.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { BadRequestException, ConflictException, Inject, Injectable, NotFoundException, } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Cache } from 'cache-manager'; -import { QueryFailedError, Repository } from 'typeorm'; -import { CreateTranslationDto } from './dto/create-translation.dto'; -import { ListTranslationsQueryDto } from './dto/list-translations-query.dto'; -import { TranslationImportRowDto } from './dto/import-translations.dto'; -import { UpdateTranslationDto } from './dto/update-translation.dto'; -import { Translation } from './entities/translation.entity'; -import { bundleCacheKey } from './localization.constants'; -import { LanguageDetectionService } from './language-detection.service'; - -export interface ITranslationListItemDto { - id: string; - namespace: string; - key: string; - locale: string; - value: string; - createdAt: Date; - updatedAt: Date; -} - -export interface IPaginatedTranslations { - items: ITranslationListItemDto[]; - total: number; - page: number; - limit: number; -} - -/** - * Provides localization operations. - */ -@Injectable() -export class LocalizationService { - constructor( - @InjectRepository(Translation) - private readonly translationRepo: Repository, - @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, - private readonly configService: ConfigService, - private readonly languageDetection: LanguageDetectionService, - ) {} - - private getCacheTtlMs(): number { - const sec = parseInt(this.configService.get('I18N_CACHE_TTL_SECONDS') || '300', 10); - return Math.max(0, sec) * 1000; - } - - private getDefaultLocale(): string { - return this.languageDetection.getDefaultLocale(); - } - - private toItem(entity: Translation): ITranslationListItemDto { - return { - id: entity.id, - namespace: entity.namespace, - key: entity.translationKey, - locale: entity.locale, - value: entity.value, - createdAt: entity.createdAt, - updatedAt: entity.updatedAt, - }; - } - - /** - * Invalidates bundles. - * @param pairs The pairs. - */ - async invalidateBundles(pairs: Array<{ namespace: string; locale: string }>): Promise { - const seen = new Set(); - for (const { namespace, locale } of pairs) { - const k = `${namespace}\0${locale}`; - if (seen.has(k)) continue; - seen.add(k); - await this.cacheManager.del(bundleCacheKey(namespace, locale)); - } - private getDefaultLocale(): string { - return this.languageDetection.getDefaultLocale(); - } - return map; - } - - /** - * Retrieves raw Bundle Cached. - * @param namespace The namespace. - * @param locale The locale. - * @returns The resulting record. - */ - async getRawBundleCached(namespace: string, locale: string): Promise> { - const loc = languageDetectionNormalize(locale); - const key = bundleCacheKey(namespace, loc); - const ttl = this.getCacheTtlMs(); - const cached = await this.cacheManager.get>(key); - if (cached) return cached; - const fresh = await this.loadRawBundleFromDb(namespace, loc); - if (ttl > 0) { - await this.cacheManager.set(key, fresh, ttl); - } else { - await this.cacheManager.set(key, fresh); - } - return fresh; - } - - /** - * Retrieves messages Merged. - * @param namespace The namespace. - * @param locale The locale. - * @returns The resulting record. - */ - async getMessagesMerged(namespace: string, locale: string): Promise> { - const loc = this.languageDetection.pickSupported(locale) || this.getDefaultLocale(); - const primary = await this.getRawBundleCached(namespace, loc); - const def = this.getDefaultLocale(); - if (loc === def) { - return { ...primary }; - } - const fallback = await this.getRawBundleCached(namespace, def); - return { ...fallback, ...primary }; - } - - /** - * Executes interpolate. - * @param template The template. - * @param vars The vars. - * @returns The resulting string value. - */ - interpolate(template: string, vars?: Record): string { - if (!vars) return template; - return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => - Object.prototype.hasOwnProperty.call(vars, k) ? String(vars[k]) : `{{${k}}}`, - ); - } - - /** - * Executes translate. - * @param namespace The namespace. - * @param key The key. - * @param locale The locale. - * @param vars The vars. - * @returns The resulting string value. - */ - async translate( - namespace: string, - key: string, - locale?: string, - vars?: Record, - ): Promise { - const loc = locale - ? this.languageDetection.pickSupported(locale) || this.getDefaultLocale() - : this.getDefaultLocale(); - const merged = await this.getMessagesMerged(namespace, loc); - const raw = merged[key]; - const text = raw !== undefined && raw !== null && raw !== '' ? raw : `${namespace}.${key}`; - return this.interpolate(text, vars); - } - - /** - * Retrieves bundle For Api. - * @param namespace The namespace. - * @param locale The locale. - * @returns The operation result. - */ - async getBundleForApi( - namespace: string, - locale: string, - ): Promise<{ locale: string; namespace: string; messages: Record }> { - const loc = this.languageDetection.pickSupported(locale) || this.getDefaultLocale(); - const messages = await this.getMessagesMerged(namespace, loc); - return { namespace, locale: loc, messages }; - } - - async create(dto: CreateTranslationDto): Promise { - const namespace = dto.namespace.trim(); - const key = dto.key.trim(); - const locale = languageDetectionNormalize(dto.locale); - const existing = await this.translationRepo.findOne({ - where: { namespace, translationKey: key, locale }, - withDeleted: true, - }); - - if (existing) { - if (!existing.deletedAt) { - throw new ConflictException( - 'Translation already exists for this namespace, key, and locale', - ); - } - - existing.value = dto.value; - existing.deletedAt = null; - - const restored = await this.translationRepo.save(existing); - await this.invalidateBundles([{ namespace: restored.namespace, locale: restored.locale }]); - return this.toItem(restored); - } - async getRawBundleCached(namespace: string, locale: string): Promise> { - const loc = languageDetectionNormalize(locale); - const key = bundleCacheKey(namespace, loc); - const ttl = this.getCacheTtlMs(); - const cached = await this.cacheManager.get>(key); - if (cached) - return cached; - const fresh = await this.loadRawBundleFromDb(namespace, loc); - if (ttl > 0) { - await this.cacheManager.set(key, fresh, ttl); - } - else { - await this.cacheManager.set(key, fresh); - } - return fresh; - } - } - - async findAll(query: ListTranslationsQueryDto): Promise { - const page = query.page ?? 1; - const limit = query.limit ?? 20; - const qb = this.translationRepo.createQueryBuilder('t'); - if (query.namespace) { - qb.andWhere('t.namespace = :ns', { ns: query.namespace.trim() }); - } - interpolate(template: string, vars?: Record): string { - if (!vars) - return template; - return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => Object.prototype.hasOwnProperty.call(vars, k) ? String(vars[k]) : `{{${k}}}`); - } - async translate(namespace: string, key: string, locale?: string, vars?: Record): Promise { - const loc = locale - ? this.languageDetection.pickSupported(locale) || this.getDefaultLocale() - : this.getDefaultLocale(); - const merged = await this.getMessagesMerged(namespace, loc); - const raw = merged[key]; - const text = raw !== undefined && raw !== null && raw !== '' ? raw : `${namespace}.${key}`; - return this.interpolate(text, vars); - } - qb.orderBy('t.namespace', 'ASC') - .addOrderBy('t.locale', 'ASC') - .addOrderBy('t.translationKey', 'ASC'); - const total = await qb.getCount(); - qb.skip((page - 1) * limit).take(limit); - const rows = await qb.getMany(); - return { - items: rows.map((r) => this.toItem(r)), - total, - page, - limit, - }; - } - - async findOne(id: string): Promise { - const row = await this.translationRepo.findOne({ where: { id } }); - if (!row) throw new NotFoundException('Translation not found'); - return this.toItem(row); - } - - async update(id: string, dto: UpdateTranslationDto): Promise { - const row = await this.translationRepo.findOne({ where: { id } }); - if (!row) throw new NotFoundException('Translation not found'); - const before = { namespace: row.namespace, locale: row.locale }; - if (dto.namespace !== undefined) row.namespace = dto.namespace.trim(); - if (dto.key !== undefined) row.translationKey = dto.key.trim(); - if (dto.locale !== undefined) row.locale = languageDetectionNormalize(dto.locale); - if (dto.value !== undefined) row.value = dto.value; - try { - const saved = await this.translationRepo.save(row); - await this.invalidateBundles([before, { namespace: saved.namespace, locale: saved.locale }]); - return this.toItem(saved); - } catch (e) { - if (isUniqueViolation(e)) { - throw new ConflictException( - 'Translation already exists for this namespace, key, and locale', - ); - } - throw e; - } - } - - /** - * Removes the requested record. - * @param id The identifier. - */ - async remove(id: string): Promise { - const row = await this.translationRepo.findOne({ where: { id } }); - if (!row) throw new NotFoundException('Translation not found'); - await this.translationRepo.softRemove(row); - await this.invalidateBundles([{ namespace: row.namespace, locale: row.locale }]); - } - - /** - * Imports rows. - * @param rows The rows. - * @returns The operation result. - */ - async importRows(rows: TranslationImportRowDto[]): Promise<{ upserted: number }> { - if (!rows?.length) { - throw new BadRequestException('Import payload must contain at least one row'); - } - async findAll(query: ListTranslationsQueryDto): Promise { - const page = query.page ?? 1; - const limit = query.limit ?? 20; - const qb = this.translationRepo.createQueryBuilder('t'); - if (query.namespace) { - qb.andWhere('t.namespace = :ns', { ns: query.namespace.trim() }); - } - if (query.locale) { - qb.andWhere('t.locale = :loc', { loc: languageDetectionNormalize(query.locale) }); - } - if (query.search?.trim()) { - const term = `%${query.search.trim()}%`; - qb.andWhere('(t.translationKey ILIKE :term OR t.value ILIKE :term)', { term }); - } - qb.orderBy('t.namespace', 'ASC') - .addOrderBy('t.locale', 'ASC') - .addOrderBy('t.translationKey', 'ASC'); - const total = await qb.getCount(); - qb.skip((page - 1) * limit).take(limit); - const rows = await qb.getMany(); - return { - items: rows.map((r) => this.toItem(r)), - total, - page, - limit, - }; - } - - const pairs = rows.map((r) => ({ - namespace: r.namespace.trim(), - locale: languageDetectionNormalize(r.locale), - })); - await this.invalidateBundles(pairs); - return { upserted: rows.length }; - } - - /** - * Exports rows. - * @param namespace The namespace. - * @param locale The locale. - * @returns The resulting array<{ namespace: string; key: string; locale: string; value: string }>. - */ - async exportRows( - namespace: string, - locale?: string, - ): Promise> { - const qb = this.translationRepo - .createQueryBuilder('t') - .where('t.namespace = :ns', { ns: namespace.trim() }); - if (locale) { - qb.andWhere('t.locale = :loc', { loc: languageDetectionNormalize(locale) }); - } - qb.orderBy('t.locale', 'ASC').addOrderBy('t.translationKey', 'ASC'); - const rows = await qb.getMany(); - return rows.map((r) => ({ - namespace: r.namespace, - key: r.translationKey, - locale: r.locale, - value: r.value, - })); - } - - /** - * Executes to Csv. - * @param rows The rows. - * @returns The resulting string value. - */ - static toCsv( - rows: Array<{ namespace: string; key: string; locale: string; value: string }>, - ): string { - const header = 'namespace,key,locale,value'; - const lines = rows.map((r) => - [csvEscape(r.namespace), csvEscape(r.key), csvEscape(r.locale), csvEscape(r.value)].join(','), - ); - return [header, ...lines].join('\n'); - } -} -function languageDetectionNormalize(locale: string): string { - return locale.trim().toLowerCase().replace(/_/g, '-'); -} -function csvEscape(field: string): string { - if (/[",\n\r]/.test(field)) { - return `"${field.replace(/"/g, '""')}"`; - } - return field; -} -function isUniqueViolation(err: unknown): boolean { - return (err instanceof QueryFailedError && - (err as { - driverError?: { - code?: string; - }; - }).driverError?.code === '23505'); -} diff --git a/src/main.ts b/src/main.ts index 10f494e1..8c589193 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,349 +1,51 @@ import { NestFactory } from '@nestjs/core'; -import { ValidationPipe, Logger, VersioningType } from '@nestjs/common'; +import { ValidationPipe, Logger } from '@nestjs/common'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import cluster from 'node:cluster'; -import { cpus } from 'node:os'; -import { json, urlencoded, type NextFunction, type Request, type Response } from 'express'; -import session, { type Session, type SessionData } from 'express-session'; -import { RedisStore } from 'connect-redis'; -import Redis from 'ioredis'; import { AppModule } from './app.module'; -import { GlobalExceptionFilter } from './common/interceptors/global-exception.filter'; -import { ResponseTransformInterceptor } from './common/interceptors/response-transform.interceptor'; -import { correlationMiddleware } from './common/utils/correlation.utils'; -import { API_VERSION_HEADER, DEFAULT_API_VERSION, SUPPORTED_API_VERSIONS, } from './common/interceptors/api-version.interceptor'; -import { API_VERSIONING_DOCUMENTATION } from './common/modules/api-versioning.module'; -import { sessionConfig } from './config/cache.config'; -import { SESSION_REDIS_CLIENT } from './session/session.constants'; -import helmet from 'helmet'; -import { corsConfig } from './config/cors.config'; -import { ShutdownStateService } from './common/services/shutdown-state.service'; -import { TIME, BYTES } from './common/constants/time.constants'; -import { AuditLogService } from './audit-log/audit-log.service'; -import { createAuditLoggerMiddleware } from './middleware/audit/audit-logger.middleware'; -type SessionRequest = Request & { - session?: Session & Partial & { userAgent?: string }; -}; - -async function bootstrapWorker(): Promise { +async function bootstrap() { const logger = new Logger('Bootstrap'); - const bootstrapStartTime = Date.now(); - const requestBodyLimit = process.env.REQUEST_BODY_LIMIT || '1mb'; - const fileUploadMaxBytes = parseInt( - process.env.FILE_UPLOAD_MAX_BYTES || `${10 * BYTES.ONE_MB_BYTES}`, - 10, - ); - - // Create the application with dynamic module loading - const app = await NestFactory.create(await AppModule.forRoot(), { rawBody: true }); - const shutdownState = app.get(ShutdownStateService); - - app.enableVersioning({ - type: VersioningType.HEADER, - header: API_VERSION_HEADER, - defaultVersion: DEFAULT_API_VERSION, - }); - - // ─── Security Headers ───────────────────────────────────────────────────── - app.use( - helmet({ - hsts: { - maxAge: TIME.ONE_YEAR_SECONDS, - includeSubDomains: true, - preload: true, - }, - crossOriginEmbedderPolicy: false, - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'"], - scriptSrc: ["'self'"], - imgSrc: ["'self'", 'data:', 'https:'], - }, - }, - }), - ); - - app.use(json({ limit: requestBodyLimit })); - app.use(urlencoded({ extended: true, limit: requestBodyLimit })); - - app.use((req: Request, res: Response, next: NextFunction): void => { - const contentType = req.headers['content-type']; - const contentLengthHeader = req.headers['content-length']; - const isMultipart = - typeof contentType === 'string' && contentType.toLowerCase().includes('multipart/form-data'); - - if (!isMultipart) { - next(); - return; - } - - const contentLengthValue = Array.isArray(contentLengthHeader) - ? contentLengthHeader[0] - : contentLengthHeader; - const contentLength = parseInt(contentLengthValue || '', 10); - - if (!Number.isNaN(contentLength) && contentLength > fileUploadMaxBytes) { - res.status(413).json({ - message: 'File upload too large', - maxBytes: fileUploadMaxBytes, - }); - return; - } - - next(); - }); - - const redisClient = app.get(SESSION_REDIS_CLIENT); - - if (sessionConfig.trustProxy) { - const expressApp = app.getHttpAdapter().getInstance(); - expressApp.set('trust proxy', 1); - } - - app.use(correlationMiddleware); - + try { - const auditLogService = app.get(AuditLogService, { strict: false }); - app.use(createAuditLoggerMiddleware(auditLogService)); - } catch { - logger.warn('AuditLogService not available. Global audit middleware was not registered.'); - } - - app.use( - session({ - store: new RedisStore({ - client: redisClient, - prefix: sessionConfig.prefix, - ttl: sessionConfig.ttlSeconds, - }), - name: sessionConfig.name, - secret: sessionConfig.secret, - resave: false, - saveUninitialized: false, - rolling: true, - cookie: { - maxAge: sessionConfig.cookieMaxAgeMs, - httpOnly: true, - sameSite: 'strict', - secure: true, - }, - }), - ); - - // Session fixation protection: bind session to User-Agent - app.use((req: SessionRequest, res: Response, next: NextFunction): void => { - if (!req.session) { - next(); - return; - } + const app = await NestFactory.create(AppModule); - const userAgent = req.headers['user-agent'] || 'unknown'; - if (!req.session.userAgent) { - req.session.userAgent = userAgent; - } else if (req.session.userAgent !== userAgent) { - req.session.destroy((err: unknown): void => { - if (err) { - logger.error('Error destroying session', err); - } - const userAgent = req.headers['user-agent'] || 'unknown'; - if (!req.session.userAgent) { - req.session.userAgent = userAgent; - } - else if (req.session.userAgent !== userAgent) { - return req.session.destroy((err: unknown) => { - if (err) { - logger.error('Error destroying session', err); - } - res.status(401).json({ message: 'Session invalidation due to fixation protection' }); - }); - } - next(); + // Enable CORS + app.enableCors({ + origin: true, + credentials: true, }); - // ─── Global Exception Filter ────────────────────────────────────────────── - app.useGlobalFilters(new GlobalExceptionFilter()); - // ─── Global Logging Interceptor ─────────────────────────────────────────── - app.useGlobalInterceptors(new LoggingInterceptor()); - // ─── Global Response Transform Interceptor ─────────────────────────────── - app.useGlobalInterceptors(new ResponseTransformInterceptor()); - // ─── Global Timeout Interceptor ───────────────────────────────────────── - // TimeoutInterceptor is now provided globally via APP_INTERCEPTOR in AppModule - // ─── CORS ───────────────────────────────────────────────────────────────── - app.enableCors(corsConfig); - // ─── Validation ────────────────────────────────────────────────────────── - app.useGlobalPipes(new ValidationPipe({ + + // Global validation pipe + app.useGlobalPipes( + new ValidationPipe({ whitelist: true, - transform: true, forbidNonWhitelisted: true, - })); - // ─── Swagger ────────────────────────────────────────────────────────────── + transform: true, + }), + ); + + // Swagger documentation const config = new DocumentBuilder() - .setTitle('TeachLink API') - .setDescription(`The TeachLink API documentation - Unified System. ${API_VERSIONING_DOCUMENTATION}`) - .setVersion('1.0') - .addBearerAuth() - .addTag('gamification', 'Gamification and user rewards') - .addTag('Email Marketing - Campaigns', 'Create and manage email campaigns') - .addTag('Email Marketing - Templates', 'Email template management') - .addTag('Email Marketing - Automation', 'Automation workflows') - .addTag('Email Marketing - Segments', 'Audience segmentation') - .addTag('Email Marketing - A/B Testing', 'A/B testing for campaigns') - .addTag('Email Marketing - Analytics', 'Campaign analytics and reporting') - .build(); + .setTitle('TeachLink API') + .setDescription('TeachLink Backend API Documentation') + .setVersion('1.0') + .addTag('App') + .build(); + const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); + + // Start server const port = process.env.PORT || 3000; - app.enableShutdownHooks(); await app.listen(port); - const startupTime = Date.now() - bootstrapStartTime; - if (sessionConfig.stickySessionsRequired) { - logger.log('Sticky sessions are enabled by policy. Configure LB cookie affinity on teachlink.sid.'); - } - next(); - }); - - // ─── Global Exception Filter ────────────────────────────────────────────── - app.useGlobalFilters(new GlobalExceptionFilter()); - - // ─── Global Response Transform Interceptor ─────────────────────────────── - app.useGlobalInterceptors(new ResponseTransformInterceptor()); - - // ─── Global Timeout Interceptor ───────────────────────────────────────── - // TimeoutInterceptor is now provided globally via APP_INTERCEPTOR in AppModule - - // ─── CORS ───────────────────────────────────────────────────────────────── - app.enableCors(corsConfig); - - // ─── Validation ────────────────────────────────────────────────────────── - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - transform: true, - forbidNonWhitelisted: true, - forbidUnknownValues: true, - stopAtFirstError: true, - validationError: { - target: false, - value: false, - }, - }), - ); - - // ─── Swagger ────────────────────────────────────────────────────────────── - const config = new DocumentBuilder() - .setTitle('TeachLink API') - .setDescription( - `The TeachLink API documentation - Unified System. ${API_VERSIONING_DOCUMENTATION}`, - ) - .setVersion('1.0') - .addBearerAuth() - .addTag('gamification', 'Gamification and user rewards') - .addTag('Email Marketing - Campaigns', 'Create and manage email campaigns') - .addTag('Email Marketing - Templates', 'Email template management') - .addTag('Email Marketing - Automation', 'Automation workflows') - .addTag('Email Marketing - Segments', 'Audience segmentation') - .addTag('Email Marketing - A/B Testing', 'A/B testing for campaigns') - .addTag('Email Marketing - Analytics', 'Campaign analytics and reporting') - .build(); - - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api', app, document); - - const port = process.env.PORT || 3000; - app.enableShutdownHooks(); - await app.listen(port); - - const startupTime = Date.now() - bootstrapStartTime; - - if (sessionConfig.stickySessionsRequired) { - logger.log( - 'Sticky sessions are enabled by policy. Configure LB cookie affinity on teachlink.sid.', - ); + + logger.log(`Server is running on port ${port}`); + logger.log(`Swagger docs available at http://localhost:${port}/api`); + + } catch (error) { + logger.error('Application failed to start:', error); + process.exit(1); } - - logger.log(`🚀 TeachLink API running on http://localhost:${port}`); - logger.log(`📚 Swagger docs available at http://localhost:${port}/api`); - logger.log( - `🧭 API versioning enabled via ${API_VERSION_HEADER}. Supported versions: ${SUPPORTED_API_VERSIONS.join(', ')}; default route version: ${DEFAULT_API_VERSION}.`, - ); - logger.log(`⏱️ Application startup completed in ${startupTime}ms`); - - const shutdownTimeoutMs = parseInt(process.env.SHUTDOWN_TIMEOUT_MS || '30000', 10); - let isShuttingDown = false; - - const shutdown = async (signal: string): Promise => { - if (isShuttingDown) { - return; - } - - isShuttingDown = true; - shutdownState.markShuttingDown(); - logger.log(`Received ${signal}. Starting graceful shutdown...`); - - const forceExitTimer = setTimeout(() => { - logger.error(`Graceful shutdown timed out after ${shutdownTimeoutMs}ms. Forcing exit.`); - process.exit(1); - }, shutdownTimeoutMs); - forceExitTimer.unref(); - - try { - await app.close(); - logger.log('Graceful shutdown completed.'); - process.exit(0); - } catch (error) { - logger.error( - 'Error during graceful shutdown', - error instanceof Error ? error.stack : String(error), - ); - process.exit(1); - } - }; - - process.on('SIGTERM', () => { - void shutdown('SIGTERM'); - }); - process.on('SIGINT', () => { - void shutdown('SIGINT'); - }); -} - -async function bootstrap(): Promise { - const logger = new Logger('Cluster'); - const clusterModeEnabled = (process.env.CLUSTER_MODE || 'false') === 'true'; - - if (clusterModeEnabled && cluster.isPrimary) { - const workerCount = parseInt(process.env.CLUSTER_WORKERS || `${cpus().length}`, 10); - const shutdownTimeoutMs = parseInt(process.env.SHUTDOWN_TIMEOUT_MS || '30000', 10); - let isShuttingDown = false; - const shutdown = async (signal: string): Promise => { - if (isShuttingDown) { - return; - } - isShuttingDown = true; - shutdownState.markShuttingDown(); - logger.log(`Received ${signal}. Starting graceful shutdown...`); - const forceExitTimer = setTimeout(() => { - logger.error(`Graceful shutdown timed out after ${shutdownTimeoutMs}ms. Forcing exit.`); - process.exit(1); - }, shutdownTimeoutMs); - forceExitTimer.unref(); - try { - await app.close(); - logger.log('Graceful shutdown completed.'); - process.exit(0); - } - catch (error) { - logger.error('Error during graceful shutdown', error instanceof Error ? error.stack : String(error)); - process.exit(1); - } - }; - process.on('SIGTERM', () => { - void shutdown('SIGTERM'); - }); - process.on('SIGINT', () => { - void shutdown('SIGINT'); - }); } -void bootstrap(); +bootstrap(); diff --git a/src/media/file-cleanup.task.ts b/src/media/file-cleanup.task.ts deleted file mode 100644 index a62ea9c8..00000000 --- a/src/media/file-cleanup.task.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron, CronExpression } from '@nestjs/schedule'; -import { MediaService } from './media.service'; - -@Injectable() -export class FileCleanupTask { - private readonly logger = new Logger(FileCleanupTask.name); - - constructor(private readonly mediaService: MediaService) {} - - @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) - async handleCleanup() { - this.logger.log('Starting scheduled file cleanup...'); - const deletedCount = await this.mediaService.cleanupExpiredFiles(); - this.logger.log(`Scheduled file cleanup completed. Deleted ${deletedCount} files.`); - } - - @Cron(CronExpression.EVERY_HOUR) - async logStorageUsage() { - const stats = await this.mediaService.getStorageUsage(); - this.logger.log( - `Current Storage Usage: ${stats.fileCount} files, ` + - `${(stats.totalSize / 1024 / 1024).toFixed(2)} MB total size`, - ); - } -} diff --git a/src/media/media.controller.spec.ts b/src/media/media.controller.spec.ts deleted file mode 100644 index 686b0abc..00000000 --- a/src/media/media.controller.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { UnsupportedMediaTypeException } from '@nestjs/common'; -import { MediaController } from './media.controller'; -import { MediaService } from './media.service'; -describe('MediaController', () => { - let controller: MediaController; - let mediaService: { - createFromUpload: jest.Mock; - }; - beforeEach(() => { - mediaService = { - createFromUpload: jest.fn(), - }; - controller = new MediaController(mediaService as unknown as MediaService); - }); - it('rejects uploads blocked by the MIME whitelist', async () => { - await expect(controller.upload(undefined as unknown, { - uploadValidationError: { - message: 'File type "application/x-msdownload" is not allowed', - }, - } as unknown)).rejects.toBeInstanceOf(UnsupportedMediaTypeException); - }); - it('passes validated files to the media service with enforced scanning', async () => { - const file = { - originalname: 'avatar.png', - mimetype: 'image/png', - size: 1024, - buffer: Buffer.from('png'), - }; - const req = { - user: { - id: 'user-1', - tenantId: 'tenant-1', - }, - }; - mediaService.createFromUpload.mockResolvedValue({ content: { contentId: 'content-1' } }); - await expect(controller.upload(file as unknown, req, { - compress: 'false', - generateThumbnails: 'true', - })).resolves.toEqual({ content: { contentId: 'content-1' } }); - expect(mediaService.createFromUpload).toHaveBeenCalledWith('user-1', 'tenant-1', file, { - compress: false, - generateThumbnails: true, - trackProgress: true, - }); - }); -}); diff --git a/src/media/media.controller.ts b/src/media/media.controller.ts deleted file mode 100644 index 3e6ff20b..00000000 --- a/src/media/media.controller.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { - Controller, - Post, - UseInterceptors, - UseGuards, - Get, - Param, - Req, - HttpException, - HttpStatus, - Logger, - Body, - UnsupportedMediaTypeException, - Delete, -} from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { Throttle } from '@nestjs/throttler'; -import { THROTTLE } from '../common/constants/throttle.constants'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiConsumes, - ApiBody, - ApiParam, - ApiBearerAuth, -} from '@nestjs/swagger'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { MediaService } from './media.service'; -import { - buildUploadValidationDetails, - MEDIA_UPLOAD_INTERCEPTOR_OPTIONS, -} from './validation/upload-validation.util'; -import { BulkDeleteMediaDto } from './dto/media.dto'; - -/** - * Exposes media endpoints. - */ -@ApiTags('Media') -@ApiBearerAuth() -@Controller('media') -export class MediaController { - private readonly logger = new Logger(MediaController.name); - - constructor(private readonly mediaService: MediaService) {} - - /** - * Uploads upload. - * @param file The file to process. - * @param req The req. - * @param body The body. - * @returns The operation result. - */ - @Post('upload') - @Throttle({ default: THROTTLE.MODERATE }) - @UseGuards(JwtAuthGuard) - @UseInterceptors(FileInterceptor('file', MEDIA_UPLOAD_INTERCEPTOR_OPTIONS)) - @ApiOperation({ summary: 'Upload media file with full validation' }) - @ApiConsumes('multipart/form-data') - @ApiBody({ - description: 'Media file upload', - schema: { - type: 'object', - properties: { - file: { - type: 'string', - format: 'binary', - description: 'File to upload', - }, - compress: { - type: 'boolean', - description: 'Compress images automatically', - default: true, - }, - generateThumbnails: { - type: 'boolean', - description: 'Generate thumbnails for images', - default: true, - }, - }, - }, - }) - @ApiResponse({ status: 201, description: 'File uploaded successfully' }) - @ApiResponse({ status: 400, description: 'Validation failed' }) - @ApiResponse({ status: 403, description: 'Malware detected' }) - @ApiResponse({ status: 413, description: 'File too large' }) - @ApiResponse({ status: 415, description: 'Unsupported file type' }) - @ApiResponse({ status: 503, description: 'Malware scanning unavailable' }) - async upload( - @Req() req: any, - @Body() body?: { compress?: string; generateThumbnails?: string }, - ) { - if (req.uploadValidationError) { - throw new UnsupportedMediaTypeException({ - message: req.uploadValidationError.message, - ...buildUploadValidationDetails(), - }); - } - @Get('uploads/progress/:uploadId') - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: 'Get upload progress by ID' }) - @ApiParam({ name: 'uploadId', description: 'Upload tracking ID' }) - @ApiResponse({ status: 200, description: 'Upload progress', type: Object }) - @ApiResponse({ status: 404, description: 'Upload not found' }) - async getUploadProgress( - @Param('uploadId') - uploadId: string) { - const progress = await this.mediaService.getUploadProgress(uploadId); - if (!progress) { - throw new HttpException('Upload not found', HttpStatus.NOT_FOUND); - } - return progress; - } - - const user = req.user; - this.logger.log(`User ${user?.id} uploading file ${file.originalname}`); - - const options = { - compress: body?.compress !== 'false', - generateThumbnails: body?.generateThumbnails !== 'false', - trackProgress: true, - }; - - const result = await this.mediaService.createFromUpload( - user?.id, - user?.tenantId, - file, - options, - ); - - return result; - } - - /** - * Returns upload Progress. - * @param uploadId The upload identifier. - * @returns The operation result. - */ - @Get('uploads/progress/:uploadId') - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: 'Get upload progress by ID' }) - @ApiParam({ name: 'uploadId', description: 'Upload tracking ID' }) - @ApiResponse({ status: 200, description: 'Upload progress', type: Object }) - @ApiResponse({ status: 404, description: 'Upload not found' }) - async getUploadProgress(@Param('uploadId') uploadId: string) { - const progress = await this.mediaService.getUploadProgress(uploadId); - if (!progress) { - throw new HttpException('Upload not found', HttpStatus.NOT_FOUND); - } - return progress; - } - - /** - * Returns active Uploads. - * @returns The operation result. - */ - @Get('uploads/active') - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: 'List active uploads' }) - @ApiResponse({ status: 200, description: 'List of active uploads' }) - async listActiveUploads() { - return this.mediaService.listActiveUploads(); - } - - /** - * Returns upload Statistics. - * @returns The operation result. - */ - @Get('uploads/statistics') - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: 'Get upload statistics' }) - @ApiResponse({ status: 200, description: 'Upload statistics' }) - async getUploadStatistics() { - return this.mediaService.getUploadStatistics(); - } - - /** - * Returns metadata. - * @param contentId The content identifier. - * @param req The req. - * @returns The operation result. - */ - @Get(':contentId') - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: 'Get media metadata by content ID' }) - @ApiParam({ name: 'contentId', description: 'Content identifier' }) - @ApiResponse({ status: 200, description: 'Media metadata' }) - @ApiResponse({ status: 404, description: 'Not found' }) - @ApiResponse({ status: 403, description: 'Forbidden' }) - async getMetadata(@Param('contentId') contentId: string, @Req() req: any) { - const user = req.user; - const meta = await this.mediaService.findByContentId(contentId); - if (!meta) throw new HttpException('Not found', HttpStatus.NOT_FOUND); - - // Access control: owner or same tenant or admin - if ( - meta.ownerId && - meta.ownerId !== user?.id && - user?.role !== 'admin' && - meta.tenantId !== user?.tenantId - ) { - throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); - } - - return meta; - } - - @Delete(':contentId') - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: 'Delete media by content ID' }) - @ApiParam({ name: 'contentId', description: 'Content identifier' }) - async deleteMedia(@Param('contentId') contentId: string, @Req() req: any) { - const user = req.user; - const meta = await this.mediaService.findByContentId(contentId); - if (!meta) throw new HttpException('Not found', HttpStatus.NOT_FOUND); - - if (meta.ownerId !== user?.id && user?.role !== 'admin') { - throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); - } - - await this.mediaService.deleteMedia(contentId); - return { success: true }; - } - - @Post('bulk-delete') - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: 'Delete multiple media files' }) - async bulkDelete(@Body() bulkDto: BulkDeleteMediaDto) { - // For bulk delete, we'll let the service handle it but we should ideally validate ownership here too. - // However, to keep it simple and efficient, the service will attempt deletion and we'll return results. - // In a real app, we might want to filter the IDs first. - - // Simple filter: only allow admins to bulk delete everything, or users to bulk delete their own (needs more complex query) - return this.mediaService.bulkDeleteMedia(bulkDto.contentIds); - } -} diff --git a/src/media/media.module.ts b/src/media/media.module.ts deleted file mode 100644 index 463e86e6..00000000 --- a/src/media/media.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { BullModule } from '@nestjs/bull'; -import { QUEUE_NAMES } from '../common/constants/queue.constants'; -import { MediaController } from './media.controller'; -import { MediaService } from './media.service'; -import { FileStorageService } from './storage/file-storage.service'; -import { VideoProcessingService } from './processing/video-processing.service'; -import { DocumentProcessingService } from './processing/document-processing.service'; -import { ImageProcessingService } from './processing/image-processing.service'; -import { FileValidationService } from './validation/file-validation.service'; -import { MalwareScanningService } from './validation/malware-scanning.service'; -import { UploadProgressService } from './validation/upload-progress.service'; -import { ContentMetadata } from '../cdn/entities/content-metadata.entity'; -import { VideoProcessor } from './processing/video.processor'; -import { FileCleanupTask } from './file-cleanup.task'; - -/** - * Registers the media module. - */ -@Module({ - imports: [ - TypeOrmModule.forFeature([ContentMetadata]), - BullModule.registerQueue({ name: QUEUE_NAMES.MEDIA_PROCESSING }), - ], - controllers: [MediaController], - providers: [ - MediaService, - FileStorageService, - VideoProcessingService, - DocumentProcessingService, - ImageProcessingService, - FileValidationService, - MalwareScanningService, - UploadProgressService, - // processors - VideoProcessor, - FileCleanupTask, - ], - exports: [ - MediaService, - FileStorageService, - VideoProcessingService, - FileValidationService, - ImageProcessingService, - ], -}) -export class MediaModule { -} diff --git a/src/media/media.service.spec.ts b/src/media/media.service.spec.ts deleted file mode 100644 index 7eeb9f10..00000000 --- a/src/media/media.service.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ForbiddenException, ServiceUnavailableException } from '@nestjs/common'; -import { ContentStatus } from '../cdn/entities/content-metadata.entity'; -import { MediaService } from './media.service'; -describe('MediaService', () => { - // ─── Declarations ────────────────────────────────────────────────────────── - let service: MediaService; - let mockContentRepo: jest.Mocked; - let mockStorage: jest.Mocked; - let mockVideoProcessing: jest.Mocked; - let mockFileValidation: jest.Mocked; - let mockMalwareScanning: jest.Mocked; - let mockImageProcessing: jest.Mocked; - let mockUploadProgress: jest.Mocked; - const file = { - originalname: 'avatar.png', - mimetype: 'image/png', - size: 1024, - buffer: Buffer.from('png'), - }; - // ─── Setup ───────────────────────────────────────────────────────────────── - beforeEach(() => { - // Initialize all dependency mocks with proper typing - mockContentRepo = { - create: jest.fn().mockImplementation((value) => value), - save: jest.fn().mockImplementation(async (value) => ({ - status: ContentStatus.READY, - ...value, - })), - findOne: jest.fn(), - } as jest.Mocked; - mockStorage = { - uploadFile: jest.fn(), - uploadProcessedFile: jest.fn(), - } as jest.Mocked; - mockVideoProcessing = { - enqueueTranscode: jest.fn(), - } as jest.Mocked; - mockFileValidation = { - validateFile: jest.fn().mockResolvedValue({ - valid: true, - errors: [], - warnings: [], - metadata: {}, - }), - } as jest.Mocked; - mockMalwareScanning = { - isScanningAvailable: jest.fn(), - scanFile: jest.fn(), - } as jest.Mocked; - mockImageProcessing = { - compressImage: jest.fn(), - generateThumbnails: jest.fn(), - } as jest.Mocked; - mockUploadProgress = { - initializeUpload: jest.fn(), - updateProgress: jest.fn(), - markFailed: jest.fn(), - markCompleted: jest.fn(), - getProgress: jest.fn(), - listActiveUploads: jest.fn(), - getStatistics: jest.fn(), - } as jest.Mocked; - service = new MediaService(mockContentRepo, mockStorage, mockVideoProcessing, mockFileValidation, mockMalwareScanning, mockImageProcessing, mockUploadProgress); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('fails closed when malware scanning is required but unavailable', async () => { - mockMalwareScanning.isScanningAvailable.mockReturnValue(false); - await expect(service.createFromUpload('user-1', 'tenant-1', file as unknown)).rejects.toBeInstanceOf(ServiceUnavailableException); - expect(mockMalwareScanning.scanFile).not.toHaveBeenCalled(); - expect(mockUploadProgress.markFailed).toHaveBeenCalled(); - }); - it('blocks uploads when malware is detected', async () => { - mockMalwareScanning.isScanningAvailable.mockReturnValue(true); - mockMalwareScanning.scanFile.mockResolvedValue({ - clean: false, - threats: ['EICAR-Test-File'], - scanTime: 42, - }); - await expect(service.createFromUpload('user-1', 'tenant-1', file as unknown)).rejects.toBeInstanceOf(ForbiddenException); - expect(mockUploadProgress.markFailed).toHaveBeenCalledWith(expect.any(String), 'Malware detected: EICAR-Test-File'); - }); -}); diff --git a/src/media/media.service.ts b/src/media/media.service.ts deleted file mode 100644 index 6c7a0ff9..00000000 --- a/src/media/media.service.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { Injectable, Logger, BadRequestException, ForbiddenException, ServiceUnavailableException, } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { ContentMetadata, ContentStatus, ContentType, } from '../cdn/entities/content-metadata.entity'; -import { FileStorageService } from './storage/file-storage.service'; -import { VideoProcessingService } from './processing/video-processing.service'; -import { UploadedFile } from '@nestjs/common'; -import { FileValidationService } from './validation/file-validation.service'; -import { MalwareScanningService } from './validation/malware-scanning.service'; -import { ImageProcessingService } from './processing/image-processing.service'; -import { UploadProgressService } from './validation/upload-progress.service'; -import { v4 as uuidv4 } from 'uuid'; - -export interface IUploadOptions { - compress?: boolean; - generateThumbnails?: boolean; - scanForMalware?: boolean; - trackProgress?: boolean; - expiresIn?: number; // TTL in seconds -} - -export interface IUploadResult { - content: ContentMetadata; - thumbnails?: Array<{ name: string; url: string }>; - compressionRatio?: number; - scanResult?: { clean: boolean; threats: string[] }; -} - -/** - * Provides media operations. - */ -@Injectable() -export class MediaService { - private readonly logger = new Logger(MediaService.name); - constructor( - @InjectRepository(ContentMetadata) - private readonly contentRepo: Repository, - private readonly storage: FileStorageService, - private readonly videoProcessing: VideoProcessingService, - private readonly fileValidation: FileValidationService, - private readonly malwareScanning: MalwareScanningService, - private readonly imageProcessing: ImageProcessingService, - private readonly uploadProgress: UploadProgressService, - ) {} - - /** - * Creates from Upload. - * @param ownerId The owner identifier. - * @param tenantId The tenant identifier. - * @param file The file to process. - * @param options The options. - * @returns The resulting upload result. - */ - async createFromUpload( - ownerId: string, - tenantId: string | undefined, - file: UploadedFile, - options: IUploadOptions = {}, - ): Promise { - const uploadId = uuidv4(); - const result: IUploadResult = { content: null as unknown as ContentMetadata }; - - try { - // Initialize progress tracking if enabled - if (options.trackProgress !== false) { - await this.uploadProgress.initializeUpload(uploadId, file.originalname, file.size); - } - - // Step 1: Validate file - if (options.trackProgress !== false) { - await this.uploadProgress.updateProgress(uploadId, { - status: 'validating', - progress: 10, - stage: 'validation', - message: 'Validating file...', - }); - } - - const validationResult = await this.fileValidation.validateFile(file); - if (!validationResult.valid) { - if (options.trackProgress !== false) { - await this.uploadProgress.markFailed(uploadId, validationResult.errors.join(', ')); - } - throw new BadRequestException({ - message: 'File validation failed', - errors: validationResult.errors, - warnings: validationResult.warnings, - }); - } - - // Step 2: Malware scanning - if (options.scanForMalware !== false) { - if (!this.malwareScanning.isScanningAvailable()) { - const errorMsg = - 'Malware scanning is required for uploads but no scanning service is available'; - - if (options.trackProgress !== false) { - await this.uploadProgress.markFailed(uploadId, errorMsg); - } - - throw new ServiceUnavailableException(errorMsg); - } - - if (options.trackProgress !== false) { - await this.uploadProgress.updateProgress(uploadId, { - status: 'scanning', - progress: 25, - stage: 'malware-scan', - message: 'Scanning for malware...', - }); - } - - const scanResult = await this.malwareScanning.scanFile(file); - result.scanResult = { - clean: scanResult.clean, - threats: scanResult.threats, - }; - - if (!scanResult.clean) { - const detectedThreats = scanResult.threats.filter(Boolean); - const errorMsg = - detectedThreats.length > 0 - ? `Malware detected: ${detectedThreats.join(', ')}` - : scanResult.error || 'File failed security scan'; - - if (options.trackProgress !== false) { - await this.uploadProgress.markFailed(uploadId, errorMsg); - } - - if (detectedThreats.length > 0) { - throw new ForbiddenException(errorMsg); - } - - throw new ServiceUnavailableException(errorMsg); - } - } - - // Create metadata entity - const content = this.contentRepo.create({ - contentId: `media_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, - originalUrl: '', - cdnUrl: '', - contentType: this.mapContentType(file.mimetype), - fileName: file.originalname, - mimeType: file.mimetype, - fileSize: file.size, - status: ContentStatus.UPLOADING, - provider: 'media', - metadata: validationResult.metadata || {}, - ownerId, - tenantId, - } as Partial); - - await this.contentRepo.save(content); - result.content = content; - - // Step 3: Process image (compression & thumbnails) - let processedFile = file; - if (content.contentType === ContentType.IMAGE && options.compress !== false) { - if (options.trackProgress !== false) { - await this.uploadProgress.updateProgress(uploadId, { - status: 'processing', - progress: 40, - stage: 'image-processing', - message: 'Optimizing image...', - }); - } - - try { - // Initialize progress tracking if enabled - if (options.trackProgress !== false) { - await this.uploadProgress.initializeUpload(uploadId, file.originalname, file.size); - } - - const thumbnails = await this.imageProcessing.generateThumbnails(file.buffer); - result.thumbnails = []; - - for (const thumb of thumbnails) { - const thumbKey = `${content.contentId}/thumbnails/${thumb.name}.webp`; - await this.storage.uploadProcessedFile(thumb.buffer, thumbKey, 'image/webp'); - result.thumbnails.push({ - name: thumb.name, - url: this.storage.getPublicUrl(thumbKey), - }); - } - const validationResult = await this.fileValidation.validateFile(file); - if (!validationResult.valid) { - if (options.trackProgress !== false) { - await this.uploadProgress.markFailed(uploadId, validationResult.errors.join(', ')); - } - throw new BadRequestException({ - message: 'File validation failed', - errors: validationResult.errors, - warnings: validationResult.warnings, - }); - } - // Step 2: Malware scanning - if (options.scanForMalware !== false) { - if (!this.malwareScanning.isScanningAvailable()) { - const errorMsg = 'Malware scanning is required for uploads but no scanning service is available'; - if (options.trackProgress !== false) { - await this.uploadProgress.markFailed(uploadId, errorMsg); - } - throw new ServiceUnavailableException(errorMsg); - } - if (options.trackProgress !== false) { - await this.uploadProgress.updateProgress(uploadId, { - status: 'scanning', - progress: 25, - stage: 'malware-scan', - message: 'Scanning for malware...', - }); - } - const scanResult = await this.malwareScanning.scanFile(file); - result.scanResult = { - clean: scanResult.clean, - threats: scanResult.threats, - }; - if (!scanResult.clean) { - const detectedThreats = scanResult.threats.filter(Boolean); - const errorMsg = detectedThreats.length > 0 - ? `Malware detected: ${detectedThreats.join(', ')}` - : scanResult.error || 'File failed security scan'; - if (options.trackProgress !== false) { - await this.uploadProgress.markFailed(uploadId, errorMsg); - } - if (detectedThreats.length > 0) { - throw new ForbiddenException(errorMsg); - } - throw new ServiceUnavailableException(errorMsg); - } - } - // Create metadata entity - const content = this.contentRepo.create({ - contentId: `media_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, - originalUrl: '', - cdnUrl: '', - contentType: this.mapContentType(file.mimetype), - fileName: file.originalname, - mimeType: file.mimetype, - fileSize: file.size, - status: ContentStatus.UPLOADING, - provider: 'media', - metadata: validationResult.metadata || {}, - ownerId, - tenantId, - } as Partial); - await this.contentRepo.save(content); - result.content = content; - // Step 3: Process image (compression & thumbnails) - let processedFile = file; - if (content.contentType === ContentType.IMAGE && options.compress !== false) { - if (options.trackProgress !== false) { - await this.uploadProgress.updateProgress(uploadId, { - status: 'processing', - progress: 40, - stage: 'image-processing', - message: 'Optimizing image...', - }); - } - try { - const compressed = await this.imageProcessing.compressImage(file.buffer); - processedFile = { - ...file, - buffer: compressed.buffer, - size: compressed.size, - }; - result.compressionRatio = compressed.compressionRatio; - content.optimizedSize = compressed.size; - // Update metadata with dimensions - if (compressed.width && compressed.height) { - content.metadata = { - ...content.metadata, - width: compressed.width, - height: compressed.height, - }; - } - // Generate thumbnails - if (options.generateThumbnails !== false) { - if (options.trackProgress !== false) { - await this.uploadProgress.updateProgress(uploadId, { - progress: 60, - stage: 'thumbnail-generation', - message: 'Generating thumbnails...', - }); - } - const thumbnails = await this.imageProcessing.generateThumbnails(file.buffer); - result.thumbnails = []; - for (const thumb of thumbnails) { - const thumbKey = `${content.contentId}/thumbnails/${thumb.name}.webp`; - await this.storage.uploadProcessedFile(thumb.buffer, thumbKey, 'image/webp'); - result.thumbnails.push({ - name: thumb.name, - url: `https://${process.env.AWS_S3_BUCKET}.s3.amazonaws.com/${thumbKey}`, - }); - } - // Store thumbnail URLs in variants - content.variants = result.thumbnails.map((t) => ({ - name: t.name, - url: t.url, - width: 0, // Will be populated from thumbnail data - height: 0, - size: 0, - })); - } - } - catch (error) { - this.logger.warn('Image processing failed, using original:', error); - } - } - // Step 4: Upload to storage - if (options.trackProgress !== false) { - await this.uploadProgress.updateProgress(uploadId, { - status: 'uploading', - progress: 80, - stage: 'storage-upload', - message: 'Uploading to storage...', - }); - } - const upload = await this.storage.uploadFile(processedFile, content); - content.originalUrl = upload.url; - content.cdnUrl = upload.url; - content.etag = upload.etag; - content.status = ContentStatus.READY; - await this.contentRepo.save(content); - // Step 5: Video processing (if applicable) - if (content.contentType === ContentType.VIDEO) { - this.logger.log(`Enqueue video processing for ${content.contentId}`); - await this.videoProcessing.enqueueTranscode(content); - content.status = ContentStatus.PROCESSING; - await this.contentRepo.save(content); - } - // Mark as completed - if (options.trackProgress !== false) { - await this.uploadProgress.markCompleted(uploadId, { - contentId: content.contentId, - url: content.cdnUrl, - thumbnails: result.thumbnails?.map((t) => t.url), - }); - } - return result; - } - catch (error) { - this.logger.error('Upload failed:', error); - if (options.trackProgress !== false) { - await this.uploadProgress.markFailed(uploadId, error instanceof Error ? error.message : 'Unknown error'); - } - throw error; - } - } - - // Step 4: Upload to storage - if (options.trackProgress !== false) { - await this.uploadProgress.updateProgress(uploadId, { - status: 'uploading', - progress: 80, - stage: 'storage-upload', - message: 'Uploading to storage...', - }); - } - - const upload = await this.storage.uploadFile(processedFile, content); - - content.originalUrl = upload.url; - content.cdnUrl = upload.url; - content.etag = upload.etag; - content.status = ContentStatus.READY; - - if (options.expiresIn) { - content.expiresAt = new Date(Date.now() + options.expiresIn * 1000); - } - - await this.contentRepo.save(content); - - // Step 5: Video processing (if applicable) - if (content.contentType === ContentType.VIDEO) { - this.logger.log(`Enqueue video processing for ${content.contentId}`); - await this.videoProcessing.enqueueTranscode(content); - content.status = ContentStatus.PROCESSING; - await this.contentRepo.save(content); - } - - // Mark as completed - if (options.trackProgress !== false) { - await this.uploadProgress.markCompleted(uploadId, { - contentId: content.contentId, - url: content.cdnUrl, - thumbnails: result.thumbnails?.map((t) => t.url), - }); - } - - return result; - } catch (error) { - this.logger.error('Upload failed:', error); - - if (options.trackProgress !== false) { - await this.uploadProgress.markFailed( - uploadId, - error instanceof Error ? error.message : 'Unknown error', - ); - } - - throw error; - } - } - - async deleteMedia(contentId: string): Promise { - const content = await this.findByContentId(contentId); - if (!content) { - throw new BadRequestException('Content not found'); - } - - // Delete from storage - if (content.originalUrl) { - const key = this.extractKeyFromUrl(content.originalUrl); - if (key) await this.storage.deleteFile(key); - } - - // Delete thumbnails/variants - if (content.variants && content.variants.length > 0) { - for (const variant of content.variants) { - const key = this.extractKeyFromUrl(variant.url); - if (key) await this.storage.deleteFile(key); - } - } - - // Delete from database - await this.contentRepo.delete({ contentId }); - this.logger.log(`Deleted media and metadata for ${contentId}`); - } - - async cleanupExpiredFiles(): Promise { - const expired = await this.contentRepo - .createQueryBuilder('content') - .where('content.expiresAt IS NOT NULL') - .andWhere('content.expiresAt < :now', { now: new Date() }) - .getMany(); - - if (expired.length === 0) return 0; - - this.logger.log(`Cleaning up ${expired.length} expired files`); - - for (const content of expired) { - try { - await this.deleteMedia(content.contentId); - } catch (error) { - this.logger.error(`Failed to cleanup expired file ${content.contentId}`, error); - } - } - - return expired.length; - } - - async getStorageUsage(): Promise<{ totalSize: number; fileCount: number }> { - const result = await this.contentRepo - .createQueryBuilder('content') - .select('SUM(content.fileSize)', 'totalSize') - .addSelect('COUNT(content.id)', 'fileCount') - .getRawOne(); - - return { - totalSize: parseInt(result.totalSize || '0', 10), - fileCount: parseInt(result.fileCount || '0', 10), - }; - } - - async bulkDeleteMedia(contentIds: string[]): Promise<{ success: string[]; failed: string[] }> { - const results = { success: [], failed: [] }; - - for (const contentId of contentIds) { - try { - await this.deleteMedia(contentId); - results.success.push(contentId); - } catch (error) { - this.logger.error(`Failed to delete media ${contentId} in bulk`, error); - results.failed.push(contentId); - } - } - - return results; - } - - private extractKeyFromUrl(url: string): string | null { - try { - if (!url) return null; - if (!url.startsWith('http')) return url; - - const parsed = new URL(url); - const host = parsed.hostname; - const path = parsed.pathname.replace(/^\/+/, ''); - - if (!path) return null; - - if (host.endsWith('.cloudfront.net')) { - return path; - } - - if (host.includes('.s3.') || host.endsWith('.s3.amazonaws.com')) { - return path; - } - - if (host.startsWith('s3.') || host === 's3.amazonaws.com') { - const [, ...rest] = path.split('/'); - return rest.length > 0 ? rest.join('/') : null; - } - - return null; - } catch { - return null; - } - } - - async findByContentId(contentId: string) { - return this.contentRepo.findOne({ where: { contentId } }); - } - - /** - * Get upload progress - */ - async getUploadProgress(uploadId: string) { - return this.uploadProgress.getProgress(uploadId); - } - - /** - * List active uploads - */ - async listActiveUploads() { - return this.uploadProgress.listActiveUploads(); - } - - /** - * Get upload statistics - */ - async getUploadStatistics() { - return this.uploadProgress.getStatistics(); - } - - private mapContentType(mime: string) { - if (mime.startsWith('image/')) return ContentType.IMAGE; - if (mime.startsWith('video/')) return ContentType.VIDEO; - if (mime.startsWith('audio/')) return ContentType.AUDIO; - return ContentType.DOCUMENT; - } -} diff --git a/src/media/processing/document-processing.service.ts b/src/media/processing/document-processing.service.ts deleted file mode 100644 index 6ae5eded..00000000 --- a/src/media/processing/document-processing.service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import pdfParse from 'pdf-parse'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { ContentMetadata } from '../../cdn/entities/content-metadata.entity'; -import { FileStorageService } from '../storage/file-storage.service'; - -/** - * Provides document Processing operations. - */ -@Injectable() -export class DocumentProcessingService { - private readonly logger = new Logger(DocumentProcessingService.name); - constructor(private readonly storage: FileStorageService, - @InjectRepository(ContentMetadata) - private readonly contentRepo: Repository, - ) {} - - /** - * Executes parse Pdf From Content. - * @param contentId The content identifier. - * @returns The operation result. - */ - async parsePdfFromContent(contentId: string) { - const meta = await this.contentRepo.findOne({ where: { contentId } }); - if (!meta) return null; - - const signed = await this.storage.getSignedUrl(meta.cdnUrl, 60); - const buffer = await downloadToBuffer(signed); - - try { - const parsed = await pdfParse(buffer); - meta.metadata = meta.metadata || {}; - // Extend metadata type to include text for documents - (meta.metadata as any).text = parsed.text; - await this.contentRepo.save(meta); - return parsed.text; - } catch (err) { - this.logger.error('PDF parsing failed', err); - throw err; - } -} -async function downloadToBuffer(url: string): Promise { - const https = url.startsWith('https') ? await import('https') : await import('http'); - return new Promise((resolve, reject) => { - const req = https.get(url, (res: unknown) => { - if (res.statusCode >= 400) - return reject(new Error(`Failed to download: ${res.statusCode}`)); - const data: Buffer[] = []; - res.on('data', (chunk: Buffer) => data.push(chunk)); - res.on('end', () => resolve(Buffer.concat(data))); - }); - req.on('error', (err: Error) => reject(err)); - }); -} diff --git a/src/media/processing/image-processing.service.ts b/src/media/processing/image-processing.service.ts deleted file mode 100644 index bec1eeca..00000000 --- a/src/media/processing/image-processing.service.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import sharp from 'sharp'; -import { - COMPRESSION_CONFIG, - THUMBNAIL_CONFIG, - IMAGE_DIMENSION_LIMITS, -} from '../validation/file-validation.constants'; - -export interface IProcessedImage { - buffer: Buffer; - format: string; - width: number; - height: number; - size: number; - originalSize: number; - compressionRatio: number; -} - -export interface IThumbnailResult { - name: string; - buffer: Buffer; - width: number; - height: number; - size: number; - url?: string; -} -export interface ImageMetadata { - width: number; - height: number; - format: string; - hasAlpha: boolean; - size: number; - density?: number; - space?: string; - channels?: number; -} - -/** - * Provides image Processing operations. - */ -@Injectable() -export class ImageProcessingService { - private readonly logger = new Logger(ImageProcessingService.name); - - /** - * Get image metadata - */ - async getMetadata(buffer: Buffer): Promise { - const metadata = await sharp(buffer).metadata(); - const stats = await sharp(buffer).stats(); - - return { - width: metadata.width || 0, - height: metadata.height || 0, - format: metadata.format || 'unknown', - hasAlpha: metadata.hasAlpha || false, - size: buffer.length, - density: metadata.density, - space: metadata.space, - channels: stats.channels.length, - }; - } - - /** - * Compress and optimize image - */ - async compressImage( - buffer: Buffer, - options?: { - maxWidth?: number; - maxHeight?: number; - quality?: number; - format?: 'jpeg' | 'png' | 'webp' | 'avif'; - preserveAspectRatio?: boolean; - }, - ): Promise { - const originalSize = buffer.length; - const metadata = await sharp(buffer).metadata(); - - let pipeline = sharp(buffer); - - // Determine output format - const outputFormat = options?.format || this.getOptimalFormat(metadata.format); - - // Resize if dimensions exceed limits - const maxWidth = options?.maxWidth || COMPRESSION_CONFIG.MAX_DIMENSION; - const maxHeight = options?.maxHeight || COMPRESSION_CONFIG.MAX_DIMENSION; - - if (metadata.width && metadata.height) { - if (metadata.width > maxWidth || metadata.height > maxHeight) { - pipeline = pipeline.resize(maxWidth, maxHeight, { - fit: 'inside', - withoutEnlargement: true, - }); - } - } - - // Apply format-specific compression - const quality = options?.quality || this.getDefaultQuality(outputFormat); - - switch (outputFormat) { - case 'jpeg': - case 'jpg': - pipeline = pipeline.jpeg({ - quality, - progressive: true, - mozjpeg: true, - }); - break; - - case 'png': - pipeline = pipeline.png({ - compressionLevel: COMPRESSION_CONFIG.PNG_COMPRESSION_LEVEL, - progressive: true, - adaptiveFiltering: true, - }); - break; - - case 'webp': - pipeline = pipeline.webp({ - quality, - effort: 6, - }); - break; - - case 'avif': - pipeline = pipeline.avif({ - quality, - effort: 4, - }); - break; - - default: - // Keep original format with default compression - pipeline = pipeline.jpeg({ quality: COMPRESSION_CONFIG.JPEG_QUALITY }); - } - - // Process image - const processedBuffer = await pipeline.toBuffer(); - const processedMetadata = await sharp(processedBuffer).metadata(); - - const compressionRatio = - originalSize > 0 ? ((originalSize - processedBuffer.length) / originalSize) * 100 : 0; - - this.logger.log( - `Image compressed: ${originalSize} -> ${processedBuffer.length} bytes (${compressionRatio.toFixed(1)}% reduction)`, - ); - - return { - buffer: processedBuffer, - format: outputFormat, - width: processedMetadata.width || 0, - height: processedMetadata.height || 0, - size: processedBuffer.length, - originalSize, - compressionRatio, - }; - } - - /** - * Generate thumbnails in multiple sizes - */ - async generateThumbnails( - buffer: Buffer, - options?: { - sizes?: Array<{ name: string; width: number; height: number }>; - format?: 'jpeg' | 'png' | 'webp'; - quality?: number; - }, - ): Promise { - const sizes = options?.sizes || THUMBNAIL_CONFIG.SIZES; - const format = options?.format || THUMBNAIL_CONFIG.DEFAULT_FORMAT; - const quality = options?.quality || THUMBNAIL_CONFIG.DEFAULT_QUALITY; - - const thumbnails: IThumbnailResult[] = []; - - for (const size of sizes) { - try { - let pipeline = sharp(buffer); - // Determine output format - const outputFormat = options?.format || this.getOptimalFormat(metadata.format); - // Resize if dimensions exceed limits - const maxWidth = options?.maxWidth || COMPRESSION_CONFIG.MAX_DIMENSION; - const maxHeight = options?.maxHeight || COMPRESSION_CONFIG.MAX_DIMENSION; - if (metadata.width && metadata.height) { - if (metadata.width > maxWidth || metadata.height > maxHeight) { - pipeline = pipeline.resize(maxWidth, maxHeight, { - fit: 'inside', - withoutEnlargement: true, - }); - } - } - // Apply format-specific compression - const quality = options?.quality || this.getDefaultQuality(outputFormat); - switch (outputFormat) { - case 'jpeg': - case 'jpg': - pipeline = pipeline.jpeg({ - quality, - progressive: true, - mozjpeg: true, - }); - break; - case 'png': - pipeline = pipeline.png({ - compressionLevel: COMPRESSION_CONFIG.PNG_COMPRESSION_LEVEL, - progressive: true, - adaptiveFiltering: true, - }); - break; - case 'webp': - pipeline = pipeline.webp({ - quality, - effort: 6, - }); - break; - case 'avif': - pipeline = pipeline.avif({ - quality, - effort: 4, - }); - break; - default: - // Keep original format with default compression - pipeline = pipeline.jpeg({ quality: COMPRESSION_CONFIG.JPEG_QUALITY }); - } - // Process image - const processedBuffer = await pipeline.toBuffer(); - const processedMetadata = await sharp(processedBuffer).metadata(); - const compressionRatio = originalSize > 0 ? ((originalSize - processedBuffer.length) / originalSize) * 100 : 0; - this.logger.log(`Image compressed: ${originalSize} -> ${processedBuffer.length} bytes (${compressionRatio.toFixed(1)}% reduction)`); - return { - buffer: processedBuffer, - format: outputFormat, - width: processedMetadata.width || 0, - height: processedMetadata.height || 0, - size: processedBuffer.length, - originalSize, - compressionRatio, - }; - } - /** - * Generate thumbnails in multiple sizes - */ - async generateThumbnails(buffer: Buffer, options?: { - sizes?: Array<{ - name: string; - width: number; - height: number; - }>; - format?: 'jpeg' | 'png' | 'webp'; - quality?: number; - }): Promise { - const sizes = options?.sizes || THUMBNAIL_CONFIG.SIZES; - const format = options?.format || THUMBNAIL_CONFIG.DEFAULT_FORMAT; - const quality = options?.quality || THUMBNAIL_CONFIG.DEFAULT_QUALITY; - const thumbnails: ThumbnailResult[] = []; - for (const size of sizes) { - try { - let pipeline = sharp(buffer); - // Resize to thumbnail dimensions - pipeline = pipeline.resize(size.width, size.height, { - fit: 'cover', - position: 'center', - }); - // Apply format-specific settings - switch (format) { - case 'jpeg': - pipeline = pipeline.jpeg({ quality, progressive: true }); - break; - case 'png': - pipeline = pipeline.png({ compressionLevel: 9 }); - break; - case 'webp': - default: - pipeline = pipeline.webp({ quality, effort: 6 }); - } - const thumbnailBuffer = await pipeline.toBuffer(); - const metadata = await sharp(thumbnailBuffer).metadata(); - thumbnails.push({ - name: size.name, - buffer: thumbnailBuffer, - width: metadata.width || size.width, - height: metadata.height || size.height, - size: thumbnailBuffer.length, - }); - this.logger.log(`Generated ${size.name} thumbnail: ${thumbnailBuffer.length} bytes`); - } - catch (error) { - this.logger.error(`Failed to generate ${size.name} thumbnail:`, error); - } - } - return thumbnails; - } - /** - * Generate a single thumbnail - */ - async generateThumbnail(buffer: Buffer, width: number, height: number, options?: { - format?: 'jpeg' | 'png' | 'webp'; - quality?: number; - fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'; - }): Promise { - const format = options?.format || THUMBNAIL_CONFIG.DEFAULT_FORMAT; - const quality = options?.quality || THUMBNAIL_CONFIG.DEFAULT_QUALITY; - const fit = options?.fit || 'cover'; - let pipeline = sharp(buffer).resize(width, height, { - fit, - position: 'center', - }); - switch (format) { - case 'jpeg': - pipeline = pipeline.jpeg({ quality, progressive: true }); - break; - case 'png': - pipeline = pipeline.png({ compressionLevel: 9 }); - break; - case 'webp': - default: - pipeline = pipeline.webp({ quality, effort: 6 }); - } - return pipeline.toBuffer(); - } - /** - * Validate image dimensions - */ - async validateDimensions(buffer: Buffer): Promise<{ - valid: boolean; - width?: number; - height?: number; - errors: string[]; - }> { - const metadata = await sharp(buffer).metadata(); - const errors: string[] = []; - if (!metadata.width || !metadata.height) { - return { - valid: false, - errors: ['Could not determine image dimensions'], - }; - } - const { MIN_WIDTH, MIN_HEIGHT, MAX_WIDTH, MAX_HEIGHT, MAX_PIXELS } = IMAGE_DIMENSION_LIMITS; - if (metadata.width < MIN_WIDTH || metadata.height < MIN_HEIGHT) { - errors.push(`Image dimensions too small (minimum: ${MIN_WIDTH}x${MIN_HEIGHT})`); - } - if (metadata.width > MAX_WIDTH || metadata.height > MAX_HEIGHT) { - errors.push(`Image dimensions too large (maximum: ${MAX_WIDTH}x${MAX_HEIGHT})`); - } - const totalPixels = metadata.width * metadata.height; - if (totalPixels > MAX_PIXELS) { - errors.push(`Image has too many pixels (maximum: ${MAX_PIXELS.toLocaleString()})`); - } - return { - valid: errors.length === 0, - width: metadata.width, - height: metadata.height, - errors, - }; - } - /** - * Convert image to different format - */ - async convertFormat(buffer: Buffer, targetFormat: 'jpeg' | 'png' | 'webp' | 'avif', quality?: number): Promise { - let pipeline = sharp(buffer); - const q = quality || this.getDefaultQuality(targetFormat); - switch (targetFormat) { - case 'jpeg': - pipeline = pipeline.jpeg({ quality: q, progressive: true }); - break; - case 'png': - pipeline = pipeline.png({ compressionLevel: COMPRESSION_CONFIG.PNG_COMPRESSION_LEVEL }); - break; - case 'webp': - pipeline = pipeline.webp({ quality: q, effort: 6 }); - break; - case 'avif': - pipeline = pipeline.avif({ quality: q, effort: 4 }); - break; - } - return pipeline.toBuffer(); - } - /** - * Strip metadata from image (privacy/security) - */ - async stripMetadata(buffer: Buffer): Promise { - return sharp(buffer).withMetadata().toBuffer(); - } - /** - * Get optimal format based on content type - */ - private getOptimalFormat(currentFormat?: string): string { - switch (currentFormat) { - case 'png': - return 'png'; // Keep PNG for transparency - case 'gif': - return 'webp'; // Convert GIF to WebP - case 'svg': - return 'png'; // Rasterize SVG - case 'webp': - return 'webp'; - case 'avif': - return 'avif'; - case 'jpeg': - case 'jpg': - default: - return 'webp'; // Default to WebP for best compression - } - } - /** - * Get default quality for format - */ - private getDefaultQuality(format: string): number { - switch (format) { - case 'jpeg': - case 'jpg': - return COMPRESSION_CONFIG.JPEG_QUALITY; - case 'webp': - return COMPRESSION_CONFIG.WEBP_QUALITY; - case 'avif': - return COMPRESSION_CONFIG.AVIF_QUALITY; - case 'png': - return 100; // PNG uses compression level, not quality - default: - return 85; - } - } -} diff --git a/src/media/processing/video.processor.ts b/src/media/processing/video.processor.ts deleted file mode 100644 index 492c28ed..00000000 --- a/src/media/processing/video.processor.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { Processor, Process, OnQueueFailed, OnQueueCompleted } from '@nestjs/bull'; -import { QUEUE_NAMES, JOB_NAMES } from '../../common/constants/queue.constants'; -import { Job } from 'bull'; -import { Logger } from '@nestjs/common'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import ffmpeg from 'fluent-ffmpeg'; -import { FileStorageService } from '../storage/file-storage.service'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { UploadedFile } from '@nestjs/common'; -import { ContentMetadata } from '../../cdn/entities/content-metadata.entity'; -@Processor(QUEUE_NAMES.MEDIA_PROCESSING) -export class VideoProcessor { - private readonly logger = new Logger(VideoProcessor.name); - constructor(private readonly storage: FileStorageService, - @InjectRepository(ContentMetadata) - private readonly contentRepo: Repository, - ) {} - - @Process(JOB_NAMES.TRANSCODE_VIDEO) - async handleTranscode(job: Job) { - const { contentId, url, fileName } = job.data as { - contentId: string; - url: string; - fileName: string; - }; - this.logger.log(`Transcoding job for ${contentId} - ${url}`); - - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `media-${contentId}-`)); - const inputPath = path.join(tmpDir, fileName); - - // Download the source - const signed = await this.storage.getSignedUrl(url, 60); - await downloadToFile(signed, inputPath); - - // Produce HLS with 3 variants (1080p,720p,480p) - const hlsDir = path.join(tmpDir, 'hls'); - fs.mkdirSync(hlsDir); - - await new Promise((resolve, reject) => { - ffmpeg(inputPath) - .addOption('-preset', 'fast') - .addOption('-g', '48') - .addOption('-sc_threshold', '0') - .outputOptions(['-map 0:v', '-map 0:a?', '-c:a aac', '-c:v h264', '-profile:v main']) - .output(path.join(hlsDir, 'index.m3u8')) - .on('end', () => resolve()) - .on('error', (err) => reject(err)) - .run(); - }); - - // Upload HLS directory contents - const files = fs.readdirSync(hlsDir); - const uploaded: string[] = []; - for (const f of files) { - const p = path.join(hlsDir, f); - const buffer = fs.readFileSync(p); - // store each file under contentId/hls/ - const fakeFile: UploadedFile = { - buffer, - originalname: f, - mimetype: 'application/octet-stream', - size: buffer.length, - fieldname: 'file', - encoding: '7bit', - destination: '', - filename: f, - stream: null as any, - path: p, - }; - const keyRes = await this.storage.uploadFile(fakeFile as any, { contentId } as any); - uploaded.push(keyRes.url); - } - - // Update metadata - const meta = await this.contentRepo.findOne({ where: { contentId } }); - if (meta) { - meta.metadata = meta.metadata || {}; - // Extend metadata type to include hlsManifest for videos - (meta.metadata as any).hlsManifest = - uploaded.find((u) => u.endsWith('index.m3u8')) || uploaded[0]; - meta.variants = uploaded.map((u) => ({ - name: u.split('/').pop(), - url: u, - width: 0, - height: 0, - size: 0, - })); - meta.status = 'ready' as any; - await this.contentRepo.save(meta); - } - - // Cleanup - try { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } catch (e) { - this.logger.warn('Failed to clean tmpdir', e); - } - - return { uploaded }; - } - - /** - * Executes on Failed. - * @param job The job. - * @param err The err. - * @returns The operation result. - */ - @OnQueueFailed() - async onFailed(job: Job, err: Error) { - this.logger.error(`Job ${job.id} failed: ${err.message}`); - } - - /** - * Executes on Complete. - * @param job The job. - * @param _result The result. - * @returns The operation result. - */ - @OnQueueCompleted() - async onComplete(job: Job, _result: any) { - this.logger.log(`Job ${job.id} completed`); - } -} - -async function downloadToFile(url: string, dest: string): Promise { - const https = url.startsWith('https') ? await import('https') : await import('http'); - return new Promise((resolve, reject) => { - const file = fs.createWriteStream(dest); - const req = https.get(url, (res: any) => { - if (res.statusCode >= 400) return reject(new Error(`Failed to download: ${res.statusCode}`)); - res.pipe(file); - file.on('finish', () => { - file.close((err?: NodeJS.ErrnoException | null) => { - if (err) reject(err); - else resolve(); - }); - // Upload HLS directory contents - const files = fs.readdirSync(hlsDir); - const uploaded: string[] = []; - for (const f of files) { - const p = path.join(hlsDir, f); - const buffer = fs.readFileSync(p); - // store each file under contentId/hls/ - const fakeFile: UploadedFile = { - buffer, - originalname: f, - mimetype: 'application/octet-stream', - size: buffer.length, - fieldname: 'file', - encoding: '7bit', - destination: '', - filename: f, - stream: null as unknown, - path: p, - }; - const keyRes = await this.storage.uploadFile(fakeFile as unknown, { contentId } as unknown); - uploaded.push(keyRes.url); - } - // Update metadata - const meta = await this.contentRepo.findOne({ where: { contentId } }); - if (meta) { - meta.metadata = meta.metadata || {}; - // Extend metadata type to include hlsManifest for videos - (meta.metadata as unknown).hlsManifest = - uploaded.find((u) => u.endsWith('index.m3u8')) || uploaded[0]; - meta.variants = uploaded.map((u) => ({ - name: u.split('/').pop(), - url: u, - width: 0, - height: 0, - size: 0, - })); - meta.status = 'ready' as unknown; - await this.contentRepo.save(meta); - } - // Cleanup - try { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } - catch (e) { - this.logger.warn('Failed to clean tmpdir', e); - } - return { uploaded }; - } - @OnQueueFailed() - async onFailed(job: Job, err: Error) { - this.logger.error(`Job ${job.id} failed: ${err.message}`); - } - @OnQueueCompleted() - async onComplete(job: Job, _result: unknown) { - this.logger.log(`Job ${job.id} completed`); - } -} -async function downloadToFile(url: string, dest: string): Promise { - const https = url.startsWith('https') ? await import('https') : await import('http'); - return new Promise((resolve, reject) => { - const file = fs.createWriteStream(dest); - const req = https.get(url, (res: unknown) => { - if (res.statusCode >= 400) - return reject(new Error(`Failed to download: ${res.statusCode}`)); - res.pipe(file); - file.on('finish', () => { - file.close((err?: NodeJS.ErrnoException | null) => { - if (err) - reject(err); - else - resolve(); - }); - }); - }); - req.on('error', (err: Error) => reject(err)); - }); -} diff --git a/src/media/storage/file-storage.service.ts b/src/media/storage/file-storage.service.ts deleted file mode 100644 index 446f1fed..00000000 --- a/src/media/storage/file-storage.service.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, } from '@aws-sdk/client-s3'; -import { Readable } from 'stream'; -import { ContentMetadata } from '../../cdn/entities/content-metadata.entity'; -import { UploadedFile } from '@nestjs/common'; - -/** - * Provides file Storage operations. - */ -@Injectable() -export class FileStorageService { - private readonly logger = new Logger(FileStorageService.name); - private readonly s3Client: S3Client; - private readonly bucketName: string; - private readonly publicBaseUrl: string; - private readonly region: string; - - constructor(private configService: ConfigService) { - this.region = this.configService.get('AWS_REGION', 'us-east-1'); - this.bucketName = - this.configService.get('AWS_S3_BUCKET', '') || - this.configService.get('AWS_S3_BUCKET_NAME', ''); - - const distributionId = this.configService.get('AWS_CLOUDFRONT_DISTRIBUTION_ID', ''); - - this.publicBaseUrl = distributionId - ? `https://${distributionId}.cloudfront.net` - : this.buildS3BaseUrl(this.bucketName, this.region); - - this.s3Client = new S3Client({ - region: this.region, - credentials: { - accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID', ''), - secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY', ''), - }, - }); - } - - // Legacy method for backward compatibility - /** - * Uploads file. - * @param file The file to process. - * @param metadata The data to process. - * @returns The operation result. - */ - async uploadFile( - file: UploadedFile, - metadata: ContentMetadata, - ): Promise<{ url: string; etag?: string }> { - const key = `${metadata.contentId}/${Date.now()}_${file.originalname}`; - const etag = await this.uploadProcessedFile(file.buffer, key, file.mimetype); - return { - url: this.getPublicUrl(key), - etag, - }; - } - - // Legacy method for backward compatibility - async getSignedUrl(keyOrUrl: string, _expiresInSeconds = 900): Promise { - // If a full URL is provided, return as-is - if (keyOrUrl.startsWith('http')) return keyOrUrl; - - const command = new GetObjectCommand({ - Bucket: this.bucketName, - Key: keyOrUrl, - }); - - // For simplicity, return the key as URL (in production, generate proper signed URL) - return `https://${this.bucketName}.s3.amazonaws.com/${keyOrUrl}`; - } - - async uploadProcessedFile( - buffer: Buffer, - key: string, - contentType: string, - ): Promise { - const command = new PutObjectCommand({ - Bucket: this.bucketName, - Key: key, - Body: buffer, - ContentType: contentType, - }); - - const result = await this.s3Client.send(command); - this.logger.log(`Uploaded file to ${key}`); - return result.ETag; - } - - /** - * Downloads file. - * @param storageKey The storage key. - * @returns The resulting buffer. - */ - async downloadFile(storageKey: string): Promise { - const command = new GetObjectCommand({ - Bucket: this.bucketName, - Key: storageKey, - }); - - const response = await this.s3Client.send(command); - const stream = response.Body as Readable; - const chunks: Buffer[] = []; - - for await (const chunk of stream) { - chunks.push(chunk); - } - - return Buffer.concat(chunks); - } - - /** - * Removes file. - * @param storageKey The storage key. - */ - async deleteFile(storageKey: string): Promise { - const command = new DeleteObjectCommand({ - Bucket: this.bucketName, - Key: storageKey, - }); - - await this.s3Client.send(command); - this.logger.log(`Deleted file ${storageKey}`); - } - - getPublicUrl(storageKey: string): string { - if (!storageKey) return ''; - if (!this.publicBaseUrl) return storageKey; - return `${this.publicBaseUrl.replace(/\/+$/, '')}/${storageKey.replace(/^\/+/, '')}`; - } - - private buildS3BaseUrl(bucketName: string, region: string): string { - if (!bucketName) return ''; - if (!region || region === 'us-east-1') { - return `https://${bucketName}.s3.amazonaws.com`; - } - return `https://${bucketName}.s3.${region}.amazonaws.com`; - } -} diff --git a/src/media/validation/file-upload-validation.service.ts b/src/media/validation/file-upload-validation.service.ts deleted file mode 100644 index a8d6a8e5..00000000 --- a/src/media/validation/file-upload-validation.service.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Injectable, Logger, BadRequestException } from '@nestjs/common'; -import { - ALL_ALLOWED_FILE_TYPES, - MAX_UPLOAD_FILE_SIZE, - FILE_SIZE_LIMITS, - MAGIC_NUMBERS, -} from '../validation/file-validation.constants'; - -@Injectable() -export class FileUploadValidationService { - private readonly logger = new Logger(FileUploadValidationService.name); - - /** - * Validate file MIME type against allowed list - */ - validateMimeType(mimetype: string): boolean { - const normalizedMimeType = mimetype?.toLowerCase().trim() || ''; - return ALL_ALLOWED_FILE_TYPES.includes(normalizedMimeType); - } - - /** - * Validate file size against global and type-specific limits - */ - validateFileSize(fileSize: number, mimetype?: string): void { - if (fileSize > MAX_UPLOAD_FILE_SIZE) { - throw new BadRequestException( - `File size ${fileSize} bytes exceeds maximum allowed size of ${MAX_UPLOAD_FILE_SIZE} bytes`, - ); - } - - if (mimetype) { - const typeSpecificLimit = this.getTypeSpecificLimit(mimetype); - if (fileSize > typeSpecificLimit) { - throw new BadRequestException( - `File size ${fileSize} bytes exceeds the limit for ${mimetype} files (${typeSpecificLimit} bytes)`, - ); - } - } - } - - /** - * Validate file magic numbers (file signature) to prevent MIME type spoofing - */ - validateMagicNumber(buffer: Buffer, mimetype: string): boolean { - const expectedMagicNumbers = MAGIC_NUMBERS[mimetype]; - - if (!expectedMagicNumbers || expectedMagicNumbers.length === 0) { - // If no magic number defined, allow the file - this.logger.warn(`No magic number definition for ${mimetype}, skipping validation`); - return true; - } - - // Check if buffer matches any of the expected magic numbers - return expectedMagicNumbers.some((magicNumber) => { - if (buffer.length < magicNumber.length) { - return false; - } - - for (let i = 0; i < magicNumber.length; i++) { - if (buffer[i] !== magicNumber[i]) { - return false; - } - } - return true; - }); - } - - /** - * Scan file for malware/virus (stub implementation) - * In production, integrate with ClamAV or similar antivirus service - */ - async scanForMalware(fileBuffer: Buffer, fileName: string): Promise { - try { - // TODO: Integrate with actual antivirus scanning service - // Example: ClamAV, VirusTotal API, or cloud-based scanning - - this.logger.log(`Scanning file ${fileName} for malware...`); - - // Placeholder: In production, this would call an external scanning service - // For now, we perform basic heuristic checks - - // Check for potentially dangerous file patterns - const hasSuspiciousPatterns = this.checkSuspiciousPatterns(fileBuffer); - - if (hasSuspiciousPatterns) { - this.logger.warn(`Suspicious patterns detected in file: ${fileName}`); - return false; - } - - this.logger.log(`File ${fileName} passed malware scan`); - return true; - } catch (error) { - this.logger.error(`Malware scan failed for ${fileName}: ${error.message}`); - // Fail secure: if scan fails, reject the file - return false; - } - } - - /** - * Comprehensive file validation - */ - async validateFile(file: { - buffer: Buffer; - mimetype: string; - size: number; - originalname: string; - }): Promise { - // 1. Validate MIME type - if (!this.validateMimeType(file.mimetype)) { - throw new BadRequestException( - `File type "${file.mimetype}" is not allowed. Allowed types: ${ALL_ALLOWED_FILE_TYPES.join(', ')}`, - ); - } - - // 2. Validate file size - this.validateFileSize(file.size, file.mimetype); - - // 3. Validate magic numbers (prevent MIME type spoofing) - if (!this.validateMagicNumber(file.buffer, file.mimetype)) { - throw new BadRequestException( - `File content does not match declared type "${file.mimetype}". Possible file type spoofing detected.`, - ); - } - - // 4. Scan for malware - const isClean = await this.scanForMalware(file.buffer, file.originalname); - if (!isClean) { - throw new BadRequestException( - `File "${file.originalname}" failed security scanning and was rejected.`, - ); - } - } - - /** - * Get type-specific file size limit - */ - private getTypeSpecificLimit(mimetype: string): number { - if (mimetype.startsWith('image/')) { - return FILE_SIZE_LIMITS.IMAGE_MAX_SIZE; - } - if (mimetype.startsWith('video/')) { - return FILE_SIZE_LIMITS.VIDEO_MAX_SIZE; - } - if (mimetype.startsWith('audio/')) { - return FILE_SIZE_LIMITS.AUDIO_MAX_SIZE; - } - if ( - mimetype.startsWith('application/pdf') || - mimetype.startsWith('application/msword') || - mimetype.startsWith('application/vnd.') - ) { - return FILE_SIZE_LIMITS.DOCUMENT_MAX_SIZE; - } - if (mimetype.startsWith('application/zip') || mimetype.startsWith('application/x-')) { - return FILE_SIZE_LIMITS.ARCHIVE_MAX_SIZE; - } - return FILE_SIZE_LIMITS.DEFAULT_MAX_SIZE; - } - - /** - * Check for suspicious patterns in file buffer (basic heuristic) - */ - private checkSuspiciousPatterns(buffer: Buffer): boolean { - // Convert buffer to string for pattern matching (for text-based files) - const content = buffer.toString('utf8'); - - // Check for common malicious patterns - const suspiciousPatterns = [ - /<\?php\s+eval\s*\(/i, // PHP eval - /javascript\s*:/i, // JavaScript protocol - /on(load|error|click)\s*=/i, // Event handlers - /