From 832a54adb112c04f510dc223df06ba429193c6b1 Mon Sep 17 00:00:00 2001 From: akargi Date: Wed, 25 Feb 2026 21:54:17 +0100 Subject: [PATCH 1/4] feat(user): advanced user features (profile, preferences, activity, avatar, search, relationships, analytics, privacy, export) [Issue79] --- prisma/schema.prisma | 51 +++++++++- src/models/user.entity.ts | 36 +++++++ src/users/dto/create-user.dto.ts | 44 +++++++++ src/users/dto/user-response.dto.ts | 56 +++++++++++ src/users/user.controller.ts | 80 ++++++++++++++++ src/users/user.service.ts | 145 +++++++++++++++++++++++++++++ 6 files changed, 411 insertions(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index feb21c7..c6be34e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,6 +25,23 @@ model User { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") + // Advanced profile fields + bio String? + location String? + avatarUrl String? @map("avatar_url") + + // Preferences and privacy + preferences Json? + privacySettings Json? @map("privacy_settings") + exportRequestedAt DateTime? @map("export_requested_at") + + // Relationships + followers UserRelationship[] @relation("Followers") + following UserRelationship[] @relation("Following") + + // Activity + activities UserActivity[] + properties Property[] receivedTransactions Transaction[] @relation("UserTransactions") userRole Role? @relation(fields: [roleId], references: [id], onDelete: SetNull) @@ -36,9 +53,41 @@ model User { @@index([walletAddress]) // For searching users by wallet address @@index([role]) // For filtering users by role @@index([createdAt]) // For sorting/filtering by creation date - // Consider adding an index on isVerified if you often filter by verification status @@index([isVerified]) + @@index([location]) @@map("users") +// User activity tracking +model UserActivity { + id String @id @default(cuid()) + userId String @map("user_id") + action String + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([action]) + @@index([createdAt]) + @@map("user_activities") +} + +// User relationship (followers, connections) +model UserRelationship { + id String @id @default(cuid()) + followerId String @map("follower_id") + followingId String @map("following_id") + status String @default("active") + createdAt DateTime @default(now()) @map("created_at") + + follower User @relation("Followers", fields: [followerId], references: [id], onDelete: Cascade) + following User @relation("Following", fields: [followingId], references: [id], onDelete: Cascade) + + @@unique([followerId, followingId]) + @@index([followerId]) + @@index([followingId]) + @@map("user_relationships") +} } model Property { diff --git a/src/models/user.entity.ts b/src/models/user.entity.ts index c9243b3..cb0f800 100644 --- a/src/models/user.entity.ts +++ b/src/models/user.entity.ts @@ -19,10 +19,46 @@ export class User implements PrismaUser { createdAt: Date; updatedAt: Date; + + // Advanced profile fields + bio?: string | null; + location?: string | null; + avatarUrl?: string | null; + + // Preferences and privacy + preferences?: any | null; + privacySettings?: any | null; + exportRequestedAt?: Date | null; + + // Relationships + followers?: UserRelationship[]; + following?: UserRelationship[]; + + // Activity + activities?: UserActivity[]; } /** * Input used when creating a user +// User activity entity +export class UserActivity { + id: string; + userId: string; + action: string; + metadata?: any; + createdAt: Date; +} + +// User relationship entity +export class UserRelationship { + id: string; + followerId: string; + followingId: string; + status: string; + createdAt: Date; + follower?: User; + following?: User; +} * Flexible enough for email/password and Web3 users */ export type CreateUserInput = { diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index 0c4a615..cc8c00d 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -76,4 +76,48 @@ export class CreateUserDto { @IsXssSafe({ message: 'Wallet address contains potentially malicious content' }) @IsNotSqlInjection({ message: 'Wallet address contains potential SQL injection' }) walletAddress?: string; + + @ApiPropertyOptional({ + description: 'User biography', + example: 'Blockchain enthusiast and property investor.', + maxLength: 500, + }) + @IsOptional() + @IsString() + @MaxLength(500) + bio?: string; + + @ApiPropertyOptional({ + description: 'User location', + example: 'London, UK', + maxLength: 100, + }) + @IsOptional() + @IsString() + @MaxLength(100) + location?: string; + + @ApiPropertyOptional({ + description: 'Avatar image URL', + example: 'https://example.com/avatar.jpg', + }) + @IsOptional() + @IsString() + avatarUrl?: string; + + @ApiPropertyOptional({ + description: 'User preferences (JSON object)', + example: '{ "theme": "dark", "notifications": true }', + type: Object, + }) + @IsOptional() + preferences?: any; + + @ApiPropertyOptional({ + description: 'User privacy settings (JSON object)', + example: '{ "profileVisible": true }', + type: Object, + }) + @IsOptional() + privacySettings?: any; } diff --git a/src/users/dto/user-response.dto.ts b/src/users/dto/user-response.dto.ts index 79dc4f1..6e75300 100644 --- a/src/users/dto/user-response.dto.ts +++ b/src/users/dto/user-response.dto.ts @@ -31,6 +31,62 @@ export class UserResponseDto { }) walletAddress?: string; + @ApiPropertyOptional({ + description: 'User biography', + example: 'Blockchain enthusiast and property investor.', + }) + bio?: string; + + @ApiPropertyOptional({ + description: 'User location', + example: 'London, UK', + }) + location?: string; + + @ApiPropertyOptional({ + description: 'Avatar image URL', + example: 'https://example.com/avatar.jpg', + }) + avatarUrl?: string; + + @ApiPropertyOptional({ + description: 'User preferences (JSON object)', + example: '{ "theme": "dark", "notifications": true }', + type: Object, + }) + preferences?: any; + + @ApiPropertyOptional({ + description: 'User privacy settings (JSON object)', + example: '{ "profileVisible": true }', + type: Object, + }) + privacySettings?: any; + + @ApiPropertyOptional({ + description: 'Followers count', + example: 10, + }) + followersCount?: number; + + @ApiPropertyOptional({ + description: 'Following count', + example: 5, + }) + followingCount?: number; + + @ApiPropertyOptional({ + description: 'User activity count', + example: 100, + }) + activityCount?: number; + + @ApiPropertyOptional({ + description: 'User login count', + example: 20, + }) + loginCount?: number; + @ApiProperty({ description: 'Whether the user email is verified', example: true, diff --git a/src/users/user.controller.ts b/src/users/user.controller.ts index b8dd75d..d8e1e99 100644 --- a/src/users/user.controller.ts +++ b/src/users/user.controller.ts @@ -23,4 +23,84 @@ export class UserController { findOne(@Param('id') id: string) { return this.userService.findById(id); } + + // --- Advanced Features --- + + @Patch(':id/profile') + @ApiOperation({ summary: 'Update user profile (bio, location, avatar)' }) + updateProfile(@Param('id') id: string, @Body() profile: { bio?: string; location?: string; avatarUrl?: string }) { + return this.userService.updateProfile(id, profile); + } + + @Patch(':id/preferences') + @ApiOperation({ summary: 'Update user preferences' }) + updatePreferences(@Param('id') id: string, @Body() preferences: any) { + return this.userService.updatePreferences(id, preferences); + } + + @Post(':id/activity') + @ApiOperation({ summary: 'Log user activity' }) + logActivity(@Param('id') id: string, @Body() body: { action: string; metadata?: any }) { + return this.userService.logActivity(id, body.action, body.metadata); + } + + @Get(':id/activity') + @ApiOperation({ summary: 'Get user activity history' }) + getActivityHistory(@Param('id') id: string) { + return this.userService.getActivityHistory(id); + } + + @Patch(':id/avatar') + @ApiOperation({ summary: 'Update user avatar' }) + updateAvatar(@Param('id') id: string, @Body() body: { avatarUrl: string }) { + return this.userService.updateAvatar(id, body.avatarUrl); + } + + @Get('search/:query') + @ApiOperation({ summary: 'Search users by name, email, or location' }) + searchUsers(@Param('query') query: string) { + return this.userService.searchUsers(query); + } + + @Post(':id/follow/:targetId') + @ApiOperation({ summary: 'Follow another user' }) + followUser(@Param('id') id: string, @Param('targetId') targetId: string) { + return this.userService.followUser(id, targetId); + } + + @Delete(':id/follow/:targetId') + @ApiOperation({ summary: 'Unfollow a user' }) + unfollowUser(@Param('id') id: string, @Param('targetId') targetId: string) { + return this.userService.unfollowUser(id, targetId); + } + + @Get(':id/followers') + @ApiOperation({ summary: 'List followers of a user' }) + getFollowers(@Param('id') id: string) { + return this.userService.getFollowers(id); + } + + @Get(':id/following') + @ApiOperation({ summary: 'List users a user is following' }) + getFollowing(@Param('id') id: string) { + return this.userService.getFollowing(id); + } + + @Get(':id/analytics') + @ApiOperation({ summary: 'Get user analytics and engagement metrics' }) + getUserAnalytics(@Param('id') id: string) { + return this.userService.getUserAnalytics(id); + } + + @Patch(':id/privacy') + @ApiOperation({ summary: 'Update user privacy settings' }) + updatePrivacySettings(@Param('id') id: string, @Body() privacySettings: any) { + return this.userService.updatePrivacySettings(id, privacySettings); + } + + @Post(':id/export') + @ApiOperation({ summary: 'Request user data export' }) + requestDataExport(@Param('id') id: string) { + return this.userService.requestDataExport(id); + } } diff --git a/src/users/user.service.ts b/src/users/user.service.ts index a553ab3..0de2726 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -283,4 +283,149 @@ export class UserService { data, }); } + /** + * Update user profile (bio, location, avatar) + */ + async updateProfile(userId: string, profile: { bio?: string; location?: string; avatarUrl?: string }) { + return this.prisma.user.update({ + where: { id: userId }, + data: profile, + }); + } + + /** + * Update user preferences (JSON) + */ + async updatePreferences(userId: string, preferences: any) { + return this.prisma.user.update({ + where: { id: userId }, + data: { preferences }, + }); + } + + /** + * Track user activity + */ + async logActivity(userId: string, action: string, metadata?: any) { + return this.prisma.userActivity.create({ + data: { userId, action, metadata }, + }); + } + + /** + * Get user activity history + */ + async getActivityHistory(userId: string, limit = 50) { + return this.prisma.userActivity.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + take: limit, + }); + } + + /** + * Update user avatar + */ + async updateAvatar(userId: string, avatarUrl: string) { + return this.prisma.user.update({ + where: { id: userId }, + data: { avatarUrl }, + }); + } + + /** + * Search users by name, email, or location + */ + async searchUsers(query: string, limit = 20) { + return this.prisma.user.findMany({ + where: { + OR: [ + { email: { contains: query, mode: 'insensitive' } }, + { firstName: { contains: query, mode: 'insensitive' } }, + { lastName: { contains: query, mode: 'insensitive' } }, + { location: { contains: query, mode: 'insensitive' } }, + ], + }, + take: limit, + }); + } + + /** + * Follow another user + */ + async followUser(followerId: string, followingId: string) { + if (followerId === followingId) throw new BadRequestException('Cannot follow yourself'); + // Prevent duplicate follows + const existing = await this.prisma.userRelationship.findUnique({ + where: { followerId_followingId: { followerId, followingId } }, + }); + if (existing) return existing; + return this.prisma.userRelationship.create({ + data: { followerId, followingId }, + }); + } + + /** + * Unfollow a user + */ + async unfollowUser(followerId: string, followingId: string) { + return this.prisma.userRelationship.delete({ + where: { followerId_followingId: { followerId, followingId } }, + }); + } + + /** + * List followers of a user + */ + async getFollowers(userId: string, limit = 50) { + return this.prisma.userRelationship.findMany({ + where: { followingId: userId }, + take: limit, + include: { follower: true }, + }); + } + + /** + * List users a user is following + */ + async getFollowing(userId: string, limit = 50) { + return this.prisma.userRelationship.findMany({ + where: { followerId: userId }, + take: limit, + include: { following: true }, + }); + } + + /** + * Get user analytics (basic engagement metrics) + */ + async getUserAnalytics(userId: string) { + const [loginCount, activityCount, followers, following] = await Promise.all([ + this.prisma.userActivity.count({ where: { userId, action: 'login' } }), + this.prisma.userActivity.count({ where: { userId } }), + this.prisma.userRelationship.count({ where: { followingId: userId } }), + this.prisma.userRelationship.count({ where: { followerId: userId } }), + ]); + return { loginCount, activityCount, followers, following }; + } + + /** + * Update privacy settings + */ + async updatePrivacySettings(userId: string, privacySettings: any) { + return this.prisma.user.update({ + where: { id: userId }, + data: { privacySettings }, + }); + } + + /** + * Request user data export + */ + async requestDataExport(userId: string) { + return this.prisma.user.update({ + where: { id: userId }, + data: { exportRequestedAt: new Date() }, + }); + } } From 313312ea91f2342efb5a32c7855a3d388269fb52 Mon Sep 17 00:00:00 2001 From: akargi Date: Wed, 25 Feb 2026 22:01:38 +0100 Subject: [PATCH 2/4] feat(observability): add distributed tracing, Prometheus metrics, and logging foundation [Issue78] --- OBSERVABILITY_README.md | 27 +++++++++++++++ src/app.module.ts | 2 ++ src/observability/metrics.interceptor.ts | 42 +++++++++++++++++++++++ src/observability/observability.module.ts | 33 ++++++++++++++++++ src/observability/tracing.service.ts | 20 +++++++++++ 5 files changed, 124 insertions(+) create mode 100644 OBSERVABILITY_README.md create mode 100644 src/observability/metrics.interceptor.ts create mode 100644 src/observability/observability.module.ts create mode 100644 src/observability/tracing.service.ts diff --git a/OBSERVABILITY_README.md b/OBSERVABILITY_README.md new file mode 100644 index 0000000..d9edd52 --- /dev/null +++ b/OBSERVABILITY_README.md @@ -0,0 +1,27 @@ +# Observability & Monitoring System + +This module introduces advanced monitoring and observability for the PropChain-BackEnd, supporting production-grade operations and troubleshooting. + +## Features +- **Distributed Tracing:** OpenTelemetry-based tracing with correlation ID propagation. +- **Metrics Collection:** Prometheus metrics for HTTP requests (latency, count, status). +- **Centralized Logging:** Winston-based structured logging, ready for aggregation. +- **Performance Monitoring:** Request duration histograms and counters. +- **Health Checks:** Existing endpoints for liveness, readiness, and dependency checks. +- **Extensible:** Ready for integration with Sentry (error tracking), Grafana/Kibana (dashboards), and anomaly detection tools. + +## Usage +- All HTTP requests are traced and measured automatically. +- Metrics are exposed at `/metrics` (Prometheus scrape endpoint). +- Logs are structured and can be forwarded to ELK, Azure Monitor, or similar. +- Health endpoints remain at `/health`, `/health/detailed`, `/health/liveness`, `/health/readiness`. + +## Next Steps +- Configure Winston transports for log aggregation. +- Integrate Sentry or similar for error tracking. +- Add Grafana/Kibana dashboard templates. +- Integrate anomaly detection and predictive monitoring as needed. + +--- + +For questions or further enhancements, see the ObservabilityModule and related files in `src/observability/`. diff --git a/src/app.module.ts b/src/app.module.ts index 4892d7d..01ab22b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -45,6 +45,7 @@ import { AuditController } from './common/controllers/audit.controller'; // Middleware import { AuthRateLimitMiddleware } from './auth/middleware/auth.middleware'; +import { ObservabilityModule } from './observability/observability.module'; @Module({ imports: [ @@ -105,6 +106,7 @@ import { AuthRateLimitMiddleware } from './auth/middleware/auth.middleware'; // Compliance & Security AuditModule, RbacModule, + ObservabilityModule, ], controllers: [ AuditController, // Add the audit controller diff --git a/src/observability/metrics.interceptor.ts b/src/observability/metrics.interceptor.ts new file mode 100644 index 0000000..799a13b --- /dev/null +++ b/src/observability/metrics.interceptor.ts @@ -0,0 +1,42 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; +import { Histogram, Counter, Registry } from 'prom-client'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +@Injectable() +export class MetricsInterceptor implements NestInterceptor { + private readonly httpRequestDuration: Histogram; + private readonly httpRequestCount: Counter; + + constructor() { + this.httpRequestDuration = new Histogram({ + name: 'http_request_duration_seconds', + help: 'Duration of HTTP requests in seconds', + labelNames: ['method', 'route', 'status'], + buckets: [0.05, 0.1, 0.2, 0.5, 1, 2, 5], + }); + this.httpRequestCount = new Counter({ + name: 'http_request_total', + help: 'Total number of HTTP requests', + labelNames: ['method', 'route', 'status'], + }); + } + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req = context.switchToHttp().getRequest(); + const method = req.method; + const route = req.route?.path || req.url; + const start = process.hrtime(); + + return next.handle().pipe( + tap(() => { + const res = context.switchToHttp().getResponse(); + const status = res.statusCode; + const duration = process.hrtime(start); + const seconds = duration[0] + duration[1] / 1e9; + this.httpRequestDuration.labels(method, route, status).observe(seconds); + this.httpRequestCount.labels(method, route, status).inc(); + }), + ); + } +} diff --git a/src/observability/observability.module.ts b/src/observability/observability.module.ts new file mode 100644 index 0000000..beaf37c --- /dev/null +++ b/src/observability/observability.module.ts @@ -0,0 +1,33 @@ +import { Module, OnModuleInit } from '@nestjs/common'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { PrometheusModule } from '@willsoto/nestjs-prometheus'; +import { WinstonModule } from 'nest-winston'; +import { LoggerService } from '../common/logger/logger.service'; +import { LoggingInterceptor } from '../common/logger/logging.interceptor'; +import { MetricsInterceptor } from './metrics.interceptor'; +import { TracingService } from './tracing.service'; + +@Module({ + imports: [ + PrometheusModule.register(), + WinstonModule.forRoot({ + // Winston config for centralized logging + transports: [ + // Add transports for console, file, or remote log aggregation + ], + }), + ], + providers: [ + LoggerService, + TracingService, + { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, + { provide: APP_INTERCEPTOR, useClass: MetricsInterceptor }, + ], + exports: [LoggerService, TracingService], +}) +export class ObservabilityModule implements OnModuleInit { + constructor(private readonly tracingService: TracingService) {} + onModuleInit() { + this.tracingService.init(); + } +} diff --git a/src/observability/tracing.service.ts b/src/observability/tracing.service.ts new file mode 100644 index 0000000..f4afc37 --- /dev/null +++ b/src/observability/tracing.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { NodeSDK } from '@opentelemetry/sdk-node'; +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; +import { Resource } from '@opentelemetry/resources'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; + +@Injectable() +export class TracingService { + private sdk: NodeSDK; + + init() { + this.sdk = new NodeSDK({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'propchain-backend', + }), + instrumentations: [getNodeAutoInstrumentations()], + }); + this.sdk.start(); + } +} From bc1f54f7d2a5ac2e8744a4ec25c1a115d3aee248 Mon Sep 17 00:00:00 2001 From: akargi Date: Wed, 25 Feb 2026 22:14:02 +0100 Subject: [PATCH 3/4] fix(observability): resolve type errors and missing initializers --- package-lock.json | 1816 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 6 + 2 files changed, 1766 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index 369b7a7..20147db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,12 @@ "@nestjs/throttler": "^5.1.1", "@nestjs/typeorm": "^10.0.2", "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@opentelemetry/auto-instrumentations-node": "^0.70.1", + "@opentelemetry/resources": "^2.5.1", + "@opentelemetry/sdk-node": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.39.0", "@prisma/client": "^6.19.2", + "@willsoto/nestjs-prometheus": "^6.0.2", "axios": "^1.6.2", "bcrypt": "^6.0.0", "bull": "^4.12.2", @@ -57,6 +62,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.11.3", + "prom-client": "^15.1.3", "qrcode": "^1.5.4", "redis": "^4.6.12", "reflect-metadata": "^0.1.13", @@ -1760,7 +1766,6 @@ "version": "1.14.3", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.8.0", @@ -1774,7 +1779,6 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "lodash.camelcase": "^4.3.0", @@ -2884,7 +2888,6 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -3927,52 +3930,1436 @@ "node": ">= 12" } }, - "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-gnu": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz", - "integrity": "sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==", - "license": "MIT", - "optional": true, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-gnu": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz", + "integrity": "sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-musl": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz", + "integrity": "sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-win32-x64-msvc": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz", + "integrity": "sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.212.0.tgz", + "integrity": "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/auto-instrumentations-node": { + "version": "0.70.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.70.1.tgz", + "integrity": "sha512-r8BKs0rHtBAzZViPIuzSD2eh65fOPau0NqVsca2sACuZ6LFGu6a+QMhqq7skXz+/OqKwFr/7/b6VsaNMS+zZpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/instrumentation-amqplib": "^0.59.0", + "@opentelemetry/instrumentation-aws-lambda": "^0.64.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.67.0", + "@opentelemetry/instrumentation-bunyan": "^0.57.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.57.0", + "@opentelemetry/instrumentation-connect": "^0.55.0", + "@opentelemetry/instrumentation-cucumber": "^0.28.0", + "@opentelemetry/instrumentation-dataloader": "^0.29.0", + "@opentelemetry/instrumentation-dns": "^0.55.0", + "@opentelemetry/instrumentation-express": "^0.60.0", + "@opentelemetry/instrumentation-fastify": "^0.56.0", + "@opentelemetry/instrumentation-fs": "^0.31.0", + "@opentelemetry/instrumentation-generic-pool": "^0.55.0", + "@opentelemetry/instrumentation-graphql": "^0.60.0", + "@opentelemetry/instrumentation-grpc": "^0.212.0", + "@opentelemetry/instrumentation-hapi": "^0.58.0", + "@opentelemetry/instrumentation-http": "^0.212.0", + "@opentelemetry/instrumentation-ioredis": "^0.60.0", + "@opentelemetry/instrumentation-kafkajs": "^0.21.0", + "@opentelemetry/instrumentation-knex": "^0.56.0", + "@opentelemetry/instrumentation-koa": "^0.60.0", + "@opentelemetry/instrumentation-lru-memoizer": "^0.56.0", + "@opentelemetry/instrumentation-memcached": "^0.55.0", + "@opentelemetry/instrumentation-mongodb": "^0.65.0", + "@opentelemetry/instrumentation-mongoose": "^0.58.0", + "@opentelemetry/instrumentation-mysql": "^0.58.0", + "@opentelemetry/instrumentation-mysql2": "^0.58.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.58.0", + "@opentelemetry/instrumentation-net": "^0.56.0", + "@opentelemetry/instrumentation-openai": "^0.10.0", + "@opentelemetry/instrumentation-oracledb": "^0.37.0", + "@opentelemetry/instrumentation-pg": "^0.64.0", + "@opentelemetry/instrumentation-pino": "^0.58.0", + "@opentelemetry/instrumentation-redis": "^0.60.0", + "@opentelemetry/instrumentation-restify": "^0.57.0", + "@opentelemetry/instrumentation-router": "^0.56.0", + "@opentelemetry/instrumentation-runtime-node": "^0.25.0", + "@opentelemetry/instrumentation-socket.io": "^0.59.0", + "@opentelemetry/instrumentation-tedious": "^0.31.0", + "@opentelemetry/instrumentation-undici": "^0.22.0", + "@opentelemetry/instrumentation-winston": "^0.56.0", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.33.2", + "@opentelemetry/resource-detector-aws": "^2.12.0", + "@opentelemetry/resource-detector-azure": "^0.20.0", + "@opentelemetry/resource-detector-container": "^0.8.3", + "@opentelemetry/resource-detector-gcp": "^0.47.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-node": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^2.0.0" + } + }, + "node_modules/@opentelemetry/configuration": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.212.0.tgz", + "integrity": "sha512-D8sAY6RbqMa1W8lCeiaSL2eMCW2MF87QI3y+I6DQE1j+5GrDMwiKPLdzpa/2/+Zl9v1//74LmooCTCJBvWR8Iw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.5.1.tgz", + "integrity": "sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz", + "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==", + "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/exporter-logs-otlp-grpc": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.212.0.tgz", + "integrity": "sha512-/0bk6fQG+eSFZ4L6NlckGTgUous/ib5+OVdg0x4OdwYeHzV3lTEo3it1HgnPY6UKpmX7ki+hJvxjsOql8rCeZA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0", + "@opentelemetry/sdk-logs": "0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.212.0.tgz", + "integrity": "sha512-JidJasLwG/7M9RTxV/64xotDKmFAUSBc9SNlxI32QYuUMK5rVKhHNWMPDzC7E0pCAL3cu+FyiKvsTwLi2KqPYw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.212.0", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0", + "@opentelemetry/sdk-logs": "0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.212.0.tgz", + "integrity": "sha512-RpKB5UVfxc7c6Ta1UaCrxXDTQ0OD7BCGT66a97Q5zR1x3+9fw4dSaiqMXT/6FAWj2HyFbem6Rcu1UzPZikGTWQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.212.0", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-logs": "0.212.0", + "@opentelemetry/sdk-trace-base": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.212.0.tgz", + "integrity": "sha512-/6Gqf9wpBq22XsomR1i0iPGnbQtCq2Vwnrq5oiDPjYSqveBdK1jtQbhGfmpK2mLLxk4cPDtD1ZEYdIou5K8EaA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.212.0", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-metrics": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.212.0.tgz", + "integrity": "sha512-8hgBw3aTTRpSTkU4b9MLf/2YVLnfWp+hfnLq/1Fa2cky+vx6HqTodo+Zv1GTIrAKMOOwgysOjufy0gTxngqeBg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-metrics": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.212.0.tgz", + "integrity": "sha512-C7I4WN+ghn3g7SnxXm2RK3/sRD0k/BYcXaK6lGU3yPjiM7a1M25MLuM6zY3PeVPPzzTZPfuS7+wgn/tHk768Xw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.212.0", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-metrics": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.212.0.tgz", + "integrity": "sha512-hJFLhCJba5MW5QHexZMHZdMhBfNqNItxOsN0AZojwD1W2kU9xM+BEICowFGJFo/vNV+I2BJvTtmuKafeDSAo7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-metrics": "2.5.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.212.0.tgz", + "integrity": "sha512-9xTuYWp8ClBhljDGAoa0NSsJcsxJsC9zCFKMSZJp1Osb9pjXCMRdA6fwXtlubyqe7w8FH16EWtQNKx/FWi+Ghw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-trace-base": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.212.0.tgz", + "integrity": "sha512-v/0wMozNoiEPRolzC4YoPo4rAT0q8r7aqdnRw3Nu7IDN0CGFzNQazkfAlBJ6N5y0FYJkban7Aw5WnN73//6YlA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-trace-base": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.212.0.tgz", + "integrity": "sha512-d1ivqPT0V+i0IVOOdzGaLqonjtlk5jYrW7ItutWzXL/Mk+PiYb59dymy/i2reot9dDnBFWfrsvxyqdutGF5Vig==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-trace-base": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.5.1.tgz", + "integrity": "sha512-Me6JVO7WqXGXsgr4+7o+B7qwKJQbt0c8WamFnxpkR43avgG9k/niTntwCaXiXUTjonWy0+61ZuX6CGzj9nn8CQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-trace-base": "2.5.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", + "integrity": "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.212.0", + "import-in-the-middle": "^2.0.6", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.59.0.tgz", + "integrity": "sha512-xscSgOJA+GHphESDZxBHNk/zjNaEgoeufMwmiqYdL+qM27Xw3BbR9vN6Ucbq9dW6Y+oYUPgTTj17qf+Za4+uzg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-lambda": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.64.0.tgz", + "integrity": "sha512-vYhM/a8fG34/Dl/Q9gfv5Ih3OFPgqeyn79S8FN+Xs/QZw6h6L8a1lDa3CyigyicOXLCmVIM7Fc9vFD4BGqgGLA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/aws-lambda": "^8.10.155" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-sdk": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.67.0.tgz", + "integrity": "sha512-btpwJnZ2RBXDh/pTpfVpInpBu9Pedi+lbLKbt3naB344SggbbYnIdT7u8EzmGIApWi9EV91vw7hm896I7nESQA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.57.0.tgz", + "integrity": "sha512-W4zLz1Y9ptCsdL+QMXR7xQaBHkJivLBmVlLCjUe23rX4V8E65fGAtlIJSKTKAfz4aEgtWgQAGMdkeqACwG0Caw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.212.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@types/bunyan": "1.8.11" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cassandra-driver": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.57.0.tgz", + "integrity": "sha512-xLwrK+XnN32IB5i6t/a2j+SVdjlq/BIgjpVRkke4HAsKjoSMy1GeSI+ZOiJffRLFb4MojcvH4RG2+nEg1uC6Zg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.55.0.tgz", + "integrity": "sha512-UfGw7ubKKZBoTRjxi5KlfeECEaXZinS20RdRNlZE5tVF+O17hJOnrcGwAoQAHp6eYmxI2jW9IQ4t6450gnNF9g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cucumber": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.28.0.tgz", + "integrity": "sha512-kim+bRxu4LZqKEyF2SgO01tgG88W+/iYltyP1XjT31FIXzlBjzQpwtSLLM8byayO85mcZIBha54WSNFDLM/7qQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.29.0.tgz", + "integrity": "sha512-220WjRb1G1UiAKbVblSMxwxxFdpyB4wj1XYIO9BJs5r62Azj2dL5fyZiXK3/WO6wB3uLul9R946iKI1bpPxktQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dns": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.55.0.tgz", + "integrity": "sha512-cfWLaFi22V+sQrKY7t6QroYzT3kO9m3PpkN1OXYmuCyfwxQaXOVlF8NSAHtua/RQYw0aQl+2fe6JOWyJdEZiwA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.60.0.tgz", + "integrity": "sha512-KghHCDqKq0D7iuPIVCuPSXut5WVAI6uwKcPrhwTUJL5VE2LC18id2vKoiAm1V8XvVlgIGAiECtEvbrFwkTCj3g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fastify": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.56.0.tgz", + "integrity": "sha512-zotOPoZsWtMF47BjottK23XaaBSmVuwG5D/R3FlGfAAwMNFoDR3IY1OGO9v9KfOU/1/xDVkxsQ22NFfu9lE8aA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.31.0.tgz", + "integrity": "sha512-C7tdXGDnkMgLVlE79VSekB+Y+P345zKUigvFMs5M7U0GIYA8ERx3FS0aAcY/ICIq9YwRmH2uuMb++Br5M2vNUg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.55.0.tgz", + "integrity": "sha512-7hWiyLbEX/dIS4LZy/h8VaAQPs8oBeEqsrysDWbos0b9PF414L6Rsbi2um/omtxIs+GTvsbuqDscWigeaxyWdA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.60.0.tgz", + "integrity": "sha512-XPATrmxAd2tFCsYbJ3eVIXt+gyvMKjc36QQuQxjtssMnAbw006Le9b5lKs7WXik7ItOpM1exATi1aDdOcCjRRg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-grpc": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.212.0.tgz", + "integrity": "sha512-r1t7LNKWVhSQMUrBdDJtooFmmLZ93kGuFixqeXPoUP8W+chJCxhey9l0c0+L3xriNdyB7TzvkKHhPXUDevgVEA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "0.212.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.58.0.tgz", + "integrity": "sha512-reuRApR2KHm2VsfyDgsrLhNE+IOy4uIU6n3oMjUleReHacEEZmf4vXxdt4/qcmJ6GoUXnRN2AOu3s5N3pMrgYA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.212.0.tgz", + "integrity": "sha512-t2nt16Uyv9irgR+tqnX96YeToOStc3X5js7Ljn3EKlI2b4Fe76VhMkTXtsTQ0aId6AsYgefrCRnXSCo/Fn/vww==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/instrumentation": "0.212.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.60.0.tgz", + "integrity": "sha512-R+nnbPD9l2ruzu248qM3YDWzpdmWVaFFFv08lQqsc0EP4pT/B1GGUg06/tHOSo3L5njB2eejwyzpkvJkjaQEMA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/redis-common": "^0.38.2", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.21.0.tgz", + "integrity": "sha512-lkLrILnKGO7SHw1xPJnuGx2S4XwbKmQiJyzUGuEImRoU/6Gj0Nka0lkbeRd4ANN20dxr/mLdXIsUsk6DzTrX6A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.56.0.tgz", + "integrity": "sha512-pKqtY5lbAQ70MC5K/BJeAA1t2gAUlRBZBAJ5ergRUNs5jw8zbdOXEZOLztiuNvQqD2z4a9N0Tkde9JMFm2pKMQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.33.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.60.0.tgz", + "integrity": "sha512-UOmu2y2LHgPzKsm9xd0sCQJimr11YP4MKFc190Do1ufd8qds7Zd5BI3f6TudqYhH9dUIhojsQyUaS6K4nv5FsQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.56.0.tgz", + "integrity": "sha512-vXtOValhKRgWA9tLAiTU3P37Q31OveRuM2N5iLSVHl4GzkMBQ5p50A9kSKvt5gReL6BzFDXPCM9ItJiAhSS2KQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-memcached": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.55.0.tgz", + "integrity": "sha512-kdhW/j5X+vNCAvHVc50PZfvE7diUScg1ZkBaNFRygY3Z6IUjgPLR0luWQMDPSFun6AVo1HaMDPxbUqJrot6qrA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/memcached": "^2.2.6" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.65.0.tgz", + "integrity": "sha512-hOAJRs5vrY7fZolSYUXmf29Y+HFDHWrek0DeLq82uwMPjPSda7h6oumQnqEX5olzw357q/QG39/uJdkclJ/JUg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.58.0.tgz", + "integrity": "sha512-3L0Fqo1y2oreISFPWaqdt/bg3NhLgrkn5U/E/9RNG1QaM81drTMBCHseMY1q8SlejjE43ZWOy+0KbmRBlUPJ+g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.58.0.tgz", + "integrity": "sha512-wZDrBCL3WfJclV6KywWVV3/B2ZiUYmDQdgyu3pq4jK/5qSfoDmezHzT/Nayln5MVVWMAGXIMLrCj8BKa6jaKQQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.58.0.tgz", + "integrity": "sha512-EubjV1XZb7XHrENqF7TW2lnah+KN0LddMneKNAB8PjGVKL5lJkVV/vhJ6EIcUNn9nCWmAwZ3GRcFVEDKCnyXfQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@opentelemetry/sql-common": "^0.41.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-nestjs-core": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.58.0.tgz", + "integrity": "sha512-0lE9oW8j6nmvBHJoOxIQgKzMQQYNfX1nhiWZdXD0sNAMFsWBtvECWS7NAPSroKrEP53I04TcHCyyhcK4I9voXg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-net": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.56.0.tgz", + "integrity": "sha512-h69x7U6f86mP3gGWWTaMkQZk0K3tBvpVMIU7E0q2kkVw6eZ5TqFm9rkaEy38moQmixiDFQ9j/2/cwxG9P7ZEeA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-openai": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-openai/-/instrumentation-openai-0.10.0.tgz", + "integrity": "sha512-0lV2zxge2mMaruVCw/bmypWVu+aJ76rc0HBvAVFCPUI3zzJdgBZJZafGIHZ1IB2F6VvrDFL+JstEnle6V8brvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.212.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-oracledb": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.37.0.tgz", + "integrity": "sha512-OzMghtAEAEkXlkUrZI4QcXSZq0MILeU6WC0/N5+1MSkuIkruIeaRw99/RtyS2of8vlPDa8XbbXl32Q1RM3wSyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@types/oracledb": "6.5.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.64.0.tgz", + "integrity": "sha512-NbfB/rlfsRI3zpTjnbvJv3qwuoGLsN8FxR/XoI+ZTn1Rs62x1IenO+TSSvk4NO+7FlXpd2MiOe8LT/oNbydHGA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.41.2", + "@types/pg": "8.15.6", + "@types/pg-pool": "2.0.7" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pino": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.58.0.tgz", + "integrity": "sha512-rgy+tA7cDjuSq6dXAO40OiYP25azIDHMBtxG3RzSmCBVEYdjggl6btyuLVasX6VkOOhP2gf6PBuLMNxVwaIqAw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.212.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.60.0.tgz", + "integrity": "sha512-Ea/GffmmzIVHc9geaMjT94IR7poVZzIv4Kk/Lw0tbxGD3cBYcMUsLFVajKxpZsE1NRCECFpidAWeifCIKD0inw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/redis-common": "^0.38.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-restify": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.57.0.tgz", + "integrity": "sha512-kO6MsZFU+RdXOKhsKw8SOSBYGYCdFSlza+mpBQRl1DQmveZcnidchv4V5JQPtNgHxCGH+1n3hDpLdxdGUbJPNA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-router": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.56.0.tgz", + "integrity": "sha512-PHECDGQElLazI/QbHU16C5m9fDC7DGJk+jLIwO5ca6bcp7bXhUPPUTT78l7da2pDsrz4mhv5ytYNZmBbW/Q3rA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-runtime-node": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.25.0.tgz", + "integrity": "sha512-XaCmwBSui5KeTn8M6OzaEn1rEsNWtUkjuc1ylg0tqQTLHibNQ0n7f8v4zdF6x/nBV1OnsiYlN8RLHauGemv/TA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-socket.io": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.59.0.tgz", + "integrity": "sha512-71DnM/FEqH0PjvU2uZvzWJeaGyVIy3rJKk8rZrxg/aS2QT3qLGb+UPL/B+1vOw4pzDPn4papLTSMpLVF9G8uvw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.31.0.tgz", + "integrity": "sha512-HoF2EtcyP3JR4R3jLPHohZ9lFcj1QLJyGmFfLKDTvUUjPiFuK4XZ6L1OV9HhaqvN0xY+tWKfNdCPS3r33rd0Xw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.22.0.tgz", + "integrity": "sha512-yb6vEWUPOrD5i7yR1XceEEqiVHbMgr5YnUPnom5eQVCjvrTkEVswyrf9i+vvJR+28wrNqILIIphWgOOx6BjnTQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.56.0.tgz", + "integrity": "sha512-ITIA0Qe61CQ6FQU/bN23pNBvJ+5U0ofoASMOOYrODtXyV9wI267AigNTTwDmv2Myt8dPEFvvVFJZKhiZLIpehA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.212.0", + "@opentelemetry/instrumentation": "^0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.212.0.tgz", + "integrity": "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/otlp-transformer": "0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.212.0.tgz", + "integrity": "sha512-YidOSlzpsun9uw0iyIWrQp6HxpMtBlECE3tiHGAsnpEqJWbAUWcMnIffvIuvTtTQ1OyRtwwaE79dWSQ8+eiB7g==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/otlp-exporter-base": "0.212.0", + "@opentelemetry/otlp-transformer": "0.212.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.212.0.tgz", + "integrity": "sha512-bj7zYFOg6Db7NUwsRZQ/WoVXpAf41WY2gsd3kShSfdpZQDRKHWJiRZIg7A8HvWsf97wb05rMFzPbmSHyjEl9tw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.212.0", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-logs": "0.212.0", + "@opentelemetry/sdk-metrics": "2.5.1", + "@opentelemetry/sdk-trace-base": "2.5.1", + "protobufjs": "8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/protobufjs": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.0.tgz", + "integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.5.1.tgz", + "integrity": "sha512-AU6sZgunZrZv/LTeHP+9IQsSSH5p3PtOfDPe8VTdwYH69nZCfvvvXehhzu+9fMW2mgJMh5RVpiH8M9xuYOu5Dg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.5.1.tgz", + "integrity": "sha512-8+SB94/aSIOVGDUPRFSBRHVUm2A8ye1vC6/qcf/D+TF4qat7PC6rbJhRxiUGDXZtMtKEPM/glgv5cBGSJQymSg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", + "integrity": "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.33.2.tgz", + "integrity": "sha512-EaS54zwYmOg9Ttc79juaktpCBYqyh2IquXl534sLls+c1/pc8LZfWPMqytFt+iBvSPQ6ajraUnvi6cun4AhSjQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-aws": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.12.0.tgz", + "integrity": "sha512-VelueKblsnQEiBVqEYcvM9VEb+B8zN6nftltdO9HAD7qi/OlicP4z/UGJ9EeW2m++WabdMoj0G3QVL8YV0P9tw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-azure": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.20.0.tgz", + "integrity": "sha512-iRy+O2cB6DOlQ/OONaK+L8Cp8nLS89dZVRp6KgnFAfzykXuq9Ws/ygJKcU3CCmjkgY5j2Vk3uVTre/E35bWhYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-container": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.8.3.tgz", + "integrity": "sha512-5J0JP2cy655rBKM9Doz26ffO3rG+Xqm7OXeNXkckzmc3JmL6Bj3dPBKugPYsfemhEIqtf7INH9UmPQqTMuWoHg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-gcp": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.47.0.tgz", + "integrity": "sha512-57T/kRVdU0ch1P4KPEkmU2b5mWNlUs8hHgqrBYVF+fNZMc1jMdL1mANZhEzoLtWKIeoCEy+57Itt7RkXAYNJiQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "gcp-metadata": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz", + "integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.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-logs": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.212.0.tgz", + "integrity": "sha512-qglb5cqTf0mOC1sDdZ7nfrPjgmAqs2OxkzOPIf2+Rqx8yKBK0pS7wRtB1xH30rqahBIut9QJDbDePyvtyqvH/Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.212.0", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/resources": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.1.tgz", + "integrity": "sha512-RKMn3QKi8nE71ULUo0g/MBvq1N4icEBo7cQSKnL3URZT16/YH3nSVgWegOjwx7FRBTrjOIkMJkCUn/ZFIEfn4A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/resources": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.212.0.tgz", + "integrity": "sha512-tJzVDk4Lo44MdgJLlP+gdYdMnjxSNsjC/IiTxj5CFSnsjzpHXwifgl3BpUX67Ty3KcdubNVfedeBc/TlqHXwwg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.212.0", + "@opentelemetry/configuration": "0.212.0", + "@opentelemetry/context-async-hooks": "2.5.1", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/exporter-logs-otlp-grpc": "0.212.0", + "@opentelemetry/exporter-logs-otlp-http": "0.212.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.212.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.212.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.212.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.212.0", + "@opentelemetry/exporter-prometheus": "0.212.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.212.0", + "@opentelemetry/exporter-trace-otlp-http": "0.212.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.212.0", + "@opentelemetry/exporter-zipkin": "2.5.1", + "@opentelemetry/instrumentation": "0.212.0", + "@opentelemetry/propagator-b3": "2.5.1", + "@opentelemetry/propagator-jaeger": "2.5.1", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/sdk-logs": "0.212.0", + "@opentelemetry/sdk-metrics": "2.5.1", + "@opentelemetry/sdk-trace-base": "2.5.1", + "@opentelemetry/sdk-trace-node": "2.5.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-base": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.1.tgz", + "integrity": "sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, "engines": { - "node": ">= 12" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-musl": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz", - "integrity": "sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==", - "license": "MIT", - "optional": true, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.5.1.tgz", + "integrity": "sha512-9lopQ6ZoElETOEN0csgmtEV5/9C7BMfA7VtF4Jape3i954b6sTY2k3Xw3CxUTKreDck/vpAuJM+EDo4zheUw+A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.5.1", + "@opentelemetry/core": "2.5.1", + "@opentelemetry/sdk-trace-base": "2.5.1" + }, "engines": { - "node": ">= 12" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@nomicfoundation/solidity-analyzer-win32-x64-msvc": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz", - "integrity": "sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==", - "license": "MIT", - "optional": true, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz", + "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==", + "license": "Apache-2.0", "engines": { - "node": ">= 12" + "node": ">=14" } }, - "node_modules/@nuxtjs/opencollective": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", - "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", - "license": "MIT", + "node_modules/@opentelemetry/sql-common": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", + "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", + "license": "Apache-2.0", "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.0", - "node-fetch": "^2.6.1" - }, - "bin": { - "opencollective": "bin/opencollective.js" + "@opentelemetry/core": "^2.0.0" }, "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" } }, "node_modules/@paralleldrive/cuid2": { @@ -4097,35 +5484,30 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "dev": true, "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==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", @@ -4136,35 +5518,30 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "dev": true, "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==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "dev": true, "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==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@redis/bloom": { @@ -4648,6 +6025,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/aws-lambda": { + "version": "8.10.160", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.160.tgz", + "integrity": "sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA==", + "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", @@ -4714,6 +6097,15 @@ "@types/node": "*" } }, + "node_modules/@types/bunyan": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", + "integrity": "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/compression": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", @@ -4729,7 +6121,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -4932,6 +6323,15 @@ "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", "license": "MIT" }, + "node_modules/@types/memcached": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz", + "integrity": "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -4969,6 +6369,15 @@ "@types/express": "*" } }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.19.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", @@ -4985,6 +6394,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/oracledb": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.5.2.tgz", + "integrity": "sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/passport": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", @@ -5030,6 +6448,26 @@ "@types/passport": "*" } }, + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.7.tgz", + "integrity": "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -5151,6 +6589,15 @@ "@types/superagent": "*" } }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -5562,6 +7009,16 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@willsoto/nestjs-prometheus": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@willsoto/nestjs-prometheus/-/nestjs-prometheus-6.0.2.tgz", + "integrity": "sha512-ePyLZYdIrOOdlOWovzzMisIgviXqhPVzFpSMKNNhn6xajhRHeBsjAzSdpxZTc6pnjR9hw1lNAHyKnKl7lAPaVg==", + "license": "Apache-2.0", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "prom-client": "^15.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5608,7 +7065,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5617,6 +7073,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -6400,6 +7865,15 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6412,6 +7886,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -7858,6 +9338,15 @@ "node": ">=8" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -8977,6 +10466,12 @@ "dev": true, "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -9133,6 +10628,29 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -9480,6 +10998,18 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", @@ -9505,6 +11035,12 @@ "node": ">= 0.6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, "node_modules/fp-ts": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz", @@ -9554,6 +11090,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -9787,7 +11392,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -9827,7 +11431,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -9889,6 +11492,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -10484,6 +12096,24 @@ "node": ">=4" } }, + "node_modules/import-in-the-middle": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + } + }, + "node_modules/import-in-the-middle/node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -11866,6 +13496,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -12503,7 +14142,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, "license": "MIT" }, "node_modules/lodash.defaults": { @@ -12885,7 +14523,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/lru_map": { @@ -13430,6 +15067,12 @@ "node": ">=10" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -13561,6 +15204,26 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -14499,6 +16162,19 @@ "dev": true, "license": "MIT" }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -14559,7 +16235,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -15175,6 +16850,19 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.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", @@ -15285,7 +16973,6 @@ "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "dev": true, "license": "ISC", "dependencies": { "glob": "^10.3.7" @@ -16352,6 +18039,15 @@ "streamx": "^2.15.0" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/telejson": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz", @@ -17500,6 +19196,15 @@ "defaults": "^1.0.3" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/web3": { "version": "4.16.0", "resolved": "https://registry.npmjs.org/web3/-/web3-4.16.0.tgz", @@ -19006,7 +20711,6 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index dec3d80..d382439 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,12 @@ "@nestjs/throttler": "^5.1.1", "@nestjs/typeorm": "^10.0.2", "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@opentelemetry/auto-instrumentations-node": "^0.70.1", + "@opentelemetry/resources": "^2.5.1", + "@opentelemetry/sdk-node": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.39.0", "@prisma/client": "^6.19.2", + "@willsoto/nestjs-prometheus": "^6.0.2", "axios": "^1.6.2", "bcrypt": "^6.0.0", "bull": "^4.12.2", @@ -100,6 +105,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.11.3", + "prom-client": "^15.1.3", "qrcode": "^1.5.4", "redis": "^4.6.12", "reflect-metadata": "^0.1.13", From 06d666bba0d92d6e1a159ae3ce813979fb968774 Mon Sep 17 00:00:00 2001 From: akargi Date: Wed, 25 Feb 2026 22:44:54 +0100 Subject: [PATCH 4/4] fix: resolve all remaining lint and syntax errors for full clean build --- .eslintrc.js | 26 +- .github/workflows/ci.yml | 4 +- .github/workflows/comprehensive-testing.yml | 4 +- CONTRIBUTING.md | 28 + OBSERVABILITY_README.md | 3 + PAGINATION_IMPLEMENTATION.md | 42 +- SECURITY.md | 24 +- SECURITY_IMPLEMENTATION_SUMMARY.md | 23 +- SETUP.md | 13 +- docker-compose.yml | 16 +- docs/DATABASE_SCHEMA.md | 8 + docs/properties-api.md | 65 +- docs/valuation-api.md | 31 +- jest.config.js | 18 +- prisma/seed.ts | 668 +++++++++--------- src/api-keys/README.md | 18 +- src/api-keys/SETUP.md | 13 +- src/auth/auth.controller.ts | 221 +++--- src/auth/auth.service.ts | 12 +- src/blockchain/blockchain.module.ts | 2 +- src/blockchain/blockchain.service.ts | 2 +- src/blockchain/enums/supported-chain.enum.ts | 2 +- src/blockchain/providers/provider.factory.ts | 6 +- src/common/pagination/PAGINATION.md | 67 +- src/common/pagination/PAGINATION_GUIDE.md | 72 +- src/database/prisma/prisma.service.ts | 1 - src/properties/properties.service.ts | 1 - src/security/README.md | 19 +- src/transactions/transactions.controller.ts | 6 +- src/transactions/transactions.service.ts | 83 ++- src/users/user.service.ts | 54 +- test/auth/mfa.service.spec.ts | 51 +- test/auth/security.e2e-spec.ts | 110 ++- test/documents/document.controller.spec.ts | 25 +- test/documents/document.service.spec.ts | 25 +- test/e2e-setup.ts | 70 +- test/integration-setup.ts | 34 +- test/jest-e2e.json | 9 +- test/performance-setup.ts | 92 +-- test/properties/properties.controller.spec.ts | 3 +- test/properties/properties.service.spec.ts | 2 +- test/security-setup.ts | 91 ++- test/setup.ts | 34 +- 43 files changed, 1151 insertions(+), 947 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c6142ab..6f2efef 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,10 +6,7 @@ module.exports = { sourceType: 'module', }, plugins: ['@typescript-eslint/eslint-plugin'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], root: true, env: { node: true, @@ -35,24 +32,27 @@ module.exports = { 'prefer-arrow-callback': 'error', 'arrow-spacing': 'error', 'comma-dangle': ['error', 'always-multiline'], - 'semi': ['error', 'always'], - 'quotes': 'off', - 'indent': 'off', + semi: ['error', 'always'], + quotes: 'off', + indent: 'off', '@typescript-eslint/indent': 'off', 'max-len': 'off', 'eol-last': 'error', 'no-trailing-spaces': 'error', 'padded-blocks': ['error', 'never'], - 'space-before-function-paren': ['error', { - 'anonymous': 'always', - 'named': 'never', - 'asyncArrow': 'always' - }], + 'space-before-function-paren': [ + 'error', + { + anonymous: 'always', + named: 'never', + asyncArrow: 'always', + }, + ], 'keyword-spacing': 'error', 'space-infix-ops': 'error', 'object-curly-spacing': ['error', 'always'], 'array-bracket-spacing': ['error', 'never'], 'brace-style': ['error', '1tbs'], - 'curly': 'error' + curly: 'error', }, }; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2079dc..c4fb262 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: test: name: Test and Quality Checks runs-on: ubuntu-latest - + services: postgres: image: postgres:15 @@ -29,7 +29,7 @@ jobs: --health-retries 5 ports: - 5432:5432 - + redis: image: redis:7 options: >- diff --git a/.github/workflows/comprehensive-testing.yml b/.github/workflows/comprehensive-testing.yml index 8798e41..0a44d65 100644 --- a/.github/workflows/comprehensive-testing.yml +++ b/.github/workflows/comprehensive-testing.yml @@ -2,9 +2,9 @@ name: Comprehensive Testing Pipeline on: push: - branches: [ main, develop, 'feature/*' ] + branches: [main, develop, 'feature/*'] pull_request: - branches: [ main, develop ] + branches: [main, develop] env: NODE_VERSION: '18' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57e742e..c92d431 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,7 @@ We welcome contributions from the community! This guide will help you get starte ### Prerequisites Before contributing, ensure you have: + - **Node.js** v18+ (LTS recommended) - **PostgreSQL** v14+ - **Redis** (for caching) @@ -20,23 +21,27 @@ Before contributing, ensure you have: ### Development Setup 1. **Fork and Clone** + ```bash git clone https://github.com/MettaChain/PropChain-BackEnd.git cd PropChain-BackEnd ``` 2. **Install Dependencies** + ```bash npm install ``` 3. **Environment Setup** + ```bash cp .env.example .env # Configure your local environment variables ``` 4. **Database Setup** + ```bash createdb propchain_dev npm run migrate @@ -67,11 +72,13 @@ Before contributing, ensure you have: ### 🔧 Code Contributions #### 1. Create an Issue + - Search existing issues to avoid duplicates - Create a new issue describing your proposed changes - Wait for maintainer approval before starting work #### 2. Set Up Your Branch + ```bash git checkout -b feature/your-feature-name # or @@ -81,12 +88,14 @@ git checkout -b fix/your-bug-fix #### 3. Development Guidelines ##### Code Style + - Follow **ESLint** and **Prettier** configurations - Use **TypeScript** strict mode - Write **descriptive commit messages** - Keep functions small and focused ##### Code Structure + ``` src/ ├── controllers/ # API route handlers @@ -99,6 +108,7 @@ src/ ``` ##### Best Practices + - **Error Handling**: Use proper HTTP status codes and error responses - **Validation**: Validate all input data using DTOs - **Security**: Sanitize inputs and implement proper authentication @@ -121,6 +131,7 @@ npm run test:coverage ``` **Testing Requirements:** + - Unit tests for all new functions - Integration tests for API endpoints - E2E tests for critical user flows @@ -129,6 +140,7 @@ npm run test:coverage #### 5. Smart Contract Contributions For smart contract changes: + ```bash # Compile contracts npm run compile:contracts @@ -141,6 +153,7 @@ npm run deploy:testnet ``` **Contract Guidelines:** + - Follow **Solidity** or **Rust** best practices - Include comprehensive test coverage - Add inline documentation @@ -160,6 +173,7 @@ test(contracts): add ERC721 tokenization tests ``` **Types:** + - `feat`: New feature - `fix`: Bug fix - `docs`: Documentation changes @@ -176,6 +190,7 @@ test(contracts): add ERC721 tokenization tests - Code comments for complex logic 2. **Run Full Test Suite** + ```bash npm run test:all npm run lint @@ -189,22 +204,27 @@ test(contracts): add ERC721 tokenization tests - Add testing instructions 4. **PR Template** + ```markdown ## Description + Brief description of changes ## Type of Change + - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Testing + - [ ] Unit tests pass - [ ] Integration tests pass - [ ] Manual testing completed ## Checklist + - [ ] Code follows style guidelines - [ ] Self-review completed - [ ] Documentation updated @@ -243,12 +263,14 @@ test(contracts): add ERC721 tokenization tests ## 📚 Development Resources ### Documentation + - [API Documentation](./docs/api.md) - [Architecture Guide](./docs/architecture.md) - [Smart Contract Docs](./docs/contracts.md) - [Deployment Guide](./docs/deployment.md) ### Tools & Extensions + - **VS Code Extensions**: - TypeScript Hero - ESLint @@ -257,6 +279,7 @@ test(contracts): add ERC721 tokenization tests - Thunder Client (API testing) ### Learning Resources + - [NestJS Documentation](https://docs.nestjs.com/) - [TypeScript Handbook](https://www.typescriptlang.org/docs/) - [Ethereum Development Guide](https://ethereum.org/en/developers/) @@ -285,6 +308,7 @@ We are committed to providing a welcoming and inclusive environment. Please read ### Contributors We value all contributions! Contributors will be: + - Listed in our README - Mentioned in release notes - Invited to our contributor Discord channel @@ -303,6 +327,7 @@ We value all contributions! Contributors will be: ### Version Management We follow [Semantic Versioning](https://semver.org/): + - **MAJOR**: Breaking changes - **MINOR**: New features (backward compatible) - **PATCH**: Bug fixes (backward compatible) @@ -331,6 +356,7 @@ We follow [Semantic Versioning](https://semver.org/): ### Common Issues **Database Connection Errors** + ```bash # Check PostgreSQL status pg_ctl status @@ -340,6 +366,7 @@ npm run db:reset ``` **Smart Contract Compilation** + ```bash # Clear cache npm run clean @@ -347,6 +374,7 @@ npm run compile:contracts ``` **Test Failures** + ```bash # Clear Jest cache npm run test:clear diff --git a/OBSERVABILITY_README.md b/OBSERVABILITY_README.md index d9edd52..f02607e 100644 --- a/OBSERVABILITY_README.md +++ b/OBSERVABILITY_README.md @@ -3,6 +3,7 @@ This module introduces advanced monitoring and observability for the PropChain-BackEnd, supporting production-grade operations and troubleshooting. ## Features + - **Distributed Tracing:** OpenTelemetry-based tracing with correlation ID propagation. - **Metrics Collection:** Prometheus metrics for HTTP requests (latency, count, status). - **Centralized Logging:** Winston-based structured logging, ready for aggregation. @@ -11,12 +12,14 @@ This module introduces advanced monitoring and observability for the PropChain-B - **Extensible:** Ready for integration with Sentry (error tracking), Grafana/Kibana (dashboards), and anomaly detection tools. ## Usage + - All HTTP requests are traced and measured automatically. - Metrics are exposed at `/metrics` (Prometheus scrape endpoint). - Logs are structured and can be forwarded to ELK, Azure Monitor, or similar. - Health endpoints remain at `/health`, `/health/detailed`, `/health/liveness`, `/health/readiness`. ## Next Steps + - Configure Winston transports for log aggregation. - Integrate Sentry or similar for error tracking. - Add Grafana/Kibana dashboard templates. diff --git a/PAGINATION_IMPLEMENTATION.md b/PAGINATION_IMPLEMENTATION.md index 7d860d7..7053866 100644 --- a/PAGINATION_IMPLEMENTATION.md +++ b/PAGINATION_IMPLEMENTATION.md @@ -7,17 +7,20 @@ A professional, enterprise-grade pagination system has been successfully impleme ## 📁 Files Created ### Core Pagination Module + - [src/common/pagination/pagination.dto.ts](src/common/pagination/pagination.dto.ts) - DTOs with validation - [src/common/pagination/pagination.service.ts](src/common/pagination/pagination.service.ts) - Core service logic - [src/common/pagination/index.ts](src/common/pagination/index.ts) - Module exports - [src/common/pagination/PAGINATION_GUIDE.md](src/common/pagination/PAGINATION_GUIDE.md) - Comprehensive documentation ### Tests + - [test/pagination/pagination.service.spec.ts](test/pagination/pagination.service.spec.ts) - Unit tests (80+ test cases) - [test/pagination/pagination.integration.spec.ts](test/pagination/pagination.integration.spec.ts) - Integration tests - [test/pagination/pagination.performance.ts](test/pagination/pagination.performance.ts) - Performance benchmarks ### Updated Files + - [src/api-keys/api-key.service.ts](src/api-keys/api-key.service.ts) - Added pagination support - [src/api-keys/api-key.controller.ts](src/api-keys/api-key.controller.ts) - Added pagination query support - [src/api-keys/api-keys.module.ts](src/api-keys/api-keys.module.ts) - Added PaginationService provider @@ -25,29 +28,35 @@ A professional, enterprise-grade pagination system has been successfully impleme ## 🎯 Acceptance Criteria - All Met ✅ **Create pagination DTO with page, limit, and sort parameters** + - PaginationQueryDto with validation - Supports page (1-indexed), limit (1-100), sortBy, sortOrder ✅ **Implement pagination helper service** + - PaginationService with 7 core methods - calculatePagination, createMetadata, formatResponse, etc. - Reusable across all list endpoints ✅ **Add pagination metadata to list responses** + - PaginationMetadataDto with 8 fields - total, page, limit, pages, hasNext, hasPrev, sortBy, sortOrder - Generic PaginatedResponseDto wrapper ✅ **Update all list endpoints to use pagination** + - API Keys endpoint fully implemented with pagination - Template for other endpoints provided ✅ **Add pagination validation and limits** + - Min/max validation with sensible defaults - Hard limit of 100 items per page - Automatic parameter normalization ✅ **Unit tests for pagination logic** + - 80+ unit test cases covering: - Pagination calculation - Metadata generation @@ -55,12 +64,14 @@ A professional, enterprise-grade pagination system has been successfully impleme - Edge cases and validation ✅ **Integration tests for paginated endpoints** + - API integration tests - Data consistency verification - Sorting and filtering validation - Edge case handling ✅ **Performance tests for large datasets** + - Benchmarks for all core operations - Tests with datasets from 0 to 1,000,000 items - Performance metrics (operations/second) @@ -68,18 +79,20 @@ A professional, enterprise-grade pagination system has been successfully impleme ## 📊 Key Features ### Query Parameters + ``` GET /api-keys?page=1&limit=10&sortBy=createdAt&sortOrder=desc ``` -| Parameter | Type | Default | Range | -|-----------|------|---------|-------| -| page | int | 1 | 1-∞ | -| limit | int | 10 | 1-100 | -| sortBy | string | createdAt | Any field | -| sortOrder | enum | desc | asc, desc | +| Parameter | Type | Default | Range | +| --------- | ------ | --------- | --------- | +| page | int | 1 | 1-∞ | +| limit | int | 10 | 1-100 | +| sortBy | string | createdAt | Any field | +| sortOrder | enum | desc | asc, desc | ### Response Format + ```json { "data": [...], @@ -97,6 +110,7 @@ GET /api-keys?page=1&limit=10&sortBy=createdAt&sortOrder=desc ``` ### Service Methods + 1. **calculatePagination** - Get skip/take for database queries 2. **createMetadata** - Build pagination metadata 3. **formatResponse** - Wrap data with pagination info @@ -105,13 +119,14 @@ GET /api-keys?page=1&limit=10&sortBy=createdAt&sortOrder=desc ## 🧪 Test Coverage -| Test Suite | Count | Coverage | -|------------|-------|----------| -| Unit Tests | 80+ | Service logic, validation, edge cases | -| Integration Tests | 12+ | API endpoints, data consistency | -| Performance Tests | 6 | Benchmarks, large datasets | +| Test Suite | Count | Coverage | +| ----------------- | ----- | ------------------------------------- | +| Unit Tests | 80+ | Service logic, validation, edge cases | +| Integration Tests | 12+ | API endpoints, data consistency | +| Performance Tests | 6 | Benchmarks, large datasets | ### Running Tests + ```bash # Unit tests npm run test:unit -- test/pagination/pagination.service.spec.ts @@ -126,12 +141,14 @@ ts-node test/pagination/pagination.performance.ts ## 📈 Performance Metrics Expected performance (on typical hardware): + - **calculatePagination**: ~1.3M ops/second - **createMetadata**: ~800K ops/second - **formatResponse**: <0.1ms per call - **getPrismaOptions**: ~1.1M ops/second ### Large Dataset Handling + - 1,000 items: <1ms - 10,000 items: <1ms - 100,000 items: <1ms @@ -140,6 +157,7 @@ Expected performance (on typical hardware): ## 🔧 Usage Examples ### Basic Implementation + ```typescript async findAll(paginationQuery?: PaginationQueryDto) { const { skip, take, orderBy } = this.paginationService.getPrismaOptions( @@ -157,6 +175,7 @@ async findAll(paginationQuery?: PaginationQueryDto) { ``` ### Controller Integration + ```typescript @Get() async findAll(@Query() paginationQuery: PaginationQueryDto) { @@ -167,6 +186,7 @@ async findAll(@Query() paginationQuery: PaginationQueryDto) { ## 📚 Documentation Comprehensive documentation available in [PAGINATION_GUIDE.md](src/common/pagination/PAGINATION_GUIDE.md) including: + - Quick start guide - API reference - Implementation guide diff --git a/SECURITY.md b/SECURITY.md index 34e156f..1f780fe 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,23 +7,26 @@ This document describes the security enhancements implemented in the PropChain a ## Security Features Implemented ### 1. Console Logging Removal + - **Issue**: Sensitive information (tokens, emails) was exposed in production logs - **Solution**: Replaced all `console.log` statements with structured logging using `StructuredLoggerService` - **Files Modified**: `src/auth/auth.service.ts` ### 2. Token Blacklisting + - **Issue**: JWT tokens couldn't be revoked once issued - **Solution**: Implemented token blacklisting using Redis with automatic TTL expiration - **Features**: - Blacklist tokens on logout with proper TTL - JWT guard checks blacklisted tokens - Automatic cleanup of expired blacklisted tokens -- **Files Modified**: +- **Files Modified**: - `src/auth/auth.service.ts` - `src/auth/guards/jwt-auth.guard.ts` - `src/auth/auth.controller.ts` ### 3. Brute Force Protection + - **Issue**: No protection against password guessing attacks - **Solution**: Implemented login attempt tracking with account locking - **Features**: @@ -32,11 +35,12 @@ This document describes the security enhancements implemented in the PropChain a - Automatic lockout expiration - Exponential backoff for repeated failures - **Files Created**: `src/auth/guards/login-attempts.guard.ts` -- **Files Modified**: +- **Files Modified**: - `src/auth/auth.controller.ts` - `src/auth/auth.module.ts` ### 4. Enhanced Password Security + - **Issue**: Weak password requirements and no validation - **Solution**: Implemented comprehensive password validation and security policies - **Features**: @@ -44,15 +48,16 @@ This document describes the security enhancements implemented in the PropChain a - Password pattern validation (length, special chars, numbers, uppercase) - Common password pattern detection - Configurable bcrypt rounds -- **Files Created**: +- **Files Created**: - `src/common/validators/password.validator.ts` -- **Files Modified**: +- **Files Modified**: - `src/users/user.service.ts` - `src/users/users.module.ts` - `src/config/configuration.ts` - `src/config/interfaces/joi-schema-config.interface.ts` ### 5. Session Management + - **Issue**: No proper session tracking or management - **Solution**: Implemented Redis-based session management - **Features**: @@ -60,11 +65,12 @@ This document describes the security enhancements implemented in the PropChain a - Session timeout configuration - API endpoints for session management - Concurrent session limiting -- **Files Modified**: +- **Files Modified**: - `src/auth/auth.service.ts` - `src/auth/auth.controller.ts` ### 6. Multi-Factor Authentication (MFA) + - **Issue**: No additional authentication factors beyond password - **Solution**: Implemented TOTP-based MFA with backup codes - **Features**: @@ -108,6 +114,7 @@ SESSION_SECRET=your-session-secret-key-change-this-in-production ## API Endpoints ### Authentication + - `POST /auth/login` - Login with email/password (protected by brute force guard) - `POST /auth/web3-login` - Web3 wallet login - `POST /auth/logout` - Logout and blacklist current token @@ -118,11 +125,13 @@ SESSION_SECRET=your-session-secret-key-change-this-in-production - `GET /auth/verify-email/:token` - Verify email address ### Session Management + - `GET /auth/sessions` - Get all active sessions - `DELETE /auth/sessions/:sessionId` - Invalidate specific session - `DELETE /auth/sessions` - Invalidate all sessions ### MFA Management + - `POST /mfa/setup` - Generate MFA setup QR code - `POST /mfa/verify` - Verify and complete MFA setup - `GET /mfa/status` - Get MFA status @@ -133,10 +142,12 @@ SESSION_SECRET=your-session-secret-key-change-this-in-production ## Security Testing ### Unit Tests + - `test/auth/mfa.service.spec.ts` - MFA service unit tests - Password validation tests in user service tests ### E2E Tests + - `test/auth/security.e2e-spec.ts` - Comprehensive security tests including: - Token blacklisting - Brute force protection @@ -146,6 +157,7 @@ SESSION_SECRET=your-session-secret-key-change-this-in-production ## Redis Schema ### Security Keys + ``` # Login Attempts login_attempts:{email} -> {count} (expires after lockout duration) @@ -190,4 +202,4 @@ mfa_verified:{userId}:{token} -> {1} (prevents replay attacks, expires after tim 3. **Adaptive authentication** - Risk-based authentication decisions 4. **Security headers** - Implement additional HTTP security headers 5. **Audit logging** - Comprehensive security event logging -6. **Compliance reporting** - Generate security compliance reports \ No newline at end of file +6. **Compliance reporting** - Generate security compliance reports diff --git a/SECURITY_IMPLEMENTATION_SUMMARY.md b/SECURITY_IMPLEMENTATION_SUMMARY.md index a467afb..0e6170b 100644 --- a/SECURITY_IMPLEMENTATION_SUMMARY.md +++ b/SECURITY_IMPLEMENTATION_SUMMARY.md @@ -7,6 +7,7 @@ This branch implements comprehensive API rate limiting and security features for ## Features Implemented ### 1. Advanced Rate Limiting System + - **Location**: `src/security/services/rate-limiting.service.ts` - **Features**: - Redis-based sliding window rate limiting @@ -16,6 +17,7 @@ This branch implements comprehensive API rate limiting and security features for - Fail-open design for service resilience ### 2. IP Blocking and Whitelisting + - **Location**: `src/security/services/ip-blocking.service.ts` - **Features**: - Automatic IP blocking after failed attempts @@ -25,6 +27,7 @@ This branch implements comprehensive API rate limiting and security features for - Automatic unblocking of expired blocks ### 3. DDoS Protection + - **Location**: `src/security/services/ddos-protection.service.ts` - **Features**: - Real-time traffic monitoring @@ -34,6 +37,7 @@ This branch implements comprehensive API rate limiting and security features for - Configurable thresholds and response actions ### 4. API Quota Management + - **Location**: `src/security/services/api-quota.service.ts` - **Features**: - Plan-based quotas (Free, Basic, Pro, Enterprise) @@ -43,6 +47,7 @@ This branch implements comprehensive API rate limiting and security features for - Quota enforcement in API key validation ### 5. Security Headers + - **Location**: `src/security/services/security-headers.service.ts` - **Features**: - Content Security Policy (CSP) with customizable directives @@ -52,6 +57,7 @@ This branch implements comprehensive API rate limiting and security features for - Environment-specific configurations ### 6. Enhanced Authentication Security + - **Location**: `src/common/guards/api-key.guard.ts` (enhanced) - **Features**: - Enhanced API key guard with quota and rate limit checking @@ -60,6 +66,7 @@ This branch implements comprehensive API rate limiting and security features for - Detailed security headers in responses ### 7. Security Infrastructure + - **Location**: `src/security/` - **Components**: - Security module with all services @@ -71,18 +78,21 @@ This branch implements comprehensive API rate limiting and security features for ## Configuration Updates ### Environment Variables Added + - Advanced rate limiting configurations - IP blocking thresholds and durations - DDoS protection settings - Security headers configuration ### Configuration Files Updated + - `.env.example` - Added new security variables - `src/config/validation/config.validation.ts` - Added validation schemas ## API Endpoints ### Security Management Endpoints + - Rate limit management - IP blocking/unblocking - Whitelist management @@ -93,11 +103,13 @@ This branch implements comprehensive API rate limiting and security features for ## Integration Points ### Main Application + - Security module integrated into `AppModule` - Security middleware available for global application - Enhanced API key guard for route protection ### Existing Modules Enhanced + - API key validation now includes quota checking - Rate limiting integrated into authentication flow - Security headers applied globally @@ -105,16 +117,19 @@ This branch implements comprehensive API rate limiting and security features for ## Testing ### Unit Tests + - Rate limiting service tests - IP blocking service tests ### Integration Tests + - Security endpoints integration tests (placeholder) - Rate limiting headers tests ## Documentation ### Comprehensive Documentation + - `src/security/README.md` - Detailed feature documentation - Inline code comments and JSDoc - Configuration examples @@ -123,16 +138,19 @@ This branch implements comprehensive API rate limiting and security features for ## Key Design Principles ### Fail-Safe Design + - Services fail open to prevent service disruption - Redis failures don't block legitimate requests - Graceful degradation when security services are unavailable ### Performance Considerations + - Redis-based implementation for high performance - Efficient data structures for rate limiting - Minimal overhead on request processing ### Security Best Practices + - Defense in depth approach - Multiple layers of protection - Comprehensive logging and monitoring @@ -141,11 +159,13 @@ This branch implements comprehensive API rate limiting and security features for ## Deployment Notes ### Requirements + - Redis server for rate limiting and security state - Proper environment variable configuration - Updated `.env` file with new security settings ### Migration + - Backward compatible with existing API key system - No breaking changes to existing endpoints - New security features can be enabled gradually @@ -153,6 +173,7 @@ This branch implements comprehensive API rate limiting and security features for ## Future Enhancements ### Planned Improvements + - Machine learning-based anomaly detection - Geographic IP blocking - Request fingerprinting @@ -200,4 +221,4 @@ src/ .env.example (added security configuration variables) ``` -This implementation provides a production-ready, comprehensive security system that protects against various attack vectors while maintaining high performance and reliability. \ No newline at end of file +This implementation provides a production-ready, comprehensive security system that protects against various attack vectors while maintaining high performance and reliability. diff --git a/SETUP.md b/SETUP.md index d6c353a..7ed483d 100644 --- a/SETUP.md +++ b/SETUP.md @@ -8,7 +8,7 @@ Before you begin, ensure you have the following installed: - **Node.js** v18+ (LTS recommended) - **npm** or **yarn** package manager -- **PostgreSQL** v14+ +- **PostgreSQL** v14+ - **Redis** v6+ - **Git** version control - **Docker** & **Docker Compose** (optional, for containerized setup) @@ -43,6 +43,7 @@ nano .env ``` **Required Environment Variables:** + - `DATABASE_URL` - PostgreSQL connection string - `JWT_SECRET` - Secret for JWT token signing - `ENCRYPTION_KEY` - 32-character encryption key @@ -178,25 +179,30 @@ src/ ## Environment Variables ### Application Configuration + - `NODE_ENV` - Environment (development/staging/production) - `PORT` - Server port (default: 3000) - `API_PREFIX` - API route prefix (default: api) - `CORS_ORIGIN` - Allowed CORS origins ### Database + - `DATABASE_URL` - PostgreSQL connection string ### Redis + - `REDIS_HOST` - Redis server host - `REDIS_PORT` - Redis server port - `REDIS_PASSWORD` - Redis password (if required) ### Security + - `JWT_SECRET` - JWT signing secret - `JWT_EXPIRES_IN` - JWT token expiration - `ENCRYPTION_KEY` - Data encryption key ### Blockchain + - `BLOCKCHAIN_NETWORK` - Blockchain network (sepolia/mainnet) - `RPC_URL` - Blockchain RPC endpoint - `PRIVATE_KEY` - Private key for transactions (development only) @@ -204,6 +210,7 @@ src/ ## API Documentation Once the server is running, visit: + - **Swagger UI**: `http://localhost:3000/api/docs` - **Health Check**: `http://localhost:3000/api/health` - **Configuration**: `http://localhost:3000/api/configuration` @@ -213,6 +220,7 @@ Once the server is running, visit: ### Common Issues **Database Connection Error** + ```bash # Check PostgreSQL status pg_ctl status @@ -222,6 +230,7 @@ npm run db:reset ``` **Redis Connection Error** + ```bash # Check Redis status redis-cli ping @@ -231,6 +240,7 @@ docker-compose restart redis ``` **Module Not Found Errors** + ```bash # Clear node modules and reinstall rm -rf node_modules package-lock.json @@ -238,6 +248,7 @@ npm install ``` **Port Already in Use** + ```bash # Find process using port 3000 lsof -ti:3000 diff --git a/docker-compose.yml b/docker-compose.yml index 1c8b719..df37a5b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,12 +11,12 @@ services: POSTGRES_USER: postgres POSTGRES_PASSWORD: password ports: - - "5432:5432" + - '5432:5432' volumes: - postgres_data:/var/lib/postgresql/data - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 10s timeout: 5s retries: 5 @@ -27,11 +27,11 @@ services: container_name: propchain-redis restart: unless-stopped ports: - - "6379:6379" + - '6379:6379' volumes: - redis_data:/data healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ['CMD', 'redis-cli', 'ping'] interval: 10s timeout: 3s retries: 5 @@ -57,7 +57,7 @@ services: SWAGGER_ENABLED: true LOG_LEVEL: info ports: - - "3000:3000" + - '3000:3000' volumes: - ./logs:/app/logs - ./uploads:/app/uploads @@ -67,7 +67,7 @@ services: redis: condition: service_healthy healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] + test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/health'] interval: 30s timeout: 10s retries: 3 @@ -79,8 +79,8 @@ services: container_name: propchain-nginx restart: unless-stopped ports: - - "80:80" - - "443:443" + - '80:80' + - '443:443' volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./nginx/ssl:/etc/nginx/ssl diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index ae2209d..d0ae6db 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -1,34 +1,42 @@ ## Scaling Strategies ### Vertical Scaling + - Increase CPU, RAM, and disk IOPS on the PostgreSQL server for higher throughput. - Adjust `connection_limit` in the DATABASE_URL to match available resources. ### Horizontal Scaling + - Use read replicas for scaling read-heavy workloads. Configure Prisma to use replicas for read queries (future enhancement). - For global scale, consider managed PostgreSQL services (e.g., AWS RDS, GCP Cloud SQL) with multi-region support. ### Connection Pool Tuning + - Tune `connection_limit` and `pool_timeout` in the DATABASE_URL for optimal pool size. - Monitor active connections and adjust pool size as needed. ### Caching + - Use Redis for caching frequently accessed data and query results. - Cache count queries and first-page results for large datasets. ### Partitioning and Sharding (Advanced) + - For very large tables, consider PostgreSQL table partitioning. - Sharding is not natively supported by Prisma, but can be implemented at the application layer if needed. ### High Availability & Failover + - Use managed PostgreSQL with automatic failover and backups. - Regularly test backup and restore procedures (see backup.sh and restore.sh). ### Monitoring + - Monitor query performance and slow queries using Prisma logs and PostgreSQL tools (pg_stat_statements). - Set up external monitoring (e.g., Prometheus, Grafana) for database metrics. // See SETUP.md for more performance and monitoring tips. + # PropChain Database Schema Documentation ## Overview diff --git a/docs/properties-api.md b/docs/properties-api.md index 844b3f6..3554102 100644 --- a/docs/properties-api.md +++ b/docs/properties-api.md @@ -1,45 +1,49 @@ # Properties API Documentation ## Overview + The Properties API provides endpoints for managing real estate properties with advanced filtering, search, and pagination capabilities. ## Endpoints ### Get All Properties + ``` GET /properties ``` #### Query Parameters -| Parameter | Type | Description | Example | -|-----------|------|-------------|---------| -| `page` | number | Page number (default: 1) | `?page=1` | -| `limit` | number | Items per page (default: 20, max: 100) | `?limit=20` | -| `sortBy` | string | Field to sort by | `?sortBy=price` | -| `sortOrder` | string | Sort direction: 'asc' or 'desc' | `?sortOrder=asc` | -| `search` | string | Search in title, description, location | `?search=apartment` | -| `type` | string | Filter by property type | `?type=RESIDENTIAL` | -| `status` | string | Filter by status | `?status=AVAILABLE` | -| `city` | string | Filter by city | `?city=New%20York` | -| `country` | string | Filter by country | `?country=USA` | -| `minPrice` | number | Minimum price | `?minPrice=100000` | -| `maxPrice` | number | Maximum price | `?maxPrice=500000` | -| `minBedrooms` | number | Minimum bedrooms | `?minBedrooms=2` | -| `maxBedrooms` | number | Maximum bedrooms | `?maxBedrooms=5` | -| `minBathrooms` | number | Minimum bathrooms | `?minBathrooms=1` | -| `maxBathrooms` | number | Maximum bathrooms | `?maxBathrooms=3` | -| `minArea` | number | Minimum square footage | `?minArea=500` | -| `maxArea` | number | Maximum square footage | `?maxArea=5000` | -| `ownerId` | string | Filter by owner ID | `?ownerId=user_123` | +| Parameter | Type | Description | Example | +| -------------- | ------ | -------------------------------------- | ------------------- | +| `page` | number | Page number (default: 1) | `?page=1` | +| `limit` | number | Items per page (default: 20, max: 100) | `?limit=20` | +| `sortBy` | string | Field to sort by | `?sortBy=price` | +| `sortOrder` | string | Sort direction: 'asc' or 'desc' | `?sortOrder=asc` | +| `search` | string | Search in title, description, location | `?search=apartment` | +| `type` | string | Filter by property type | `?type=RESIDENTIAL` | +| `status` | string | Filter by status | `?status=AVAILABLE` | +| `city` | string | Filter by city | `?city=New%20York` | +| `country` | string | Filter by country | `?country=USA` | +| `minPrice` | number | Minimum price | `?minPrice=100000` | +| `maxPrice` | number | Maximum price | `?maxPrice=500000` | +| `minBedrooms` | number | Minimum bedrooms | `?minBedrooms=2` | +| `maxBedrooms` | number | Maximum bedrooms | `?maxBedrooms=5` | +| `minBathrooms` | number | Minimum bathrooms | `?minBathrooms=1` | +| `maxBathrooms` | number | Maximum bathrooms | `?maxBathrooms=3` | +| `minArea` | number | Minimum square footage | `?minArea=500` | +| `maxArea` | number | Maximum square footage | `?maxArea=5000` | +| `ownerId` | string | Filter by owner ID | `?ownerId=user_123` | #### Property Types + - `RESIDENTIAL` - Residential properties - `COMMERCIAL` - Commercial properties - `INDUSTRIAL` - Industrial properties - `LAND` - Land parcels #### Property Status + - `AVAILABLE` - Available for sale/rent - `PENDING` - Under contract - `SOLD` - Sold @@ -48,31 +52,37 @@ GET /properties #### Example Requests **Basic filtering:** + ``` GET /properties?type=RESIDENTIAL&status=AVAILABLE ``` **Price range filter:** + ``` GET /properties?minPrice=100000&maxPrice=500000 ``` **Multiple filters:** + ``` GET /properties?type=RESIDENTIAL&minBedrooms=2&maxBathrooms=3&minArea=1000 ``` **Search with pagination:** + ``` GET /properties?search=apartment&page=1&limit=10&sortBy=price&sortOrder=desc ``` **City and country combined:** + ``` GET /properties?city=New%20York&country=USA ``` #### Response Format + ```json { "properties": [ @@ -98,41 +108,49 @@ GET /properties?city=New%20York&country=USA ``` ### Get Property by ID + ``` GET /properties/:id ``` ### Create Property + ``` POST /properties ``` ### Update Property + ``` PATCH /properties/:id ``` ### Update Property Status + ``` PATCH /properties/:id/status ``` ### Delete Property + ``` DELETE /properties/:id ``` ### Get Properties by Owner + ``` GET /properties/owner/:ownerId ``` ### Get Property Statistics + ``` GET /properties/statistics ``` ### Search Nearby Properties + ``` GET /properties/search/nearby?latitude=40.7128&longitude=-74.006&radiusKm=10 ``` @@ -140,16 +158,22 @@ GET /properties/search/nearby?latitude=40.7128&longitude=-74.006&radiusKm=10 ## Filtering Features ### Combined City and Country Filter + The city and country filters are combined into a single location search. For example: + - `?city=New York&country=USA` will search for properties with "New York, USA" in the location field ### Range Filters + All numeric filters support range queries: + - `minX` - Minimum value (inclusive) - `maxX` - Maximum value (inclusive) ### Search + The search parameter performs case-insensitive matching across: + - Property title - Property description - Property location @@ -157,6 +181,7 @@ The search parameter performs case-insensitive matching across: ## Error Handling All endpoints return standard HTTP status codes: + - `200` - Success - `400` - Bad Request (invalid parameters) - `401` - Unauthorized diff --git a/docs/valuation-api.md b/docs/valuation-api.md index ade4a74..54e528f 100644 --- a/docs/valuation-api.md +++ b/docs/valuation-api.md @@ -1,22 +1,27 @@ # Property Valuation API Documentation ## Overview + The Property Valuation API provides automated property value estimates based on market data, location, and property features. It integrates with external ML services and valuation APIs to provide accurate property valuations. ## Endpoints ### Get Property Valuation + ``` POST /valuation/:propertyId ``` #### Description + Gets a property valuation based on its features and external market data. #### Parameters + - `propertyId` (path): The ID of the property to value #### Request Body + ```json { "location": "123 Main St, City, State", @@ -30,6 +35,7 @@ Gets a property valuation based on its features and external market data. ``` #### Response + ```json { "propertyId": "property-id-string", @@ -63,17 +69,21 @@ Gets a property valuation based on its features and external market data. ``` ### Get Property Valuation History + ``` GET /valuation/:propertyId/history ``` #### Description + Retrieves the historical valuations for a specific property. #### Parameters + - `propertyId` (path): The ID of the property #### Response + ```json [ { @@ -96,17 +106,21 @@ Retrieves the historical valuations for a specific property. ``` ### Get Market Trend Analysis + ``` GET /valuation/trends/:location ``` #### Description + Retrieves market trend analysis for a specific location. #### Parameters + - `location` (path): The location to analyze #### Response + ```json { "location": "New York, NY", @@ -125,28 +139,35 @@ Retrieves market trend analysis for a specific location. ``` ### Get Latest Valuation + ``` GET /valuation/:propertyId/latest ``` #### Description + Retrieves the most recent valuation for a property. #### Parameters + - `propertyId` (path): The ID of the property #### Response + Same as Get Property Valuation endpoint. ### Batch Valuations + ``` POST /valuation/batch ``` #### Description + Get valuations for multiple properties in a single request. #### Request Body + ```json { "properties": [ @@ -173,6 +194,7 @@ Get valuations for multiple properties in a single request. ``` #### Response + ```json [ { @@ -198,24 +220,31 @@ Get valuations for multiple properties in a single request. ## Features ### Integration with External APIs + - Zillow API for property valuations - Redfin API for comparative market analysis - CoreLogic API for comprehensive property data ### Valuation Confidence Scoring + The API provides confidence scores for each valuation, indicating the reliability of the estimate based on data quality and availability. ### Historical Tracking + All valuations are stored with timestamps, allowing for trend analysis and comparison over time. ### Market Trend Analysis + The API analyzes market trends in specific locations to provide context for individual property valuations. ### Caching Strategy + Valuation results are cached to reduce API calls to external services and improve response times. ### Error Handling + Comprehensive error handling for API failures, with fallback mechanisms and graceful degradation. ### Rate Limiting -Built-in rate limiting to prevent abuse of external valuation APIs. \ No newline at end of file + +Built-in rate limiting to prevent abuse of external valuation APIs. diff --git a/jest.config.js b/jest.config.js index d14e26f..0e27d94 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,9 +3,12 @@ module.exports = { rootDir: '.', testRegex: '.*\\.spec\\.ts$', transform: { - '^.+\\.(t|j)s$': ['ts-jest', { - tsconfig: 'tsconfig.spec.json', - }], + '^.+\\.(t|j)s$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.spec.json', + }, + ], }, collectCoverageFrom: [ 'src/**/*.(t|j)s', @@ -17,18 +20,13 @@ module.exports = { '!src/**/*.mock.ts', ], coverageDirectory: 'coverage', - coverageReporters: [ - 'text', - 'lcov', - 'html', - 'json', - ], + coverageReporters: ['text', 'lcov', 'html', 'json'], coverageThreshold: { global: { branches: 28, functions: 35, lines: 35, - statements: 35 + statements: 35, }, }, testEnvironment: 'node', diff --git a/prisma/seed.ts b/prisma/seed.ts index 02131bc..180e3da 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,338 +1,350 @@ -import { PrismaClient, UserRole, PropertyStatus, TransactionStatus, TransactionType, DocumentType, DocumentStatus, Permission } from '@prisma/client'; +import { + PrismaClient, + UserRole, + PropertyStatus, + TransactionStatus, + TransactionType, + DocumentType, + DocumentStatus, + Permission, +} from '@prisma/client'; const prisma = new PrismaClient(); async function main() { - console.log('🌱 Starting database seeding...'); - - // Clean existing data (in reverse order of dependencies) - await prisma.document.deleteMany(); - await prisma.transaction.deleteMany(); - await prisma.property.deleteMany(); - await prisma.roleChangeLog.deleteMany(); - await prisma.rolePermission.deleteMany(); - await prisma.permission.deleteMany(); - await prisma.apiKey.deleteMany(); - await prisma.user.deleteMany(); - await prisma.role.deleteMany(); - await prisma.auditLog.deleteMany(); - await prisma.systemLog.deleteMany(); - - console.log('🧹 Cleaned existing data'); - - // Create Roles - const adminRole = await prisma.role.create({ - data: { - name: 'Administrator', - description: 'Full system access with all permissions', - level: 100, - isSystem: true, - }, + console.log('🌱 Starting database seeding...'); + + // Clean existing data (in reverse order of dependencies) + await prisma.document.deleteMany(); + await prisma.transaction.deleteMany(); + await prisma.property.deleteMany(); + await prisma.roleChangeLog.deleteMany(); + await prisma.rolePermission.deleteMany(); + await prisma.permission.deleteMany(); + await prisma.apiKey.deleteMany(); + await prisma.user.deleteMany(); + await prisma.role.deleteMany(); + await prisma.auditLog.deleteMany(); + await prisma.systemLog.deleteMany(); + + console.log('🧹 Cleaned existing data'); + + // Create Roles + const adminRole = await prisma.role.create({ + data: { + name: 'Administrator', + description: 'Full system access with all permissions', + level: 100, + isSystem: true, + }, + }); + + const agentRole = await prisma.role.create({ + data: { + name: 'Agent', + description: 'Property management and listing permissions', + level: 50, + isSystem: true, + }, + }); + + const userRole = await prisma.role.create({ + data: { + name: 'User', + description: 'Standard user permissions', + level: 10, + isSystem: true, + }, + }); + + console.log('✅ Created roles'); + + // Create Permissions + const permissions = await Promise.all([ + prisma.permission.create({ + data: { resource: 'users', action: 'create', description: 'Create new users' }, + }), + prisma.permission.create({ + data: { resource: 'users', action: 'read', description: 'View user information' }, + }), + prisma.permission.create({ + data: { resource: 'users', action: 'update', description: 'Update user information' }, + }), + prisma.permission.create({ + data: { resource: 'users', action: 'delete', description: 'Delete users' }, + }), + prisma.permission.create({ + data: { resource: 'properties', action: 'create', description: 'Create property listings' }, + }), + prisma.permission.create({ + data: { resource: 'properties', action: 'read', description: 'View properties' }, + }), + prisma.permission.create({ + data: { resource: 'properties', action: 'update', description: 'Update property listings' }, + }), + prisma.permission.create({ + data: { resource: 'properties', action: 'delete', description: 'Delete properties' }, + }), + prisma.permission.create({ + data: { resource: 'properties', action: 'approve', description: 'Approve property listings' }, + }), + prisma.permission.create({ + data: { resource: 'transactions', action: 'create', description: 'Create transactions' }, + }), + prisma.permission.create({ + data: { resource: 'transactions', action: 'read', description: 'View transactions' }, + }), + prisma.permission.create({ + data: { resource: 'transactions', action: 'update', description: 'Update transactions' }, + }), + prisma.permission.create({ + data: { resource: 'documents', action: 'create', description: 'Upload documents' }, + }), + prisma.permission.create({ + data: { resource: 'documents', action: 'read', description: 'View documents' }, + }), + prisma.permission.create({ + data: { resource: 'documents', action: 'verify', description: 'Verify documents' }, + }), + ]); + + console.log('✅ Created permissions'); + + // Assign all permissions to admin role + for (const permission of permissions) { + await prisma.rolePermission.create({ + data: { + roleId: adminRole.id, + permissionId: permission.id, + }, }); - - const agentRole = await prisma.role.create({ - data: { - name: 'Agent', - description: 'Property management and listing permissions', - level: 50, - isSystem: true, - }, - }); - - const userRole = await prisma.role.create({ - data: { - name: 'User', - description: 'Standard user permissions', - level: 10, - isSystem: true, - }, - }); - - console.log('✅ Created roles'); - - // Create Permissions - const permissions = await Promise.all([ - prisma.permission.create({ - data: { resource: 'users', action: 'create', description: 'Create new users' }, - }), - prisma.permission.create({ - data: { resource: 'users', action: 'read', description: 'View user information' }, - }), - prisma.permission.create({ - data: { resource: 'users', action: 'update', description: 'Update user information' }, - }), - prisma.permission.create({ - data: { resource: 'users', action: 'delete', description: 'Delete users' }, - }), - prisma.permission.create({ - data: { resource: 'properties', action: 'create', description: 'Create property listings' }, - }), - prisma.permission.create({ - data: { resource: 'properties', action: 'read', description: 'View properties' }, - }), - prisma.permission.create({ - data: { resource: 'properties', action: 'update', description: 'Update property listings' }, - }), - prisma.permission.create({ - data: { resource: 'properties', action: 'delete', description: 'Delete properties' }, - }), - prisma.permission.create({ - data: { resource: 'properties', action: 'approve', description: 'Approve property listings' }, - }), - prisma.permission.create({ - data: { resource: 'transactions', action: 'create', description: 'Create transactions' }, - }), - prisma.permission.create({ - data: { resource: 'transactions', action: 'read', description: 'View transactions' }, - }), - prisma.permission.create({ - data: { resource: 'transactions', action: 'update', description: 'Update transactions' }, - }), - prisma.permission.create({ - data: { resource: 'documents', action: 'create', description: 'Upload documents' }, - }), - prisma.permission.create({ - data: { resource: 'documents', action: 'read', description: 'View documents' }, - }), - prisma.permission.create({ - data: { resource: 'documents', action: 'verify', description: 'Verify documents' }, - }), - ]); - - console.log('✅ Created permissions'); - - // Assign all permissions to admin role - for (const permission of permissions) { - await prisma.rolePermission.create({ - data: { - roleId: adminRole.id, - permissionId: permission.id, - }, - }); - } - - // Assign property and document permissions to agent role - const agentPermissions = permissions.filter( - (p: Permission) => p.resource === 'properties' || p.resource === 'documents' || (p.resource === 'transactions' && p.action === 'read') - ); - for (const permission of agentPermissions) { - await prisma.rolePermission.create({ - data: { - roleId: agentRole.id, - permissionId: permission.id, - }, - }); - } - - // Assign basic read permissions to user role - const userPermissions = permissions.filter( - (p: Permission) => p.action === 'read' || (p.resource === 'documents' && p.action === 'create') - ); - for (const permission of userPermissions) { - await prisma.rolePermission.create({ - data: { - roleId: userRole.id, - permissionId: permission.id, - }, - }); - } - - console.log('✅ Assigned permissions to roles'); - - // Create Users - const adminUser = await prisma.user.create({ - data: { - email: 'admin@propchain.io', - walletAddress: '0x1234567890123456789012345678901234567890', - role: UserRole.ADMIN, - roleId: adminRole.id, - }, - }); - - const agentUser = await prisma.user.create({ - data: { - email: 'agent@propchain.io', - walletAddress: '0x2345678901234567890123456789012345678901', - role: UserRole.AGENT, - roleId: agentRole.id, - }, - }); - - const buyerUser = await prisma.user.create({ - data: { - email: 'buyer@example.com', - walletAddress: '0x3456789012345678901234567890123456789012', - role: UserRole.BUYER, - roleId: userRole.id, - }, + } + + // Assign property and document permissions to agent role + const agentPermissions = permissions.filter( + (p: Permission) => + p.resource === 'properties' || + p.resource === 'documents' || + (p.resource === 'transactions' && p.action === 'read'), + ); + for (const permission of agentPermissions) { + await prisma.rolePermission.create({ + data: { + roleId: agentRole.id, + permissionId: permission.id, + }, }); - - const sellerUser = await prisma.user.create({ - data: { - email: 'seller@example.com', - walletAddress: '0x4567890123456789012345678901234567890123', - role: UserRole.SELLER, - roleId: userRole.id, - }, - }); - - console.log('✅ Created users'); - - // Create Properties - const property1 = await prisma.property.create({ - data: { - title: 'Modern Downtown Apartment', - description: 'A beautiful 2-bedroom apartment in the heart of downtown with stunning city views.', - location: '123 Main Street, Downtown District', - price: 450000, - status: PropertyStatus.LISTED, - ownerId: sellerUser.id, - }, - }); - - const property2 = await prisma.property.create({ - data: { - title: 'Suburban Family Home', - description: 'Spacious 4-bedroom family home with large backyard and modern amenities.', - location: '456 Oak Avenue, Suburbia', - price: 750000, - status: PropertyStatus.APPROVED, - ownerId: sellerUser.id, - }, - }); - - const property3 = await prisma.property.create({ - data: { - title: 'Luxury Penthouse Suite', - description: 'Exclusive penthouse with panoramic views, private elevator, and rooftop terrace.', - location: '789 Skyline Tower, Financial District', - price: 2500000, - status: PropertyStatus.PENDING, - ownerId: agentUser.id, - }, - }); - - console.log('✅ Created properties'); - - // Create Transactions - const transaction1 = await prisma.transaction.create({ - data: { - fromAddress: buyerUser.walletAddress!, - toAddress: sellerUser.walletAddress!, - amount: 450000, - txHash: '0xabc123def456789012345678901234567890123456789012345678901234567890', - status: TransactionStatus.COMPLETED, - type: TransactionType.PURCHASE, - propertyId: property1.id, - }, + } + + // Assign basic read permissions to user role + const userPermissions = permissions.filter( + (p: Permission) => p.action === 'read' || (p.resource === 'documents' && p.action === 'create'), + ); + for (const permission of userPermissions) { + await prisma.rolePermission.create({ + data: { + roleId: userRole.id, + permissionId: permission.id, + }, }); - - const transaction2 = await prisma.transaction.create({ - data: { - fromAddress: buyerUser.walletAddress!, - toAddress: sellerUser.walletAddress!, - amount: 75000, - status: TransactionStatus.PENDING, - type: TransactionType.ESCROW, - propertyId: property2.id, - }, - }); - - console.log('✅ Created transactions'); - - // Create Documents - await prisma.document.create({ - data: { - name: 'Property Title Deed', - type: DocumentType.TITLE_DEED, - status: DocumentStatus.VERIFIED, - fileUrl: 'https://storage.propchain.io/documents/title-deed-1.pdf', - fileHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', - mimeType: 'application/pdf', - fileSize: 1024000, - description: 'Official title deed for the downtown apartment', - propertyId: property1.id, - uploadedById: sellerUser.id, - verifiedAt: new Date(), - }, - }); - - await prisma.document.create({ - data: { - name: 'Property Inspection Report', - type: DocumentType.INSPECTION_REPORT, - status: DocumentStatus.VERIFIED, - fileUrl: 'https://storage.propchain.io/documents/inspection-1.pdf', - fileHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', - mimeType: 'application/pdf', - fileSize: 2048000, - description: 'Professional inspection report dated 2025', - propertyId: property1.id, - transactionId: transaction1.id, - uploadedById: agentUser.id, - verifiedAt: new Date(), - }, - }); - - await prisma.document.create({ - data: { - name: 'Purchase Contract', - type: DocumentType.CONTRACT, - status: DocumentStatus.PENDING, - fileUrl: 'https://storage.propchain.io/documents/contract-2.pdf', - mimeType: 'application/pdf', - fileSize: 512000, - description: 'Purchase agreement for suburban home', - propertyId: property2.id, - transactionId: transaction2.id, - uploadedById: buyerUser.id, - }, - }); - - console.log('✅ Created documents'); - - // Create API Keys - await prisma.apiKey.create({ - data: { - name: 'Development API Key', - key: 'pk_dev_1234567890abcdef1234567890abcdef', - keyPrefix: 'pk_dev', - scopes: ['read:properties', 'read:transactions'], - isActive: true, - rateLimit: 1000, - }, - }); - - await prisma.apiKey.create({ - data: { - name: 'Integration Test Key', - key: 'pk_test_abcdef1234567890abcdef1234567890', - keyPrefix: 'pk_test', - scopes: ['read:properties', 'write:properties', 'read:transactions'], - isActive: true, - rateLimit: 100, - }, - }); - - console.log('✅ Created API keys'); - - // Create System Log entry - await prisma.systemLog.create({ - data: { - logLevel: 'INFO', - message: 'Database seeded successfully', - context: 'seed', - }, - }); - - console.log('🎉 Database seeding completed successfully!'); + } + + console.log('✅ Assigned permissions to roles'); + + // Create Users + const adminUser = await prisma.user.create({ + data: { + email: 'admin@propchain.io', + walletAddress: '0x1234567890123456789012345678901234567890', + role: UserRole.ADMIN, + roleId: adminRole.id, + }, + }); + + const agentUser = await prisma.user.create({ + data: { + email: 'agent@propchain.io', + walletAddress: '0x2345678901234567890123456789012345678901', + role: UserRole.AGENT, + roleId: agentRole.id, + }, + }); + + const buyerUser = await prisma.user.create({ + data: { + email: 'buyer@example.com', + walletAddress: '0x3456789012345678901234567890123456789012', + role: UserRole.BUYER, + roleId: userRole.id, + }, + }); + + const sellerUser = await prisma.user.create({ + data: { + email: 'seller@example.com', + walletAddress: '0x4567890123456789012345678901234567890123', + role: UserRole.SELLER, + roleId: userRole.id, + }, + }); + + console.log('✅ Created users'); + + // Create Properties + const property1 = await prisma.property.create({ + data: { + title: 'Modern Downtown Apartment', + description: 'A beautiful 2-bedroom apartment in the heart of downtown with stunning city views.', + location: '123 Main Street, Downtown District', + price: 450000, + status: PropertyStatus.LISTED, + ownerId: sellerUser.id, + }, + }); + + const property2 = await prisma.property.create({ + data: { + title: 'Suburban Family Home', + description: 'Spacious 4-bedroom family home with large backyard and modern amenities.', + location: '456 Oak Avenue, Suburbia', + price: 750000, + status: PropertyStatus.APPROVED, + ownerId: sellerUser.id, + }, + }); + + const property3 = await prisma.property.create({ + data: { + title: 'Luxury Penthouse Suite', + description: 'Exclusive penthouse with panoramic views, private elevator, and rooftop terrace.', + location: '789 Skyline Tower, Financial District', + price: 2500000, + status: PropertyStatus.PENDING, + ownerId: agentUser.id, + }, + }); + + console.log('✅ Created properties'); + + // Create Transactions + const transaction1 = await prisma.transaction.create({ + data: { + fromAddress: buyerUser.walletAddress!, + toAddress: sellerUser.walletAddress!, + amount: 450000, + txHash: '0xabc123def456789012345678901234567890123456789012345678901234567890', + status: TransactionStatus.COMPLETED, + type: TransactionType.PURCHASE, + propertyId: property1.id, + }, + }); + + const transaction2 = await prisma.transaction.create({ + data: { + fromAddress: buyerUser.walletAddress!, + toAddress: sellerUser.walletAddress!, + amount: 75000, + status: TransactionStatus.PENDING, + type: TransactionType.ESCROW, + propertyId: property2.id, + }, + }); + + console.log('✅ Created transactions'); + + // Create Documents + await prisma.document.create({ + data: { + name: 'Property Title Deed', + type: DocumentType.TITLE_DEED, + status: DocumentStatus.VERIFIED, + fileUrl: 'https://storage.propchain.io/documents/title-deed-1.pdf', + fileHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + mimeType: 'application/pdf', + fileSize: 1024000, + description: 'Official title deed for the downtown apartment', + propertyId: property1.id, + uploadedById: sellerUser.id, + verifiedAt: new Date(), + }, + }); + + await prisma.document.create({ + data: { + name: 'Property Inspection Report', + type: DocumentType.INSPECTION_REPORT, + status: DocumentStatus.VERIFIED, + fileUrl: 'https://storage.propchain.io/documents/inspection-1.pdf', + fileHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + mimeType: 'application/pdf', + fileSize: 2048000, + description: 'Professional inspection report dated 2025', + propertyId: property1.id, + transactionId: transaction1.id, + uploadedById: agentUser.id, + verifiedAt: new Date(), + }, + }); + + await prisma.document.create({ + data: { + name: 'Purchase Contract', + type: DocumentType.CONTRACT, + status: DocumentStatus.PENDING, + fileUrl: 'https://storage.propchain.io/documents/contract-2.pdf', + mimeType: 'application/pdf', + fileSize: 512000, + description: 'Purchase agreement for suburban home', + propertyId: property2.id, + transactionId: transaction2.id, + uploadedById: buyerUser.id, + }, + }); + + console.log('✅ Created documents'); + + // Create API Keys + await prisma.apiKey.create({ + data: { + name: 'Development API Key', + key: 'pk_dev_1234567890abcdef1234567890abcdef', + keyPrefix: 'pk_dev', + scopes: ['read:properties', 'read:transactions'], + isActive: true, + rateLimit: 1000, + }, + }); + + await prisma.apiKey.create({ + data: { + name: 'Integration Test Key', + key: 'pk_test_abcdef1234567890abcdef1234567890', + keyPrefix: 'pk_test', + scopes: ['read:properties', 'write:properties', 'read:transactions'], + isActive: true, + rateLimit: 100, + }, + }); + + console.log('✅ Created API keys'); + + // Create System Log entry + await prisma.systemLog.create({ + data: { + logLevel: 'INFO', + message: 'Database seeded successfully', + context: 'seed', + }, + }); + + console.log('🎉 Database seeding completed successfully!'); } main() - .catch((e) => { - console.error('❌ Error seeding database:', e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); + .catch(e => { + console.error('❌ Error seeding database:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/api-keys/README.md b/src/api-keys/README.md index 35bfc8e..5887620 100644 --- a/src/api-keys/README.md +++ b/src/api-keys/README.md @@ -48,6 +48,7 @@ Content-Type: application/json ``` **Response** (201 Created): + ```json { "id": "cly1234567890", @@ -74,6 +75,7 @@ Authorization: Bearer ``` **Response** (200 OK): + ```json [ { @@ -150,7 +152,6 @@ import { ApiKey } from '../common/decorators/api-key.decorator'; @Controller('properties') @UseGuards(ApiKeyGuard) export class PropertiesController { - @Get() @RequireScopes('read:properties') async findAll(@ApiKey() apiKey: any) { @@ -176,7 +177,6 @@ import { RequireScopes } from '../common/decorators/require-scopes.decorator'; @Controller('external-api') export class ExternalApiController { - // This endpoint requires API key with read:properties scope @Get('properties') @UseGuards(ApiKeyGuard) @@ -266,15 +266,15 @@ const BASE_URL = 'https://api.propchain.io'; // Using Authorization header const response = await axios.get(`${BASE_URL}/properties`, { headers: { - 'Authorization': `Bearer ${API_KEY}` - } + Authorization: `Bearer ${API_KEY}`, + }, }); // Or using X-API-Key header const response = await axios.get(`${BASE_URL}/properties`, { headers: { - 'X-API-Key': API_KEY - } + 'X-API-Key': API_KEY, + }, }); ``` @@ -314,29 +314,35 @@ curl -H "X-API-Key: propchain_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \ ## Troubleshooting ### "Invalid API key format" + - Ensure the key starts with `propchain_live_` - Check for extra spaces or line breaks ### "Invalid or revoked API key" + - Key might be revoked - check with `GET /api-keys/:id` - Key might be from a different environment ### "Insufficient permissions" + - Check that the API key has the required scopes - Update scopes with `PATCH /api-keys/:id` ### "Rate limit exceeded" + - Wait 1 minute for the rate limit window to reset - Or increase the rate limit for the key ## Testing Run unit tests: + ```bash npm run test -- api-key.service.spec.ts ``` Run e2e tests: + ```bash npm run test:e2e -- api-keys.e2e-spec.ts ``` diff --git a/src/api-keys/SETUP.md b/src/api-keys/SETUP.md index b655e2a..5831c50 100644 --- a/src/api-keys/SETUP.md +++ b/src/api-keys/SETUP.md @@ -21,6 +21,7 @@ ENCRYPTION_KEY=your-secure-32-character-encryption-key-here-minimum ``` **Generate a secure key:** + ```bash # Option 1: Using Node.js node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" @@ -72,6 +73,7 @@ npm run start:dev Once the server is running: 1. **Register/Login** to get a JWT token: + ```bash curl -X POST http://localhost:3000/auth/login \ -H "Content-Type: application/json" \ @@ -79,6 +81,7 @@ curl -X POST http://localhost:3000/auth/login \ ``` 2. **Create an API key**: + ```bash curl -X POST http://localhost:3000/api-keys \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ @@ -136,17 +139,18 @@ See `src/api-keys/examples/properties-with-api-keys.example.ts` for detailed exa To add new scopes for your specific use case: 1. Define scope constants (optional but recommended): + ```typescript // src/api-keys/constants/scopes.ts export const API_KEY_SCOPES = { // Properties READ_PROPERTIES: 'read:properties', WRITE_PROPERTIES: 'write:properties', - + // Transactions READ_TRANSACTIONS: 'read:transactions', WRITE_TRANSACTIONS: 'write:transactions', - + // Add your custom scopes here READ_ANALYTICS: 'read:analytics', ADMIN_ACCESS: 'admin:all', @@ -154,6 +158,7 @@ export const API_KEY_SCOPES = { ``` 2. Use the scopes in your controllers: + ```typescript @ApiKeyScopes(API_KEY_SCOPES.READ_ANALYTICS) ``` @@ -192,6 +197,7 @@ curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \ ``` Check the response for: + - `requestCount` - Total requests made - `lastUsedAt` - Last time the key was used - `isActive` - Whether the key is still active @@ -201,6 +207,7 @@ Check the response for: ### Migration Issues If migration fails: + ```bash # Reset database (⚠️ WARNING: This deletes all data) npx prisma migrate reset @@ -214,6 +221,7 @@ npx prisma migrate dev ### Encryption Errors If you see "ENCRYPTION_KEY must be at least 32 characters": + ```bash # Verify your .env file has the key cat .env | grep ENCRYPTION_KEY @@ -225,6 +233,7 @@ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" ### Rate Limiting Issues If requests are being rate limited unexpectedly: + - Current implementation is simple (checks lastUsedAt within 1 minute) - For production, consider implementing Redis-based rate limiting - See `RedisService` for integration diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 8912893..e13d685 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -17,10 +17,10 @@ import { Request } from 'express'; /** * AuthController - * + * * Handles all authentication endpoints including user registration, login (traditional and Web3), * token management, password reset, email verification, and session management. - * + * * All endpoints that require authentication are protected with JwtAuthGuard. * Login attempts are rate-limited to prevent brute-force attacks. */ @@ -31,13 +31,13 @@ export class AuthController { /** * Register a new user account - * + * * Creates a new user with email and password credentials. Validates password strength * and checks for duplicate email addresses. Sends verification email upon success. - * + * * @param {CreateUserDto} createUserDto - User registration data * @returns {Promise<{message: string}>} Success message with verification instructions - * + * * @example * ```json * { @@ -49,51 +49,57 @@ export class AuthController { * ``` */ @Post('register') - @ApiOperation({ + @ApiOperation({ summary: 'Register a new user account', - description: 'Creates a new user account with email/password. Sends verification email. Password must be at least 8 characters with uppercase, lowercase, number, and special character.' + description: + 'Creates a new user account with email/password. Sends verification email. Password must be at least 8 characters with uppercase, lowercase, number, and special character.', }) - @ApiResponse({ - status: 201, + @ApiResponse({ + status: 201, description: 'User registered successfully. Verification email sent.', schema: { properties: { - message: { type: 'string', example: 'User registered successfully. Please check your email for verification.' } - } - } + message: { type: 'string', example: 'User registered successfully. Please check your email for verification.' }, + }, + }, }) @ApiResponse({ status: 409, description: 'User with this email already exists.', type: ErrorResponseDto }) - @ApiResponse({ status: 400, description: 'Validation failed (weak password, invalid email, etc).', type: ErrorResponseDto }) + @ApiResponse({ + status: 400, + description: 'Validation failed (weak password, invalid email, etc).', + type: ErrorResponseDto, + }) async register(@Body() createUserDto: CreateUserDto) { return this.authService.register(createUserDto); } /** * Authenticate user with email and password - * + * * Traditional email/password authentication. Enforces rate limiting after failed attempts. * Returns JWT access token (short-lived) and refresh token (long-lived). - * + * * @param {LoginDto} loginDto - Email and password credentials * @param {Request} req - Express request object * @returns {Promise<{access_token: string, refresh_token: string, user: object}>} Auth tokens */ @Post('login') @UseGuards(LoginAttemptsGuard) - @ApiOperation({ + @ApiOperation({ summary: 'Login with email and password', - description: 'Authenticates user with email and password. Returns access token (valid 15m) and refresh token (valid 7d). Rate limit: 5 attempts per 10 minutes.' + description: + 'Authenticates user with email and password. Returns access token (valid 15m) and refresh token (valid 7d). Rate limit: 5 attempts per 10 minutes.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Login successful.', schema: { properties: { access_token: { type: 'string', description: 'JWT access token for API requests' }, refresh_token: { type: 'string', description: 'JWT refresh token for obtaining new access tokens' }, - user: { type: 'object', description: 'User information' } - } - } + user: { type: 'object', description: 'User information' }, + }, + }, }) @ApiResponse({ status: 401, description: 'Invalid credentials or too many attempts.', type: ErrorResponseDto }) @ApiResponse({ status: 400, description: 'Validation failed.', type: ErrorResponseDto }) @@ -107,28 +113,29 @@ export class AuthController { /** * Web3 wallet authentication - * + * * Authenticates user via blockchain wallet address and signature. * Automatically creates account for new wallet addresses (JIT provisioning). - * + * * @param {LoginWeb3Dto} loginDto - Wallet address and signature * @returns {Promise<{access_token: string, refresh_token: string, user: object}>} Auth tokens */ @Post('web3-login') - @ApiOperation({ + @ApiOperation({ summary: 'Web3 wallet login', - description: 'Authenticates user via blockchain wallet signature. Creates account automatically if wallet not registered. Supports Ethereum-based networks.' + description: + 'Authenticates user via blockchain wallet signature. Creates account automatically if wallet not registered. Supports Ethereum-based networks.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Web3 login successful.', schema: { properties: { access_token: { type: 'string' }, refresh_token: { type: 'string' }, - user: { type: 'object' } - } - } + user: { type: 'object' }, + }, + }, }) @ApiResponse({ status: 401, description: 'Invalid wallet address or signature.', type: ErrorResponseDto }) @HttpCode(HttpStatus.OK) @@ -141,28 +148,28 @@ export class AuthController { /** * Refresh access token - * + * * Exchanges an expired or expiring access token for a new one using a refresh token. * Implements token rotation for enhanced security. - * + * * @param {RefreshTokenDto} refreshTokenDto - Refresh token * @returns {Promise<{access_token: string, refresh_token: string}>} New token pair */ @Post('refresh-token') - @ApiOperation({ + @ApiOperation({ summary: 'Refresh access token', - description: 'Exchanges refresh token for new access token. Implements token rotation.' + description: 'Exchanges refresh token for new access token. Implements token rotation.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Token refreshed successfully.', schema: { properties: { access_token: { type: 'string' }, refresh_token: { type: 'string' }, - user: { type: 'object' } - } - } + user: { type: 'object' }, + }, + }, }) @ApiResponse({ status: 401, description: 'Invalid or expired refresh token.', type: ErrorResponseDto }) @HttpCode(HttpStatus.OK) @@ -172,28 +179,28 @@ export class AuthController { /** * Logout user - * + * * Invalidates current session by blacklisting access token and revoking refresh token. * Requires authentication with valid JWT token. - * + * * @param {Request} req - Express request with user context * @returns {Promise<{message: string}>} Logout confirmation */ @Post('logout') @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOperation({ + @ApiOperation({ summary: 'Logout current user', - description: 'Invalidates current session by blacklisting tokens. Requires valid access token.' + description: 'Invalidates current session by blacklisting tokens. Requires valid access token.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Logged out successfully.', schema: { properties: { - message: { type: 'string', example: 'Logged out successfully' } - } - } + message: { type: 'string', example: 'Logged out successfully' }, + }, + }, }) @ApiResponse({ status: 401, description: 'Unauthorized - invalid or missing token.', type: ErrorResponseDto }) @HttpCode(HttpStatus.OK) @@ -206,26 +213,26 @@ export class AuthController { /** * Request password reset - * + * * Initiates password reset flow by sending reset link to user email. * Returns generic message regardless of email existence to prevent enumeration. - * + * * @param {ForgotPasswordDto} forgotPasswordDto - User email address * @returns {Promise<{message: string}>} Generic success message */ @Post('forgot-password') - @ApiOperation({ + @ApiOperation({ summary: 'Request password reset email', - description: 'Sends password reset link to user email. Returns generic message for security.' + description: 'Sends password reset link to user email. Returns generic message for security.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Password reset email sent (if email exists).', schema: { properties: { - message: { type: 'string', example: 'If email exists, a reset link has been sent' } - } - } + message: { type: 'string', example: 'If email exists, a reset link has been sent' }, + }, + }, }) @HttpCode(HttpStatus.OK) async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) { @@ -234,26 +241,26 @@ export class AuthController { /** * Reset password with token - * + * * Completes password reset using token from email. Validates token hasn't expired. * New password must meet strength requirements. - * + * * @param {ResetPasswordDto} resetPasswordDto - Reset token and new password * @returns {Promise<{message: string}>} Success message */ @Put('reset-password') - @ApiOperation({ + @ApiOperation({ summary: 'Reset password using reset token', - description: 'Sets new password using token from password reset email. Token valid for 1 hour.' + description: 'Sets new password using token from password reset email. Token valid for 1 hour.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Password reset successfully.', schema: { properties: { - message: { type: 'string', example: 'Password reset successfully' } - } - } + message: { type: 'string', example: 'Password reset successfully' }, + }, + }, }) @ApiResponse({ status: 400, description: 'Invalid, expired, or already-used reset token.', type: ErrorResponseDto }) @HttpCode(HttpStatus.OK) @@ -263,50 +270,54 @@ export class AuthController { /** * Verify email address - * + * * Marks user email as verified using token from verification email. * Token expires after 1 hour. - * + * * @param {VerifyEmailParamsDto} params - Email verification token * @returns {Promise<{message: string}>} Verification success message */ @Get('verify-email/:token') - @ApiOperation({ + @ApiOperation({ summary: 'Verify email address', - description: 'Confirms email ownership using token from verification email. Token valid for 1 hour.' + description: 'Confirms email ownership using token from verification email. Token valid for 1 hour.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Email verified successfully.', schema: { properties: { - message: { type: 'string', example: 'Email verified successfully' } - } - } + message: { type: 'string', example: 'Email verified successfully' }, + }, + }, + }) + @ApiResponse({ + status: 400, + description: 'Invalid, expired, or already-used verification token.', + type: ErrorResponseDto, }) - @ApiResponse({ status: 400, description: 'Invalid, expired, or already-used verification token.', type: ErrorResponseDto }) async verifyEmail(@Param() params: VerifyEmailParamsDto) { return this.authService.verifyEmail(params.token); } /** * Get all active sessions - * + * * Returns list of all active sessions for authenticated user. * Requires valid JWT access token. - * + * * @param {Request} req - Express request with user context * @returns {Promise} List of active sessions with metadata */ @Get('sessions') @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOperation({ + @ApiOperation({ summary: 'Get all active sessions for current user', - description: 'Lists all active sessions with IP, user agent, and expiration time.' + description: 'Lists all active sessions with IP, user agent, and expiration time.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Sessions retrieved successfully.', schema: { type: 'array', @@ -317,10 +328,10 @@ export class AuthController { createdAt: { type: 'string', format: 'date-time' }, userAgent: { type: 'string' }, ip: { type: 'string' }, - expiresIn: { type: 'number' } - } - } - } + expiresIn: { type: 'number' }, + }, + }, + }, }) @ApiResponse({ status: 401, description: 'Unauthorized.', type: ErrorResponseDto }) @HttpCode(HttpStatus.OK) @@ -331,10 +342,10 @@ export class AuthController { /** * Invalidate specific session - * + * * Logs out a specific session by session ID. Useful for remote logout * of specific devices without affecting other sessions. - * + * * @param {Request} req - Express request with user context * @param {string} sessionId - ID of session to invalidate * @returns {Promise<{message: string}>} Success confirmation @@ -342,18 +353,18 @@ export class AuthController { @Delete('sessions/:sessionId') @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOperation({ + @ApiOperation({ summary: 'Invalidate a specific session', - description: 'Logs out a specific device/session without affecting other user sessions.' + description: 'Logs out a specific device/session without affecting other user sessions.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Session invalidated successfully.', schema: { properties: { - message: { type: 'string', example: 'Session invalidated successfully' } - } - } + message: { type: 'string', example: 'Session invalidated successfully' }, + }, + }, }) @ApiResponse({ status: 401, description: 'Unauthorized.', type: ErrorResponseDto }) @HttpCode(HttpStatus.OK) @@ -365,28 +376,28 @@ export class AuthController { /** * Invalidate all sessions - * + * * Logs out all sessions for the authenticated user. * Useful for account security after password change or suspected breach. - * + * * @param {Request} req - Express request with user context * @returns {Promise<{message: string}>} Success confirmation */ @Delete('sessions') @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOperation({ + @ApiOperation({ summary: 'Invalidate all sessions for current user', - description: 'Logs out all devices/sessions. Useful after password change or security incident.' + description: 'Logs out all devices/sessions. Useful after password change or security incident.', }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'All sessions invalidated successfully.', schema: { properties: { - message: { type: 'string', example: 'All sessions invalidated successfully' } - } - } + message: { type: 'string', example: 'All sessions invalidated successfully' }, + }, + }, }) @ApiResponse({ status: 401, description: 'Unauthorized.', type: ErrorResponseDto }) @HttpCode(HttpStatus.OK) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 474de78..9932268 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -305,21 +305,13 @@ export class AuthService { } private generateTokens(user: any) { -eat-remove-todo-comments - const jti = uuidv4(); // JWT ID for blacklisting - const payload = { - sub: user.id, - email: user.email, - jti, - // === UNIQUE JWT ID (JTI) === // Enables per-token blacklisting even if JWT signature is still valid const jti = uuidv4(); - const payload = { + const payload = { sub: user.id, // Subject (user ID) email: user.email, - jti: jti // JWT ID for blacklisting - + jti, // JWT ID for blacklisting }; const accessToken = this.jwtService.sign(payload, { diff --git a/src/blockchain/blockchain.module.ts b/src/blockchain/blockchain.module.ts index a4e8b78..9bdd9a0 100644 --- a/src/blockchain/blockchain.module.ts +++ b/src/blockchain/blockchain.module.ts @@ -5,4 +5,4 @@ import { BlockchainService } from './blockchain.service'; providers: [BlockchainService], exports: [BlockchainService], }) -export class BlockchainModule {} \ No newline at end of file +export class BlockchainModule {} diff --git a/src/blockchain/blockchain.service.ts b/src/blockchain/blockchain.service.ts index ec982d0..e876f4f 100644 --- a/src/blockchain/blockchain.service.ts +++ b/src/blockchain/blockchain.service.ts @@ -29,4 +29,4 @@ export class BlockchainService { healthy: true, }; } -} \ No newline at end of file +} diff --git a/src/blockchain/enums/supported-chain.enum.ts b/src/blockchain/enums/supported-chain.enum.ts index 698a01c..f2b82cf 100644 --- a/src/blockchain/enums/supported-chain.enum.ts +++ b/src/blockchain/enums/supported-chain.enum.ts @@ -2,4 +2,4 @@ export enum SupportedChain { ETHEREUM = 'ethereum', POLYGON = 'polygon', BSC = 'bsc', -} \ No newline at end of file +} diff --git a/src/blockchain/providers/provider.factory.ts b/src/blockchain/providers/provider.factory.ts index bdf2730..ae0f1f3 100644 --- a/src/blockchain/providers/provider.factory.ts +++ b/src/blockchain/providers/provider.factory.ts @@ -11,8 +11,10 @@ export class ProviderFactory { }; const rpcUrl = rpcMap[chain]; - if (!rpcUrl) throw new Error(`RPC not configured for ${chain}`); + if (!rpcUrl) { + throw new Error(`RPC not configured for ${chain}`); + } return new JsonRpcProvider(rpcUrl); } -} \ No newline at end of file +} diff --git a/src/common/pagination/PAGINATION.md b/src/common/pagination/PAGINATION.md index 89e22ab..6b8b7c4 100644 --- a/src/common/pagination/PAGINATION.md +++ b/src/common/pagination/PAGINATION.md @@ -43,9 +43,7 @@ export class ItemsController { ) {} @Get() - async findAll( - @Query() paginationQuery: PaginationQueryDto, - ): Promise> { + async findAll(@Query() paginationQuery: PaginationQueryDto): Promise> { return this.itemsService.findAll(paginationQuery); } } @@ -64,9 +62,7 @@ export class ItemsService { private readonly paginationService: PaginationService, ) {} - async findAll( - paginationQuery?: PaginationQueryDto, - ): Promise> { + async findAll(paginationQuery?: PaginationQueryDto): Promise> { if (!paginationQuery) { // Backward compatibility: return all items without pagination return this.prisma.item.findMany(); @@ -107,12 +103,12 @@ export class ItemsModule {} All list endpoints support the following query parameters: -| Parameter | Type | Default | Min | Max | Description | -|-----------|----------|---------|-----|-----|-------------| -| `page` | integer | 1 | 1 | ∞ | Page number (1-indexed) | -| `limit` | integer | 10 | 1 | 100 | Items per page | -| `sortBy` | string | createdAt | - | - | Field to sort by | -| `sortOrder` | enum | desc | - | - | Sort direction (asc or desc) | +| Parameter | Type | Default | Min | Max | Description | +| ----------- | ------- | --------- | --- | --- | ---------------------------- | +| `page` | integer | 1 | 1 | ∞ | Page number (1-indexed) | +| `limit` | integer | 10 | 1 | 100 | Items per page | +| `sortBy` | string | createdAt | - | - | Field to sort by | +| `sortOrder` | enum | desc | - | - | Sort direction (asc or desc) | ## Response Format @@ -142,35 +138,39 @@ All list endpoints support the following query parameters: ### Pagination Metadata Fields -| Field | Type | Description | -|-----------|---------|-------------| -| `total` | number | Total number of items matching the query | -| `page` | number | Current page number | -| `limit` | number | Items per page | -| `pages` | number | Total number of pages | -| `hasNext` | boolean | Whether a next page exists | -| `hasPrev` | boolean | Whether a previous page exists | -| `sortBy` | string | Field used for sorting | -| `sortOrder` | enum | Sort direction (asc or desc) | +| Field | Type | Description | +| ----------- | ------- | ---------------------------------------- | +| `total` | number | Total number of items matching the query | +| `page` | number | Current page number | +| `limit` | number | Items per page | +| `pages` | number | Total number of pages | +| `hasNext` | boolean | Whether a next page exists | +| `hasPrev` | boolean | Whether a previous page exists | +| `sortBy` | string | Field used for sorting | +| `sortOrder` | enum | Sort direction (asc or desc) | ## API Examples ### Get First Page (Default) + ```bash curl https://api.propchain.io/api-keys ``` ### Get Second Page with 20 Items Per Page + ```bash curl "https://api.propchain.io/api-keys?page=2&limit=20" ``` ### Sort by Name in Ascending Order + ```bash curl "https://api.propchain.io/api-keys?page=1&limit=10&sortBy=name&sortOrder=asc" ``` ### Get Maximum Items Per Page + ```bash curl "https://api.propchain.io/api-keys?page=1&limit=100" ``` @@ -238,14 +238,13 @@ Applied: page=1, limit=100 ### Optimization Tips 1. **Always fetch total count and items in parallel** + ```typescript - const [items, total] = await Promise.all([ - this.prisma.item.findMany({ skip, take }), - this.prisma.item.count(), - ]); + const [items, total] = await Promise.all([this.prisma.item.findMany({ skip, take }), this.prisma.item.count()]); ``` 2. **Add database indexes on sort fields** + ```sql CREATE INDEX idx_items_created_at ON items(created_at); CREATE INDEX idx_items_name ON items(name); @@ -258,24 +257,26 @@ Applied: page=1, limit=100 4. **Consider caching for static lists** ```typescript if (!paginationQuery?.page || paginationQuery.page === 1) { - return this.cache.get('items:page:1') ?? - this.fetchAndCacheFirstPage(); + return this.cache.get('items:page:1') ?? this.fetchAndCacheFirstPage(); } ``` ## Testing ### Unit Tests + ```bash npm run test -- pagination.service.spec.ts ``` ### Integration Tests + ```bash npm run test -- api-keys.pagination.spec.ts ``` ### Performance Tests + ```bash npm run test -- pagination.performance.spec.ts ``` @@ -285,6 +286,7 @@ npm run test -- pagination.performance.spec.ts ### Updating Existing Endpoints 1. **Add PaginationService to module** + ```typescript @Module({ providers: [ItemsService, PaginationService], @@ -292,6 +294,7 @@ npm run test -- pagination.performance.spec.ts ``` 2. **Update service method signature** + ```typescript // Before async findAll(): Promise @@ -301,6 +304,7 @@ npm run test -- pagination.performance.spec.ts ``` 3. **Update controller method signature** + ```typescript // Before @Get() @@ -316,16 +320,21 @@ npm run test -- pagination.performance.spec.ts ## Common Issues & Solutions ### Issue: "Can't resolve dependencies of PaginationService" + **Solution**: Ensure `PaginationService` is provided in the module's `providers` array. ### Issue: Maximum limit not enforced + **Solution**: Always use `getPrismaOptions()` or `calculatePagination()` instead of manual offset calculations. ### Issue: Inconsistent sort results + **Solution**: Ensure the sort field exists in the model and add appropriate database indexes. ### Issue: Slow pagination queries -**Solution**: + +**Solution**: + - Add indexes on sort fields - Fetch items and count in parallel - Consider implementing cursor-based pagination for large datasets diff --git a/src/common/pagination/PAGINATION_GUIDE.md b/src/common/pagination/PAGINATION_GUIDE.md index 86397d4..60cfd93 100644 --- a/src/common/pagination/PAGINATION_GUIDE.md +++ b/src/common/pagination/PAGINATION_GUIDE.md @@ -53,12 +53,14 @@ GET /api-keys?page=2&limit=20&sortBy=name&sortOrder=asc ## Query Parameters ### page + - **Type**: integer - **Default**: 1 - **Min**: 1 - **Description**: Page number (1-indexed) ### limit + - **Type**: integer - **Default**: 10 - **Min**: 1 @@ -66,11 +68,13 @@ GET /api-keys?page=2&limit=20&sortBy=name&sortOrder=asc - **Description**: Number of items per page ### sortBy + - **Type**: string - **Default**: createdAt - **Description**: Field to sort by (must be a valid model field) ### sortOrder + - **Type**: string (enum) - **Default**: desc - **Values**: `asc`, `desc` @@ -79,27 +83,35 @@ GET /api-keys?page=2&limit=20&sortBy=name&sortOrder=asc ## Response Metadata ### total + Total number of items matching the filter criteria ### page + Current page number ### limit + Items per page ### pages + Total number of pages available ### hasNext + Boolean indicating if there's a next page ### hasPrev + Boolean indicating if there's a previous page ### sortBy + Field currently sorted by ### sortOrder + Current sort direction ## Implementation Guide @@ -127,7 +139,7 @@ export class ItemService { // Get pagination options for Prisma const { skip, take, orderBy } = this.paginationService.getPrismaOptions( paginationQuery, - 'createdAt' // default sort field + 'createdAt', // default sort field ); // Fetch data and count in parallel @@ -155,9 +167,7 @@ export class ItemController { constructor(private readonly itemService: ItemService) {} @Get() - async findAll( - @Query() paginationQuery: PaginationQueryDto, - ): Promise> { + async findAll(@Query() paginationQuery: PaginationQueryDto): Promise> { return this.itemService.findAll(paginationQuery); } } @@ -167,18 +177,20 @@ export class ItemController { The pagination system enforces these constraints automatically: -| Parameter | Min | Max | Behavior | -|-----------|-----|-----|----------| -| page | 1 | ∞ | Below 1 defaults to 1 | -| limit | 1 | 100 | Above 100 capped at 100 | -| sortOrder | - | - | Must be 'asc' or 'desc' | +| Parameter | Min | Max | Behavior | +| --------- | --- | --- | ----------------------- | +| page | 1 | ∞ | Below 1 defaults to 1 | +| limit | 1 | 100 | Above 100 capped at 100 | +| sortOrder | - | - | Must be 'asc' or 'desc' | ## Sorting ### Default Sorting + By default, results are sorted by `createdAt` in descending order (newest first). ### Custom Sort Fields + ```bash # Sort by email ascending GET /api-keys?page=1&sortBy=email&sortOrder=asc @@ -188,7 +200,9 @@ GET /api-keys?page=1&sortBy=updatedAt&sortOrder=desc ``` ### Valid Sort Fields + Each endpoint documents which fields can be sorted. Generally: + - `createdAt` - `updatedAt` - `name` @@ -198,17 +212,20 @@ Each endpoint documents which fields can be sorted. Generally: ## Performance Considerations ### Large Datasets + - Use reasonable limits (10-50 items) for initial loads - Implement pagination in UI to avoid loading all items - Consider caching frequently accessed pages ### Database Impact + ``` Single query for count + data fetch = 2 database queries Use Promise.all() to execute in parallel ``` ### Optimization Tips + ```typescript // Good: Parallel execution const [items, total] = await Promise.all([ @@ -224,24 +241,31 @@ const total = await this.prisma.item.count(); // Extra query ## Testing ### Unit Tests + Run pagination service tests: + ```bash npm run test:unit -- test/pagination/pagination.service.spec.ts ``` ### Integration Tests + Run full endpoint tests: + ```bash npm run test:integration -- test/pagination/pagination.integration.spec.ts ``` ### Performance Benchmarks + Run performance tests: + ```bash ts-node test/pagination/pagination.performance.ts ``` Expected performance: + - ~100K operations/second for calculatePagination - ~100K operations/second for createMetadata - <1ms for formatResponse with typical data @@ -249,22 +273,26 @@ Expected performance: ## Common Use Cases ### Get Recent Items (First Page) + ```bash GET /api-keys?page=1&limit=10&sortBy=createdAt&sortOrder=desc ``` ### Navigate to Last Page + ```bash GET /api-keys?page=10&limit=10 # Use meta.pages to determine last page ``` ### Search and Paginate + ```bash GET /api-keys?page=1&limit=20&sortBy=name&sortOrder=asc ``` ### Iterate Through All Items + ```typescript let page = 1; let hasMore = true; @@ -272,10 +300,10 @@ let hasMore = true; while (hasMore) { const response = await fetch(`/api-keys?page=${page}&limit=50`); const { data, meta } = await response.json(); - + // Process data processItems(data); - + hasMore = meta.hasNext; page++; } @@ -304,11 +332,13 @@ GET /api-keys?sortOrder=unknown ### Updating Existing Endpoints 1. **Add PaginationService to module providers** + ```typescript -providers: [ItemService, PaginationService] +providers: [ItemService, PaginationService]; ``` 2. **Update service method** + ```typescript // Before async findAll(): Promise { @@ -329,6 +359,7 @@ async findAll(paginationQuery?: PaginationQueryDto) { ``` 3. **Update controller method** + ```typescript // Before async findAll(): Promise { @@ -344,14 +375,18 @@ async findAll(@Query() paginationQuery: PaginationQueryDto) { ## API Compatibility ### Backward Compatibility + Existing endpoints without pagination continue to work. Pagination is additive and optional on the service layer. ### Response Format Changes + When pagination is enabled: + - Response wraps data in `data` field - Adds `meta` object with pagination details ### Version Support + - Works with NestJS 9+ - Works with Prisma 4+ - Compatible with TypeScript 4.5+ @@ -361,6 +396,7 @@ When pagination is enabled: 1. **Always use pagination for list endpoints** - Even if starting with small datasets, plan for growth 2. **Validate sortBy fields** - Maintain a whitelist of sortable fields + ```typescript const SORTABLE_FIELDS = ['createdAt', 'name', 'email']; if (!SORTABLE_FIELDS.includes(sortBy)) { @@ -379,24 +415,32 @@ When pagination is enabled: ## Troubleshooting ### Issue: "hasNext is always false" + Check that you're using `meta.pages` and `meta.page` correctly: + ```typescript const hasNext = page < pages; // Correct const hasNext = page <= pages; // Incorrect ``` ### Issue: "Duplicates across pages" + Ensure consistent sorting with `orderBy`: + ```typescript // Good: Deterministic sorting -orderBy: { createdAt: 'desc' } +orderBy: { + createdAt: 'desc'; +} // Risky: Multiple sort fields needed -orderBy: [{ priority: 'desc' }, { createdAt: 'desc' }] +orderBy: [{ priority: 'desc' }, { createdAt: 'desc' }]; ``` ### Issue: "Performance degradation with large limits" + Remember the 100-item hard limit and pagination in UI: + ```typescript // Always enforced limit = Math.min(limit, 100); diff --git a/src/database/prisma/prisma.service.ts b/src/database/prisma/prisma.service.ts index 321a37a..3a1ca4e 100644 --- a/src/database/prisma/prisma.service.ts +++ b/src/database/prisma/prisma.service.ts @@ -1,4 +1,3 @@ - // PrismaService manages the PrismaClient lifecycle and database connection pooling. // Connection pooling is configured via the DATABASE_URL environment variable. // Example: postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=30 diff --git a/src/properties/properties.service.ts b/src/properties/properties.service.ts index 4fb8ee5..3d670e0 100644 --- a/src/properties/properties.service.ts +++ b/src/properties/properties.service.ts @@ -439,7 +439,6 @@ export class PropertiesService { if (updatePropertyDto.address) { updateData.location = this.formatAddress(updatePropertyDto.address); } - } if (updatePropertyDto.status !== undefined) { updateData.status = this.mapPropertyStatus(updatePropertyDto.status); diff --git a/src/security/README.md b/src/security/README.md index 8a23628..310ca81 100644 --- a/src/security/README.md +++ b/src/security/README.md @@ -5,6 +5,7 @@ This document describes the comprehensive security features implemented in the P ## Features Implemented ### 1. Advanced Rate Limiting + - **Redis-based rate limiting** with sliding window algorithm - **Multiple rate limit tiers**: API, Auth, Expensive operations, User-based - **Customizable configurations** via environment variables @@ -12,6 +13,7 @@ This document describes the comprehensive security features implemented in the P - **Fail-open design** to prevent service disruption ### 2. IP Blocking and Whitelisting + - **Automatic IP blocking** after failed attempts - **Manual IP blocking/unblocking** via API - **IP whitelist** functionality @@ -19,6 +21,7 @@ This document describes the comprehensive security features implemented in the P - **Real-time blocking checks** in middleware ### 3. DDoS Protection + - **Traffic monitoring** and anomaly detection - **Automatic attack mitigation** - **Multiple mitigation strategies**: IP blocking, rate limiting, challenges @@ -26,6 +29,7 @@ This document describes the comprehensive security features implemented in the P - **Configurable thresholds** and response actions ### 4. API Quota Management + - **Plan-based quotas**: Free, Basic, Pro, Enterprise - **Daily and monthly usage tracking** - **Automatic quota reset** schedules @@ -33,6 +37,7 @@ This document describes the comprehensive security features implemented in the P - **Quota enforcement** in API key validation ### 5. Security Headers + - **Content Security Policy (CSP)** with customizable directives - **HTTP Strict Transport Security (HSTS)** - **X-Frame-Options**, **X-Content-Type-Options**, **X-XSS-Protection** @@ -40,6 +45,7 @@ This document describes the comprehensive security features implemented in the P - **Environment-specific configurations** ### 6. Enhanced Authentication Security + - **Enhanced API key guard** with quota and rate limit checking - **Comprehensive validation** including expiration and active status - **Usage tracking** and quota consumption @@ -78,6 +84,7 @@ HSTS_PRELOAD=true ## API Endpoints ### Security Management + - `GET /api/security/rate-limit/:key` - Get rate limit information - `DELETE /api/security/rate-limit/:key` - Reset rate limit - `GET /api/security/ip-blocks` - Get blocked IPs @@ -100,6 +107,7 @@ HSTS_PRELOAD=true ## Usage Examples ### Rate Limiting Decorator + ```typescript import { RateLimit } from '../security/decorators/rate-limit.decorator'; import { AdvancedRateLimitGuard } from '../security/guards/advanced-rate-limit.guard'; @@ -107,12 +115,11 @@ import { AdvancedRateLimitGuard } from '../security/guards/advanced-rate-limit.g @Controller('api/expensive') @UseGuards(AdvancedRateLimitGuard) export class ExpensiveOperationsController { - @Post('operation') @RateLimit({ windowMs: 60000, // 1 minute maxRequests: 10, // 10 requests per minute - keyPrefix: 'expensive_ops' + keyPrefix: 'expensive_ops', }) async performExpensiveOperation() { // Your expensive operation here @@ -121,13 +128,13 @@ export class ExpensiveOperationsController { ``` ### Enhanced API Key Protection + ```typescript import { EnhancedApiKeyGuard } from '../common/guards/api-key.guard'; @Controller('api/protected') @UseGuards(EnhancedApiKeyGuard) export class ProtectedController { - @Get('data') async getData(@Request() req) { // req.apiKey contains quota and usage information @@ -168,6 +175,7 @@ When rate limiting is applied, the following headers are included: ## Monitoring and Logging All security events are logged with appropriate severity levels: + - **WARN**: Rate limit exceeded, IP blocked - **ERROR**: Security service failures - **INFO**: Security operations, configuration changes @@ -175,6 +183,7 @@ All security events are logged with appropriate severity levels: ## Fail-Safe Design The security system is designed with fail-open principles: + - If Redis is unavailable, rate limiting is bypassed - If security services fail, requests are allowed - Critical business operations continue during security service outages @@ -182,6 +191,7 @@ The security system is designed with fail-open principles: ## Testing Comprehensive tests are included for all security features: + - Unit tests for each service - Integration tests for combined functionality - Performance tests for high-load scenarios @@ -190,9 +200,10 @@ Comprehensive tests are included for all security features: ## Future Enhancements Planned improvements: + - Machine learning-based anomaly detection - Geographic IP blocking - Request fingerprinting - Advanced CAPTCHA integration - Rate limit analytics dashboard -- Automated threat intelligence integration \ No newline at end of file +- Automated threat intelligence integration diff --git a/src/transactions/transactions.controller.ts b/src/transactions/transactions.controller.ts index 5642c60..263d45e 100644 --- a/src/transactions/transactions.controller.ts +++ b/src/transactions/transactions.controller.ts @@ -1,5 +1,5 @@ -import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common"; -import { TransactionsService } from "./transactions.service"; +import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { TransactionsService } from './transactions.service'; @Controller('transactions') export class TransactionsController { @@ -29,4 +29,4 @@ export class TransactionsController { dispute(@Param('id') id: string, @Body() dto: DisputeDto) { return this.service.raiseDispute(id, dto); } -} \ No newline at end of file +} diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index e7c0fa6..e680189 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -19,25 +19,22 @@ export class TransactionsService { }; } - private validateTransition( - current: TransactionStatus, - next: TransactionStatus, -) { - const allowedTransitions = { - PENDING: ['ESCROW_FUNDED', 'CANCELLED'], - ESCROW_FUNDED: ['BLOCKCHAIN_SUBMITTED'], - BLOCKCHAIN_SUBMITTED: ['CONFIRMING'], - CONFIRMING: ['CONFIRMED', 'FAILED'], - CONFIRMED: ['COMPLETED'], - }; + private validateTransition(current: TransactionStatus, next: TransactionStatus) { + const allowedTransitions = { + PENDING: ['ESCROW_FUNDED', 'CANCELLED'], + ESCROW_FUNDED: ['BLOCKCHAIN_SUBMITTED'], + BLOCKCHAIN_SUBMITTED: ['CONFIRMING'], + CONFIRMING: ['CONFIRMED', 'FAILED'], + CONFIRMED: ['COMPLETED'], + }; - if (!allowedTransitions[current]?.includes(next)) { - throw new BusinessException( - ERROR_CODES.INVALID_TRANSACTION_STATE, - `Invalid transition from ${current} to ${next}`, - ); + if (!allowedTransitions[current]?.includes(next)) { + throw new BusinessException( + ERROR_CODES.INVALID_TRANSACTION_STATE, + `Invalid transition from ${current} to ${next}`, + ); + } } -} async createTransaction(dto: CreateTransactionDto) { const fees = this.calculateFees(dto.amount); @@ -52,38 +49,38 @@ export class TransactionsService { } async fundEscrow(transactionId: string) { - const tx = await this.getTransaction(transactionId); + const tx = await this.getTransaction(transactionId); - const escrowWallet = await this.blockchainService.createEscrowWallet(); + const escrowWallet = await this.blockchainService.createEscrowWallet(); - await this.prisma.transaction.update({ - where: { id: transactionId }, - data: { - escrowWallet, - status: 'ESCROW_FUNDED', - }, - }); + await this.prisma.transaction.update({ + where: { id: transactionId }, + data: { + escrowWallet, + status: 'ESCROW_FUNDED', + }, + }); - return escrowWallet; -} + return escrowWallet; + } -async monitorBlockchain(transactionId: string) { - const tx = await this.getTransaction(transactionId); + async monitorBlockchain(transactionId: string) { + const tx = await this.getTransaction(transactionId); - if (!tx.blockchainHash) return; + if (!tx.blockchainHash) { + return; + } - const receipt = await this.blockchainService.getTransactionReceipt( - tx.blockchainHash, - ); + const receipt = await this.blockchainService.getTransactionReceipt(tx.blockchainHash); - if (receipt.confirmations >= 6) { - await this.prisma.transaction.update({ - where: { id: tx.id }, - data: { - confirmations: receipt.confirmations, - status: 'CONFIRMED', - }, - }); + if (receipt.confirmations >= 6) { + await this.prisma.transaction.update({ + where: { id: tx.id }, + data: { + confirmations: receipt.confirmations, + status: 'CONFIRMED', + }, + }); + } } } -} diff --git a/src/users/user.service.ts b/src/users/user.service.ts index 0de2726..fa8b5e2 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -12,17 +12,17 @@ import { PasswordValidator } from '../common/validators/password.validator'; /** * UserService - * + * * Handles user account management operations including: * - User registration with password hashing * - User lookup by email or wallet address * - Password updates with validation * - Email verification * - Profile management - * + * * All passwords are hashed using bcrypt with configurable salt rounds. * Ensures data integrity through unique constraint validation. - * + * * @class UserService * @injectable */ @@ -35,19 +35,19 @@ export class UserService { /** * Create a new user account - * + * * Performs comprehensive validation: * - Password strength (minimum 8 chars, mixed case, numbers, special chars) * - Email and wallet address uniqueness - * + * * Passwords are hashed using bcrypt with saltRounds from config (default: 12). * Default role is 'USER' - can be elevated by administrators. - * + * * @param {CreateUserDto} createUserDto - User data (email, password, firstName, lastName, walletAddress) * @returns {Promise} Created user object (password removed from response) * @throws {BadRequestException} If password doesn't meet strength requirements * @throws {ConflictException} If email or wallet already registered - * + * * @example * ```typescript * const user = await userService.create({ @@ -108,10 +108,10 @@ export class UserService { /** * Find user by email address - * + * * @param {string} email - Email address to search for * @returns {Promise} User object if found, null otherwise - * + * * @example * ```typescript * const user = await userService.findByEmail('user@example.com'); @@ -125,11 +125,11 @@ export class UserService { /** * Find user by ID - * + * * @param {string} id - User ID to search for * @returns {Promise} User object * @throws {NotFoundException} If user doesn't exist - * + * * @example * ```typescript * const user = await userService.findById('clx123abc'); @@ -149,12 +149,12 @@ export class UserService { /** * Find user by blockchain wallet address - * + * * Supports Web3 authentication without traditional email/password. - * + * * @param {string} walletAddress - Blockchain wallet address (e.g., 0x...) * @returns {Promise} User object if found, null otherwise - * + * * @example * ```typescript * const user = await userService.findByWalletAddress('0x742d35Cc6634C0532925a3b844Bc59e4e7aa6cA6'); @@ -168,16 +168,16 @@ export class UserService { /** * Update user password with validation - * + * * Validates new password strength before updating. * Uses bcrypt for secure hashing. - * + * * @param {string} userId - ID of user whose password to update * @param {string} newPassword - New password (must pass strength validation) * @returns {Promise} Updated user object * @throws {BadRequestException} If password doesn't meet strength requirements * @throws {NotFoundException} If user doesn't exist - * + * * @example * ```typescript * await userService.updatePassword(userId, 'NewSecurePass123!'); @@ -204,14 +204,14 @@ export class UserService { /** * Mark user email as verified - * + * * Called after successful email verification. * Sets isVerified flag to true. - * + * * @param {string} userId - ID of user to verify * @returns {Promise} Updated user object * @throws {NotFoundException} If user doesn't exist - * + * * @example * ```typescript * await userService.verifyUser(userId); @@ -227,10 +227,10 @@ export class UserService { /** * Update user profile information - * + * * Supports partial updates for email, wallet address, and active status. * Validates uniqueness of new email and wallet address. - * + * * @param {string} id - User ID to update * @param {Object} data - Data to update * @param {string} [data.email] - New email address @@ -239,7 +239,7 @@ export class UserService { * @returns {Promise} Updated user object * @throws {ConflictException} If email or wallet already taken by another user * @throws {NotFoundException} If user doesn't exist - * + * * @example * ```typescript * await userService.updateUser(userId, { @@ -354,12 +354,16 @@ export class UserService { * Follow another user */ async followUser(followerId: string, followingId: string) { - if (followerId === followingId) throw new BadRequestException('Cannot follow yourself'); + if (followerId === followingId) { + throw new BadRequestException('Cannot follow yourself'); + } // Prevent duplicate follows const existing = await this.prisma.userRelationship.findUnique({ where: { followerId_followingId: { followerId, followingId } }, }); - if (existing) return existing; + if (existing) { + return existing; + } return this.prisma.userRelationship.create({ data: { followerId, followingId }, }); diff --git a/test/auth/mfa.service.spec.ts b/test/auth/mfa.service.spec.ts index 83d5212..c6394df 100644 --- a/test/auth/mfa.service.spec.ts +++ b/test/auth/mfa.service.spec.ts @@ -11,7 +11,7 @@ describe('MfaService', () => { const mockConfigService = { get: jest.fn().mockImplementation((key: string) => { const config = { - 'MFA_CODE_EXPIRY': 300, + MFA_CODE_EXPIRY: 300, }; return config[key]; }), @@ -51,61 +51,54 @@ describe('MfaService', () => { describe('generateMfaSecret', () => { it('should generate MFA secret and QR code', async () => { const result = await service.generateMfaSecret('user123', 'test@example.com'); - + expect(result).toHaveProperty('secret'); expect(result).toHaveProperty('qrCode'); // secret is base32; length may vary but should be at least 32 chars expect(result.secret.length).toBeGreaterThanOrEqual(32); expect(result.qrCode).toContain('data:image/png;base64'); - expect(mockRedisService.setex).toHaveBeenCalledWith( - 'mfa_setup:user123', - 300, - expect.any(String) - ); + expect(mockRedisService.setex).toHaveBeenCalledWith('mfa_setup:user123', 300, expect.any(String)); }); }); describe('verifyMfaSetup', () => { it('should verify MFA setup with valid token', async () => { mockRedisService.get.mockResolvedValue('JBSWY3DPEHPK3PXP'); - + // Spy on speakeasy verify method to return true const speakeasy = require('speakeasy'); jest.spyOn(speakeasy.totp, 'verify').mockReturnValue(true); const result = await service.verifyMfaSetup('user123', '123456'); - + expect(result).toBe(true); - expect(mockRedisService.set).toHaveBeenCalledWith( - 'mfa_secret:user123', - 'JBSWY3DPEHPK3PXP' - ); + expect(mockRedisService.set).toHaveBeenCalledWith('mfa_secret:user123', 'JBSWY3DPEHPK3PXP'); expect(mockRedisService.del).toHaveBeenCalledWith('mfa_setup:user123'); }); it('should throw error for expired setup session', async () => { mockRedisService.get.mockResolvedValue(null); - - await expect(service.verifyMfaSetup('user123', '123456')) - .rejects - .toThrow('MFA setup session expired or not found'); + + await expect(service.verifyMfaSetup('user123', '123456')).rejects.toThrow( + 'MFA setup session expired or not found', + ); }); }); describe('isMfaEnabled', () => { it('should return true when MFA is enabled', async () => { mockRedisService.get.mockResolvedValue('JBSWY3DPEHPK3PXP'); - + const result = await service.isMfaEnabled('user123'); - + expect(result).toBe(true); }); it('should return false when MFA is not enabled', async () => { mockRedisService.get.mockResolvedValue(null); - + const result = await service.isMfaEnabled('user123'); - + expect(result).toBe(false); }); }); @@ -113,17 +106,17 @@ describe('MfaService', () => { describe('generateBackupCodes', () => { it('should generate 10 backup codes', async () => { const result = await service.generateBackupCodes('user123'); - + expect(result).toHaveLength(10); result.forEach(code => { expect(code).toHaveLength(8); expect(code).toMatch(/^[A-Z0-9]+$/); }); - + expect(mockRedisService.setex).toHaveBeenCalledWith( 'mfa_backup_codes:user123', 3600, // 300 * 12 - JSON.stringify(result) + JSON.stringify(result), ); }); }); @@ -133,9 +126,9 @@ describe('MfaService', () => { mockRedisService.get .mockResolvedValueOnce('JBSWY3DPEHPK3PXP') // mfa_secret .mockResolvedValueOnce(JSON.stringify(['ABC123', 'DEF456'])); // backup_codes - + const result = await service.getMfaStatus('user123'); - + expect(result).toEqual({ enabled: true, hasBackupCodes: true, @@ -146,13 +139,13 @@ describe('MfaService', () => { mockRedisService.get .mockResolvedValueOnce(null) // mfa_secret .mockResolvedValueOnce(null); // backup_codes - + const result = await service.getMfaStatus('user123'); - + expect(result).toEqual({ enabled: false, hasBackupCodes: false, }); }); }); -}); \ No newline at end of file +}); diff --git a/test/auth/security.e2e-spec.ts b/test/auth/security.e2e-spec.ts index ca5c267..be023d7 100644 --- a/test/auth/security.e2e-spec.ts +++ b/test/auth/security.e2e-spec.ts @@ -44,24 +44,20 @@ describe('Authentication Security (e2e)', () => { describe('Token Blacklisting', () => { it('should blacklist token on logout', async () => { // Register a test user - const registerResponse = await request(app.getHttpServer()) - .post('/auth/register') - .send({ - email: 'test@example.com', - password: 'SecurePass123!', - firstName: 'Test', - lastName: 'User', - }); + const registerResponse = await request(app.getHttpServer()).post('/auth/register').send({ + email: 'test@example.com', + password: 'SecurePass123!', + firstName: 'Test', + lastName: 'User', + }); expect(registerResponse.status).toBe(201); // Login to get tokens - const loginResponse = await request(app.getHttpServer()) - .post('/auth/login') - .send({ - email: 'test@example.com', - password: 'SecurePass123!', - }); + const loginResponse = await request(app.getHttpServer()).post('/auth/login').send({ + email: 'test@example.com', + password: 'SecurePass123!', + }); expect(loginResponse.status).toBe(200); const { access_token } = loginResponse.body; @@ -85,25 +81,21 @@ describe('Authentication Security (e2e)', () => { describe('Brute Force Protection', () => { it('should lock account after too many failed attempts', async () => { const email = 'brute-force@test.com'; - + // Register test user - await request(app.getHttpServer()) - .post('/auth/register') - .send({ - email, - password: 'SecurePass123!', - firstName: 'Test', - lastName: 'User', - }); + await request(app.getHttpServer()).post('/auth/register').send({ + email, + password: 'SecurePass123!', + firstName: 'Test', + lastName: 'User', + }); // Make 6 failed login attempts for (let i = 0; i < 6; i++) { - const response = await request(app.getHttpServer()) - .post('/auth/login') - .send({ - email, - password: 'wrong-password', - }); + const response = await request(app.getHttpServer()).post('/auth/login').send({ + email, + password: 'wrong-password', + }); if (i < 5) { expect(response.status).toBe(401); @@ -115,12 +107,10 @@ describe('Authentication Security (e2e)', () => { } // Correct password should still fail when locked - const correctResponse = await request(app.getHttpServer()) - .post('/auth/login') - .send({ - email, - password: 'SecurePass123!', - }); + const correctResponse = await request(app.getHttpServer()).post('/auth/login').send({ + email, + password: 'SecurePass123!', + }); expect(correctResponse.status).toBe(401); expect(correctResponse.body.message).toContain('locked'); @@ -155,25 +145,19 @@ describe('Authentication Security (e2e)', () => { }); it('should accept strong passwords', async () => { - const strongPasswords = [ - 'SecurePass123!', - 'MyStr0ngP@ssw0rd', - 'Complex123#Password', - ]; + const strongPasswords = ['SecurePass123!', 'MyStr0ngP@ssw0rd', 'Complex123#Password']; for (const password of strongPasswords) { const email = `${Math.random()}@test.com`; - const response = await request(app.getHttpServer()) - .post('/auth/register') - .send({ - email, - password, - firstName: 'Test', - lastName: 'User', - }); + const response = await request(app.getHttpServer()).post('/auth/register').send({ + email, + password, + firstName: 'Test', + lastName: 'User', + }); expect(response.status).toBe(201); - + // Cleanup await prismaService.user.delete({ where: { email } }); } @@ -184,21 +168,17 @@ describe('Authentication Security (e2e)', () => { it('should manage active sessions', async () => { // Register and login const email = 'session@test.com'; - await request(app.getHttpServer()) - .post('/auth/register') - .send({ - email, - password: 'SecurePass123!', - firstName: 'Test', - lastName: 'User', - }); - - const loginResponse = await request(app.getHttpServer()) - .post('/auth/login') - .send({ - email, - password: 'SecurePass123!', - }); + await request(app.getHttpServer()).post('/auth/register').send({ + email, + password: 'SecurePass123!', + firstName: 'Test', + lastName: 'User', + }); + + const loginResponse = await request(app.getHttpServer()).post('/auth/login').send({ + email, + password: 'SecurePass123!', + }); const { access_token } = loginResponse.body; @@ -227,4 +207,4 @@ describe('Authentication Security (e2e)', () => { expect(invalidateAllResponse.status).toBe(200); }); }); -}); \ No newline at end of file +}); diff --git a/test/documents/document.controller.spec.ts b/test/documents/document.controller.spec.ts index 53dd085..05dc582 100644 --- a/test/documents/document.controller.spec.ts +++ b/test/documents/document.controller.spec.ts @@ -3,19 +3,18 @@ import { DocumentAccessLevel, DocumentType } from '../../src/documents/document. import { DocumentService } from '../../src/documents/document.service'; describe('DocumentController', () => { - const createMockFile = (): Express.Multer.File => - ({ - fieldname: 'file', - originalname: 'photo.png', - encoding: '7bit', - mimetype: 'image/png', - size: 10, - buffer: Buffer.from('image'), - stream: null as unknown as Express.Multer.File['stream'], - destination: '', - filename: '', - path: '', - }); + const createMockFile = (): Express.Multer.File => ({ + fieldname: 'file', + originalname: 'photo.png', + encoding: '7bit', + mimetype: 'image/png', + size: 10, + buffer: Buffer.from('image'), + stream: null as unknown as Express.Multer.File['stream'], + destination: '', + filename: '', + path: '', + }); it('parses metadata and forwards upload request', async () => { const service: Partial = { diff --git a/test/documents/document.service.spec.ts b/test/documents/document.service.spec.ts index db17d0c..d3848e7 100644 --- a/test/documents/document.service.spec.ts +++ b/test/documents/document.service.spec.ts @@ -15,19 +15,18 @@ const createMockFile = ( buffer: Buffer, mimetype = 'application/pdf', originalname = 'document.pdf', -): Express.Multer.File => - ({ - fieldname: 'file', - originalname, - encoding: '7bit', - mimetype, - size: buffer.length, - buffer, - stream: null as unknown as Express.Multer.File['stream'], - destination: '', - filename: '', - path: '', - }); +): Express.Multer.File => ({ + fieldname: 'file', + originalname, + encoding: '7bit', + mimetype, + size: buffer.length, + buffer, + stream: null as unknown as Express.Multer.File['stream'], + destination: '', + filename: '', + path: '', +}); describe('DocumentService', () => { let service: DocumentService; diff --git a/test/e2e-setup.ts b/test/e2e-setup.ts index 3b8a208..46f1ffe 100644 --- a/test/e2e-setup.ts +++ b/test/e2e-setup.ts @@ -10,7 +10,7 @@ beforeAll(async () => { process.env.DATABASE_URL = process.env.E2E_DATABASE_URL || 'postgresql://test:test@localhost:5432/propchain_e2e'; process.env.REDIS_URL = process.env.E2E_REDIS_URL || 'redis://localhost:6379/3'; process.env.PORT = '0'; // Use random port - + console.log('Setting up E2E test environment...'); }); @@ -25,45 +25,47 @@ afterAll(async () => { ConfigModule.forRoot({ isGlobal: true, ignoreEnvFile: true, - load: [() => ({ - NODE_ENV: 'test', - DATABASE_URL: process.env.DATABASE_URL, - REDIS_URL: process.env.REDIS_URL, - JWT_SECRET: 'e2e-test-jwt-secret', - JWT_EXPIRES_IN: '1h', - S3_BUCKET: 'e2e-test-bucket', - S3_REGION: 'us-east-1', - PORT: '0', - })], + load: [ + () => ({ + NODE_ENV: 'test', + DATABASE_URL: process.env.DATABASE_URL, + REDIS_URL: process.env.REDIS_URL, + JWT_SECRET: 'e2e-test-jwt-secret', + JWT_EXPIRES_IN: '1h', + S3_BUCKET: 'e2e-test-bucket', + S3_REGION: 'us-east-1', + PORT: '0', + }), + ], }), ...moduleImports, ], }).compile(); const app = moduleRef.createNestApplication(); - + // Configure app for testing app.enableCors({ origin: '*', credentials: true, }); - + await app.init(); - + return app; }; // HTTP request utilities (global as any).makeRequest = (app: INestApplication) => { const agent = request.agent(app.getHttpServer()); - + return { get: (url: string) => agent.get(url), post: (url: string) => agent.post(url), put: (url: string) => agent.put(url), patch: (url: string) => agent.patch(url), delete: (url: string) => agent.delete(url), - + // Auth helpers withAuth: (token: string) => ({ get: (url: string) => agent.get(url).set('Authorization', `Bearer ${token}`), @@ -72,7 +74,7 @@ afterAll(async () => { patch: (url: string) => agent.patch(url).set('Authorization', `Bearer ${token}`), delete: (url: string) => agent.delete(url).set('Authorization', `Bearer ${token}`), }), - + // API key helpers withApiKey: (apiKey: string) => ({ get: (url: string) => agent.get(url).set('X-API-Key', apiKey), @@ -86,7 +88,8 @@ afterAll(async () => { // Test data factories for E2E (global as any).createE2ETestUser = async (app: INestApplication) => { - const response = await (global as any).makeRequest(app) + const response = await (global as any) + .makeRequest(app) .post('/auth/register') .send({ email: 'e2e-test@example.com', @@ -100,16 +103,14 @@ afterAll(async () => { }; (global as any).loginE2ETestUser = async (app: INestApplication, email: string, password: string) => { - const response = await (global as any).makeRequest(app) - .post('/auth/login') - .send({ email, password }) - .expect(200); + const response = await (global as any).makeRequest(app).post('/auth/login').send({ email, password }).expect(200); return response.body.access_token; }; (global as any).createE2ETestProperty = async (app: INestApplication, token: string) => { - const response = await (global as any).makeRequest(app) + const response = await (global as any) + .makeRequest(app) .withAuth(token) .post('/properties') .send({ @@ -140,31 +141,30 @@ afterAll(async () => { (global as any).testUserFlow = async (app: INestApplication) => { // 1. Register user const user = await (global as any).createE2ETestUser(app); - + // 2. Login user const token = await (global as any).loginE2ETestUser(app, 'e2e-test@example.com', 'TestPassword123!'); - + // 3. Create property const property = await (global as any).createE2ETestProperty(app, token); - + // 4. Get property - const retrievedProperty = await (global as any).makeRequest(app) + const retrievedProperty = await (global as any) + .makeRequest(app) .withAuth(token) .get(`/properties/${property.id}`) .expect(200); - + // 5. Update property - const updatedProperty = await (global as any).makeRequest(app) + const updatedProperty = await (global as any) + .makeRequest(app) .withAuth(token) .patch(`/properties/${property.id}`) .send({ status: 'PENDING' }) .expect(200); - + // 6. Delete property - await (global as any).makeRequest(app) - .withAuth(token) - .delete(`/properties/${property.id}`) - .expect(200); - + await (global as any).makeRequest(app).withAuth(token).delete(`/properties/${property.id}`).expect(200); + return { user, token, property }; }; diff --git a/test/integration-setup.ts b/test/integration-setup.ts index 5b697a2..488452a 100644 --- a/test/integration-setup.ts +++ b/test/integration-setup.ts @@ -5,9 +5,10 @@ import { ConfigModule } from '@nestjs/config'; beforeAll(async () => { // Set integration test environment process.env.NODE_ENV = 'test'; - process.env.DATABASE_URL = process.env.TEST_DATABASE_URL || 'postgresql://test:test@localhost:5432/propchain_integration'; + process.env.DATABASE_URL = + process.env.TEST_DATABASE_URL || 'postgresql://test:test@localhost:5432/propchain_integration'; process.env.REDIS_URL = process.env.TEST_REDIS_URL || 'redis://localhost:6379/2'; - + console.log('Setting up integration test environment...'); }); @@ -18,14 +19,7 @@ afterAll(async () => { // Database cleanup utilities (global as any).cleanupDatabase = async (prisma: any) => { // Clean up in order to respect foreign key constraints - const tables = [ - 'transaction', - 'document', - 'property', - 'api_key', - 'user_session', - 'user', - ]; + const tables = ['transaction', 'document', 'property', 'api_key', 'user_session', 'user']; for (const table of tables) { try { @@ -107,15 +101,17 @@ afterAll(async () => { ConfigModule.forRoot({ isGlobal: true, ignoreEnvFile: true, - load: [() => ({ - NODE_ENV: 'test', - DATABASE_URL: process.env.DATABASE_URL, - REDIS_URL: process.env.REDIS_URL, - JWT_SECRET: 'integration-test-jwt-secret', - JWT_EXPIRES_IN: '1h', - S3_BUCKET: 'integration-test-bucket', - S3_REGION: 'us-east-1', - })], + load: [ + () => ({ + NODE_ENV: 'test', + DATABASE_URL: process.env.DATABASE_URL, + REDIS_URL: process.env.REDIS_URL, + JWT_SECRET: 'integration-test-jwt-secret', + JWT_EXPIRES_IN: '1h', + S3_BUCKET: 'integration-test-bucket', + S3_REGION: 'us-east-1', + }), + ], }), ...imports, ], diff --git a/test/jest-e2e.json b/test/jest-e2e.json index 06729f6..1a57fcf 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -9,11 +9,6 @@ "moduleNameMapper": { "^src/(.*)$": "/../src/$1" }, - "collectCoverageFrom": [ - "src/**/*.(t|j)s" - ], - "testPathIgnorePatterns": [ - "/node_modules/", - "/dist/" - ] + "collectCoverageFrom": ["src/**/*.(t|j)s"], + "testPathIgnorePatterns": ["/node_modules/", "/dist/"] } diff --git a/test/performance-setup.ts b/test/performance-setup.ts index 5467ef2..0ef117b 100644 --- a/test/performance-setup.ts +++ b/test/performance-setup.ts @@ -3,7 +3,7 @@ import { performance } from 'perf_hooks'; // Performance test setup beforeAll(async () => { console.log('Setting up performance test environment...'); - + // Set performance-specific environment process.env.NODE_ENV = 'performance'; process.env.LOG_LEVEL = 'error'; // Reduce noise during performance tests @@ -16,19 +16,19 @@ afterAll(async () => { // Performance measurement utilities global.measurePerformance = async (name: string, fn: () => Promise | any, iterations = 1) => { const results = []; - + for (let i = 0; i < iterations; i++) { const start = performance.now(); await fn(); const end = performance.now(); results.push(end - start); } - + const avg = results.reduce((sum, time) => sum + time, 0) / results.length; const min = Math.min(...results); const max = Math.max(...results); const median = results.sort((a, b) => a - b)[Math.floor(results.length / 2)]; - + return { name, iterations, @@ -42,20 +42,20 @@ global.measurePerformance = async (name: string, fn: () => Promise | any, i global.assertPerformance = async (name: string, fn: () => Promise | any, maxTimeMs: number, iterations = 1) => { const result = await measurePerformance(name, fn, iterations); - + console.log(`Performance: ${name}`); console.log(` Average: ${result.average.toFixed(2)}ms`); console.log(` Min: ${result.min.toFixed(2)}ms`); console.log(` Max: ${result.max.toFixed(2)}ms`); console.log(` Median: ${result.median.toFixed(2)}ms`); console.log(` Iterations: ${iterations}`); - + if (result.average > maxTimeMs) { throw new Error( - `Performance test failed: ${name} average time ${result.average.toFixed(2)}ms exceeds maximum allowed ${maxTimeMs}ms` + `Performance test failed: ${name} average time ${result.average.toFixed(2)}ms exceeds maximum allowed ${maxTimeMs}ms`, ); } - + return result; }; @@ -64,12 +64,12 @@ global.runLoadTest = async (name: string, fn: () => Promise, concurrency = const startTime = performance.now(); const promises: Promise[] = []; const results: number[] = []; - + // Create batches of concurrent requests for (let i = 0; i < totalRequests; i += concurrency) { const batch = Math.min(concurrency, totalRequests - i); const batchPromises = []; - + for (let j = 0; j < batch; j++) { const promise = (async () => { const start = performance.now(); @@ -83,25 +83,25 @@ global.runLoadTest = async (name: string, fn: () => Promise, concurrency = return { success: false, time: performance.now() - start, error }; } })(); - + batchPromises.push(promise); } - + promises.push(Promise.all(batchPromises)); } - + const batchResults = await Promise.all(promises); const endTime = performance.now(); - + const flatResults = batchResults.flat(); const successful = flatResults.filter(r => r.success); const failed = flatResults.filter(r => !r.success); - + const totalTime = endTime - startTime; const requestsPerSecond = (totalRequests / totalTime) * 1000; const avgResponseTime = results.reduce((sum, time) => sum + time, 0) / results.length; const successRate = (successful.length / totalRequests) * 100; - + const loadTestResult = { name, totalRequests, @@ -114,7 +114,7 @@ global.runLoadTest = async (name: string, fn: () => Promise, concurrency = failed: failed.length, results: flatResults, }; - + console.log(`Load Test: ${name}`); console.log(` Total Requests: ${totalRequests}`); console.log(` Concurrency: ${concurrency}`); @@ -124,17 +124,21 @@ global.runLoadTest = async (name: string, fn: () => Promise, concurrency = console.log(` Success Rate: ${successRate.toFixed(2)}%`); console.log(` Successful: ${successful.length}`); console.log(` Failed: ${failed.length}`); - + return loadTestResult; }; -global.assertLoadTest = async (name: string, fn: () => Promise, options: { - concurrency?: number; - totalRequests?: number; - minRequestsPerSecond?: number; - maxAvgResponseTime?: number; - minSuccessRate?: number; -}) => { +global.assertLoadTest = async ( + name: string, + fn: () => Promise, + options: { + concurrency?: number; + totalRequests?: number; + minRequestsPerSecond?: number; + maxAvgResponseTime?: number; + minSuccessRate?: number; + }, +) => { const { concurrency = 10, totalRequests = 100, @@ -142,33 +146,27 @@ global.assertLoadTest = async (name: string, fn: () => Promise, options: { maxAvgResponseTime = 1000, minSuccessRate = 95, } = options; - + const result = await runLoadTest(name, fn, concurrency, totalRequests); - + const failures = []; - + if (result.requestsPerSecond < minRequestsPerSecond) { - failures.push( - `Requests/sec ${result.requestsPerSecond.toFixed(2)} below minimum ${minRequestsPerSecond}` - ); + failures.push(`Requests/sec ${result.requestsPerSecond.toFixed(2)} below minimum ${minRequestsPerSecond}`); } - + if (result.avgResponseTime > maxAvgResponseTime) { - failures.push( - `Avg response time ${result.avgResponseTime.toFixed(2)}ms exceeds maximum ${maxAvgResponseTime}ms` - ); + failures.push(`Avg response time ${result.avgResponseTime.toFixed(2)}ms exceeds maximum ${maxAvgResponseTime}ms`); } - + if (result.successRate < minSuccessRate) { - failures.push( - `Success rate ${result.successRate.toFixed(2)}% below minimum ${minSuccessRate}%` - ); + failures.push(`Success rate ${result.successRate.toFixed(2)}% below minimum ${minSuccessRate}%`); } - + if (failures.length > 0) { throw new Error(`Load test failed: ${name}\n${failures.join('\n')}`); } - + return result; }; @@ -185,18 +183,20 @@ global.getMemoryUsage = () => { global.monitorMemory = (name: string, fn: () => Promise | any) => { const beforeMemory = getMemoryUsage(); - + const runFn = async () => { const result = await fn(); const afterMemory = getMemoryUsage(); - + console.log(`Memory Usage: ${name}`); console.log(` Before: RSS=${beforeMemory.rss}MB, Heap=${beforeMemory.heapUsed}MB`); console.log(` After: RSS=${afterMemory.rss}MB, Heap=${afterMemory.heapUsed}MB`); - console.log(` Diff: RSS=${afterMemory.rss - beforeMemory.rss}MB, Heap=${afterMemory.heapUsed - beforeMemory.heapUsed}MB`); - + console.log( + ` Diff: RSS=${afterMemory.rss - beforeMemory.rss}MB, Heap=${afterMemory.heapUsed - beforeMemory.heapUsed}MB`, + ); + return result; }; - + return runFn(); }; diff --git a/test/properties/properties.controller.spec.ts b/test/properties/properties.controller.spec.ts index efde249..459814e 100644 --- a/test/properties/properties.controller.spec.ts +++ b/test/properties/properties.controller.spec.ts @@ -172,8 +172,7 @@ describe('PropertiesController', () => { }); }); - describe('searchNearby', () => { - }); + describe('searchNearby', () => {}); describe('getStatistics', () => { it('should return property statistics', async () => { diff --git a/test/properties/properties.service.spec.ts b/test/properties/properties.service.spec.ts index 603f662..8831464 100644 --- a/test/properties/properties.service.spec.ts +++ b/test/properties/properties.service.spec.ts @@ -41,7 +41,7 @@ describe('PropertiesService', () => { yearBuilt: null, lotSize: null, latitude: 40.7128, - longitude: -74.0060, + longitude: -74.006, }; const mockPrismaService = { diff --git a/test/security-setup.ts b/test/security-setup.ts index c9edc22..6c4237d 100644 --- a/test/security-setup.ts +++ b/test/security-setup.ts @@ -4,7 +4,7 @@ import * as path from 'path'; // Security test setup beforeAll(async () => { console.log('Setting up security test environment...'); - + // Set security-specific environment process.env.NODE_ENV = 'security'; process.env.LOG_LEVEL = 'error'; // Reduce noise during security tests @@ -18,7 +18,7 @@ afterAll(async () => { global.assertSecurityHeaders = (response: any, expectedHeaders: Record) => { const headers = response.headers; const missingHeaders = []; - + for (const [header, expectedValue] of Object.entries(expectedHeaders)) { const actualValue = headers[header.toLowerCase()]; if (!actualValue) { @@ -27,31 +27,24 @@ global.assertSecurityHeaders = (response: any, expectedHeaders: Record 0) { throw new Error(`Missing or incorrect security headers: ${missingHeaders.join(', ')}`); } }; global.assertNoSensitiveData = (response: any) => { - const sensitivePatterns = [ - /password/i, - /secret/i, - /token/i, - /key/i, - /credential/i, - /auth/i, - ]; - + const sensitivePatterns = [/password/i, /secret/i, /token/i, /key/i, /credential/i, /auth/i]; + const responseBody = JSON.stringify(response.body); const violations = []; - + for (const pattern of sensitivePatterns) { if (pattern.test(responseBody)) { violations.push(pattern.source); } } - + if (violations.length > 0) { throw new Error(`Sensitive data exposed in response: ${violations.join(', ')}`); } @@ -60,7 +53,7 @@ global.assertNoSensitiveData = (response: any) => { global.assertRateLimiting = async (makeRequest: () => Promise, limit: number, windowMs: number) => { const requests = []; const startTime = Date.now(); - + // Make requests rapidly for (let i = 0; i < limit + 5; i++) { try { @@ -78,26 +71,26 @@ global.assertRateLimiting = async (makeRequest: () => Promise, limit: numbe }); } } - + const successful = requests.filter(r => r.success); const rateLimited = requests.filter(r => r.status === 429); - + if (rateLimited.length === 0) { throw new Error('No rate limiting detected - expected 429 responses'); } - + if (successful.length > limit) { throw new Error(`Rate limiting not enforced - ${successful.length} successful requests (limit: ${limit})`); } - + console.log(`Rate limiting test passed: ${successful.length} successful, ${rateLimited.length} rate limited`); - + return { successful, rateLimited, requests }; }; global.assertInputValidation = async (makeRequest: (payload: any) => Promise, invalidPayloads: any[]) => { const violations = []; - + for (const payload of invalidPayloads) { try { const response = await makeRequest(payload); @@ -117,11 +110,13 @@ global.assertInputValidation = async (makeRequest: (payload: any) => Promise 0) { - throw new Error(`Input validation failed:\n${violations.map(v => `- Payload: ${JSON.stringify(v.payload)} - ${v.error}`).join('\n')}`); + throw new Error( + `Input validation failed:\n${violations.map(v => `- Payload: ${JSON.stringify(v.payload)} - ${v.error}`).join('\n')}`, + ); } - + console.log(`Input validation passed: ${invalidPayloads.length} invalid payloads rejected`); }; @@ -137,7 +132,7 @@ global.assertAuthenticationRequired = async (makeRequest: () => Promise) => throw new Error(`Expected 401 Unauthorized but got ${status}`); } } - + console.log('Authentication requirement verified'); }; @@ -153,7 +148,7 @@ global.assertAuthorizationRequired = async (makeRequest: () => Promise) => throw new Error(`Expected 403 Forbidden but got ${status}`); } } - + console.log('Authorization requirement verified'); }; @@ -169,25 +164,17 @@ global.assertSqlInjectionSafe = async (makeRequest: (input: string) => Promise Promise 0) { - throw new Error(`SQL Injection vulnerabilities detected:\n${violations.map(v => `- Payload: "${v.payload}" - ${v.error}`).join('\n')}`); + throw new Error( + `SQL Injection vulnerabilities detected:\n${violations.map(v => `- Payload: "${v.payload}" - ${v.error}`).join('\n')}`, + ); } - + console.log(`SQL injection safety verified: ${sqlInjectionPayloads.length} payloads tested`); }; @@ -238,14 +227,14 @@ global.assertXssSafe = async (makeRequest: (input: string) => Promise) => { '