diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 5ef4d00..e4d4f27 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -14,10 +14,13 @@ import { AlertsModule } from './modules/alerts/alerts.module'; import { ReportsModule } from '../../../src/modules/reports/reports.module'; import { ProfileModule } from './modules/profile/profile.module'; +import { PrismaModule } from './database/prisma.module'; +import { IncidentsModule } from './modules/incidents/incidents.module'; @Module({ imports: [ DatabaseModule, + PrismaModule, HealthModule, NotificationsModule, ReportingModule, @@ -30,6 +33,7 @@ import { ProfileModule } from './modules/profile/profile.module'; NotesModule, AlertsModule, ProfileModule, + IncidentsModule, ], controllers: [AppController], }) diff --git a/apps/backend/src/common/decorators/current-user.decorator.ts b/apps/backend/src/common/decorators/current-user.decorator.ts new file mode 100644 index 0000000..e7e3a7e --- /dev/null +++ b/apps/backend/src/common/decorators/current-user.decorator.ts @@ -0,0 +1,10 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { AuthenticatedUser } from '../interfaces/authenticated-user.interface'; + +export const CurrentUser = createParamDecorator( + (data: keyof AuthenticatedUser | undefined, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const user: AuthenticatedUser = request.user; + return data ? user?.[data] : user; + }, +); diff --git a/apps/backend/src/common/decorators/roles.decorator.ts b/apps/backend/src/common/decorators/roles.decorator.ts new file mode 100644 index 0000000..e108a9c --- /dev/null +++ b/apps/backend/src/common/decorators/roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; +import { Role } from '../enums/role.enum'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); diff --git a/apps/backend/src/common/enums/role.enum.ts b/apps/backend/src/common/enums/role.enum.ts new file mode 100644 index 0000000..75e217a --- /dev/null +++ b/apps/backend/src/common/enums/role.enum.ts @@ -0,0 +1,5 @@ +export enum Role { + ADMIN = 'admin', + ANALYST = 'analyst', + VIEWER = 'viewer', +} diff --git a/apps/backend/src/common/guards/jwt-auth.guard.ts b/apps/backend/src/common/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..dd1e4a6 --- /dev/null +++ b/apps/backend/src/common/guards/jwt-auth.guard.ts @@ -0,0 +1,51 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { AuthenticatedUser } from '../interfaces/authenticated-user.interface'; + +interface JwtPayload { + sub: string; + email?: string; + name?: string; + roles?: string[]; +} + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor(private readonly jwtService: JwtService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = this.extractToken(request); + + if (!token) { + throw new UnauthorizedException('Missing bearer token'); + } + + try { + const payload = await this.jwtService.verifyAsync(token, { + secret: process.env.JWT_SECRET, + }); + + const user: AuthenticatedUser = { + userId: payload.sub, + email: payload.email, + name: payload.name, + roles: (payload.roles ?? []) as AuthenticatedUser['roles'], + }; + + request.user = user; + return true; + } catch { + throw new UnauthorizedException('Invalid or expired token'); + } + } + + private extractToken(request: { + headers: Record; + }): string | undefined { + const header = request.headers['authorization']; + if (!header) return undefined; + const [type, token] = header.split(' '); + return type === 'Bearer' ? token : undefined; + } +} diff --git a/apps/backend/src/common/guards/roles.guard.ts b/apps/backend/src/common/guards/roles.guard.ts new file mode 100644 index 0000000..1a9fc38 --- /dev/null +++ b/apps/backend/src/common/guards/roles.guard.ts @@ -0,0 +1,26 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Role } from '../enums/role.enum'; +import { ROLES_KEY } from '../decorators/roles.decorator'; +import { AuthenticatedUser } from '../interfaces/authenticated-user.interface'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles || requiredRoles.length === 0) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const user: AuthenticatedUser | undefined = request.user; + + return !!user && requiredRoles.some(role => user.roles?.includes(role)); + } +} diff --git a/apps/backend/src/common/interfaces/authenticated-user.interface.ts b/apps/backend/src/common/interfaces/authenticated-user.interface.ts new file mode 100644 index 0000000..f61f6fe --- /dev/null +++ b/apps/backend/src/common/interfaces/authenticated-user.interface.ts @@ -0,0 +1,8 @@ +import { Role } from '../enums/role.enum'; + +export interface AuthenticatedUser { + userId: string; + email?: string; + name?: string; + roles: Role[]; +} diff --git a/apps/backend/src/database/prisma.module.ts b/apps/backend/src/database/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/apps/backend/src/database/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/apps/backend/src/database/prisma.service.ts b/apps/backend/src/database/prisma.service.ts new file mode 100644 index 0000000..829d52f --- /dev/null +++ b/apps/backend/src/database/prisma.service.ts @@ -0,0 +1,13 @@ +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + async onModuleInit(): Promise { + await this.$connect(); + } + + async onModuleDestroy(): Promise { + await this.$disconnect(); + } +} diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 43a726a..169a3bb 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,9 +1,28 @@ +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), + ); + + const swaggerConfig = new DocumentBuilder() + .setTitle('Sentinel API') + .setDescription('Sentinel security monitoring & incident response API') + .setVersion('1.0') + .addBearerAuth() + .build(); + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('api/docs', app, document); + const port = process.env.PORT ?? 3000; await app.listen(port); } diff --git a/apps/backend/src/modules/incidents/controllers/incidents.controller.ts b/apps/backend/src/modules/incidents/controllers/incidents.controller.ts new file mode 100644 index 0000000..f0cf8b2 --- /dev/null +++ b/apps/backend/src/modules/incidents/controllers/incidents.controller.ts @@ -0,0 +1,126 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../../common/decorators/current-user.decorator'; +import { Roles } from '../../../common/decorators/roles.decorator'; +import { Role } from '../../../common/enums/role.enum'; +import { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../../common/guards/roles.guard'; +import { AuthenticatedUser } from '../../../common/interfaces/authenticated-user.interface'; +import { AssignOwnerDto } from '../dto/assign-owner.dto'; +import { CreateIncidentDto } from '../dto/create-incident.dto'; +import { QueryIncidentsDto } from '../dto/query-incidents.dto'; +import { UpdateIncidentDto } from '../dto/update-incident.dto'; +import { UpdatePriorityDto } from '../dto/update-priority.dto'; +import { UpdateSeverityDto } from '../dto/update-severity.dto'; +import { UpdateStatusDto } from '../dto/update-status.dto'; +import { IncidentEntity } from '../entities/incident.entity'; +import { IncidentAssignmentService } from '../services/incident-assignment.service'; +import { IncidentStatusService } from '../services/incident-status.service'; +import { IncidentsService } from '../services/incidents.service'; + +@ApiTags('incidents') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RolesGuard) +@Controller('incidents') +export class IncidentsController { + constructor( + private readonly incidentsService: IncidentsService, + private readonly statusService: IncidentStatusService, + private readonly assignmentService: IncidentAssignmentService, + ) {} + + @Post() + @Roles(Role.ADMIN, Role.ANALYST) + @ApiOperation({ summary: 'Create a new incident' }) + @ApiResponse({ status: 201, type: IncidentEntity }) + create(@Body() dto: CreateIncidentDto, @CurrentUser() user: AuthenticatedUser) { + return this.incidentsService.create(dto, user); + } + + @Get() + @Roles(Role.ADMIN, Role.ANALYST, Role.VIEWER) + @ApiOperation({ summary: 'List incidents with pagination, filtering, sorting and search' }) + findAll(@Query() query: QueryIncidentsDto) { + return this.incidentsService.findAll(query); + } + + @Get(':id') + @Roles(Role.ADMIN, Role.ANALYST, Role.VIEWER) + @ApiOperation({ summary: 'Retrieve full incident details, including audit trail' }) + @ApiResponse({ status: 200, type: IncidentEntity }) + findOne(@Param('id') id: string) { + return this.incidentsService.findOne(id); + } + + @Patch(':id') + @Roles(Role.ADMIN, Role.ANALYST) + @ApiOperation({ summary: 'Update incident fields (title, description, category, tags, etc.)' }) + update( + @Param('id') id: string, + @Body() dto: UpdateIncidentDto, + @CurrentUser() user: AuthenticatedUser, + ) { + return this.incidentsService.update(id, dto, user); + } + + @Patch(':id/status') + @Roles(Role.ADMIN, Role.ANALYST) + @ApiOperation({ summary: 'Transition incident lifecycle status' }) + updateStatus( + @Param('id') id: string, + @Body() dto: UpdateStatusDto, + @CurrentUser() user: AuthenticatedUser, + ) { + return this.statusService.transition(id, dto.status, user, dto.reason); + } + + @Patch(':id/assign') + @Roles(Role.ADMIN, Role.ANALYST) + @ApiOperation({ summary: 'Assign, reassign, or remove the incident owner' }) + assign( + @Param('id') id: string, + @Body() dto: AssignOwnerDto, + @CurrentUser() user: AuthenticatedUser, + ) { + return this.assignmentService.assign(id, dto.assignedUserId, user); + } + + @Patch(':id/severity') + @Roles(Role.ADMIN, Role.ANALYST) + @ApiOperation({ summary: 'Update incident severity' }) + updateSeverity( + @Param('id') id: string, + @Body() dto: UpdateSeverityDto, + @CurrentUser() user: AuthenticatedUser, + ) { + return this.incidentsService.updateSeverity(id, dto.severity, user); + } + + @Patch(':id/priority') + @Roles(Role.ADMIN, Role.ANALYST) + @ApiOperation({ summary: 'Update incident priority' }) + updatePriority( + @Param('id') id: string, + @Body() dto: UpdatePriorityDto, + @CurrentUser() user: AuthenticatedUser, + ) { + return this.incidentsService.updatePriority(id, dto.priority, user); + } + + @Delete(':id') + @Roles(Role.ADMIN) + @ApiOperation({ summary: 'Archive (soft delete) an incident' }) + archive(@Param('id') id: string, @CurrentUser() user: AuthenticatedUser) { + return this.incidentsService.archive(id, user); + } +} diff --git a/apps/backend/src/modules/incidents/dto/assign-owner.dto.ts b/apps/backend/src/modules/incidents/dto/assign-owner.dto.ts new file mode 100644 index 0000000..75a6fa1 --- /dev/null +++ b/apps/backend/src/modules/incidents/dto/assign-owner.dto.ts @@ -0,0 +1,12 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class AssignOwnerDto { + @ApiPropertyOptional({ + description: 'User ID to assign as owner. Omit or pass null to remove the current assignee.', + nullable: true, + }) + @IsOptional() + @IsString() + assignedUserId?: string | null; +} diff --git a/apps/backend/src/modules/incidents/dto/create-incident.dto.ts b/apps/backend/src/modules/incidents/dto/create-incident.dto.ts new file mode 100644 index 0000000..41c53d6 --- /dev/null +++ b/apps/backend/src/modules/incidents/dto/create-incident.dto.ts @@ -0,0 +1,74 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + ArrayMaxSize, + IsArray, + IsEnum, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; +import { IncidentPriority } from '../enums/incident-priority.enum'; +import { IncidentSeverity } from '../enums/incident-severity.enum'; + +export class CreateIncidentDto { + @ApiProperty({ example: 'Suspicious wallet drain detected' }) + @IsString() + @MinLength(3) + @MaxLength(200) + title!: string; + + @ApiProperty({ + example: 'Multiple high-value transfers out of a monitored wallet within 5 minutes.', + }) + @IsString() + @MinLength(1) + @MaxLength(5000) + description!: string; + + @ApiPropertyOptional({ enum: IncidentSeverity, default: IncidentSeverity.MEDIUM }) + @IsOptional() + @IsEnum(IncidentSeverity) + severity?: IncidentSeverity; + + @ApiPropertyOptional({ enum: IncidentPriority, default: IncidentPriority.P3 }) + @IsOptional() + @IsEnum(IncidentPriority) + priority?: IncidentPriority; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + @MaxLength(100) + category?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + organizationId?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + assignedUserId?: string; + + @ApiPropertyOptional({ type: [String] }) + @IsOptional() + @IsArray() + @ArrayMaxSize(50) + @IsString({ each: true }) + sourceAlertIds?: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + @MaxLength(100) + detectionSource?: string; + + @ApiPropertyOptional({ type: [String] }) + @IsOptional() + @IsArray() + @ArrayMaxSize(20) + @IsString({ each: true }) + tags?: string[]; +} diff --git a/apps/backend/src/modules/incidents/dto/index.ts b/apps/backend/src/modules/incidents/dto/index.ts new file mode 100644 index 0000000..95568ea --- /dev/null +++ b/apps/backend/src/modules/incidents/dto/index.ts @@ -0,0 +1,7 @@ +export * from './create-incident.dto'; +export * from './update-incident.dto'; +export * from './update-status.dto'; +export * from './assign-owner.dto'; +export * from './update-severity.dto'; +export * from './update-priority.dto'; +export * from './query-incidents.dto'; diff --git a/apps/backend/src/modules/incidents/dto/query-incidents.dto.ts b/apps/backend/src/modules/incidents/dto/query-incidents.dto.ts new file mode 100644 index 0000000..4e13c5f --- /dev/null +++ b/apps/backend/src/modules/incidents/dto/query-incidents.dto.ts @@ -0,0 +1,89 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsDateString, IsEnum, IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; +import { IncidentPriority } from '../enums/incident-priority.enum'; +import { IncidentSeverity } from '../enums/incident-severity.enum'; +import { IncidentStatus } from '../enums/incident-status.enum'; + +export class QueryIncidentsDto { + @ApiPropertyOptional({ enum: IncidentStatus }) + @IsOptional() + @IsEnum(IncidentStatus) + status?: IncidentStatus; + + @ApiPropertyOptional({ enum: IncidentSeverity }) + @IsOptional() + @IsEnum(IncidentSeverity) + severity?: IncidentSeverity; + + @ApiPropertyOptional({ enum: IncidentPriority }) + @IsOptional() + @IsEnum(IncidentPriority) + priority?: IncidentPriority; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + assignedUserId?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + category?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + organizationId?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + detectionSource?: string; + + @ApiPropertyOptional({ description: 'ISO date string, inclusive lower bound on createdAt' }) + @IsOptional() + @IsDateString() + dateFrom?: string; + + @ApiPropertyOptional({ description: 'ISO date string, inclusive upper bound on createdAt' }) + @IsOptional() + @IsDateString() + dateTo?: string; + + @ApiPropertyOptional({ + description: + 'Full-text search across id, title, description, assigned user, alert IDs, wallet/tx/contract references', + }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ default: 1, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ default: 20, minimum: 1, maximum: 100 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 20; + + @ApiPropertyOptional({ + enum: ['createdAt', 'updatedAt', 'severity', 'priority', 'status'], + default: 'createdAt', + }) + @IsOptional() + @IsIn(['createdAt', 'updatedAt', 'severity', 'priority', 'status']) + sortBy?: 'createdAt' | 'updatedAt' | 'severity' | 'priority' | 'status' = 'createdAt'; + + @ApiPropertyOptional({ enum: ['asc', 'desc'], default: 'desc' }) + @IsOptional() + @IsIn(['asc', 'desc']) + sortOrder?: 'asc' | 'desc' = 'desc'; +} diff --git a/apps/backend/src/modules/incidents/dto/update-incident.dto.ts b/apps/backend/src/modules/incidents/dto/update-incident.dto.ts new file mode 100644 index 0000000..0ebae5e --- /dev/null +++ b/apps/backend/src/modules/incidents/dto/update-incident.dto.ts @@ -0,0 +1,10 @@ +import { PartialType, OmitType } from '@nestjs/swagger'; +import { CreateIncidentDto } from './create-incident.dto'; + +/** + * Update excludes severity/priority — those go through their own + * dedicated endpoints so each field's audit trail stays unambiguous. + */ +export class UpdateIncidentDto extends PartialType( + OmitType(CreateIncidentDto, ['severity', 'priority', 'assignedUserId'] as const), +) {} diff --git a/apps/backend/src/modules/incidents/dto/update-priority.dto.ts b/apps/backend/src/modules/incidents/dto/update-priority.dto.ts new file mode 100644 index 0000000..7df9d8c --- /dev/null +++ b/apps/backend/src/modules/incidents/dto/update-priority.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; +import { IncidentPriority } from '../enums/incident-priority.enum'; + +export class UpdatePriorityDto { + @ApiProperty({ enum: IncidentPriority }) + @IsEnum(IncidentPriority) + priority!: IncidentPriority; +} diff --git a/apps/backend/src/modules/incidents/dto/update-severity.dto.ts b/apps/backend/src/modules/incidents/dto/update-severity.dto.ts new file mode 100644 index 0000000..249ed9f --- /dev/null +++ b/apps/backend/src/modules/incidents/dto/update-severity.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; +import { IncidentSeverity } from '../enums/incident-severity.enum'; + +export class UpdateSeverityDto { + @ApiProperty({ enum: IncidentSeverity }) + @IsEnum(IncidentSeverity) + severity!: IncidentSeverity; +} diff --git a/apps/backend/src/modules/incidents/dto/update-status.dto.ts b/apps/backend/src/modules/incidents/dto/update-status.dto.ts new file mode 100644 index 0000000..a32d960 --- /dev/null +++ b/apps/backend/src/modules/incidents/dto/update-status.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator'; +import { IncidentStatus } from '../enums/incident-status.enum'; + +export class UpdateStatusDto { + @ApiProperty({ enum: IncidentStatus }) + @IsEnum(IncidentStatus) + status!: IncidentStatus; + + @ApiPropertyOptional({ description: 'Optional note explaining the transition' }) + @IsOptional() + @IsString() + @MaxLength(1000) + reason?: string; +} diff --git a/apps/backend/src/modules/incidents/entities/incident-audit-log.entity.ts b/apps/backend/src/modules/incidents/entities/incident-audit-log.entity.ts new file mode 100644 index 0000000..9dae0ea --- /dev/null +++ b/apps/backend/src/modules/incidents/entities/incident-audit-log.entity.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IncidentAuditAction } from '../enums/incident-audit-action.enum'; + +export class IncidentAuditLogEntity { + @ApiProperty() + id!: string; + + @ApiProperty() + incidentId!: string; + + @ApiProperty({ enum: IncidentAuditAction }) + action!: IncidentAuditAction; + + @ApiProperty() + actorId!: string; + + @ApiProperty({ required: false, nullable: true }) + actorName?: string | null; + + @ApiProperty({ required: false, nullable: true }) + previousValues?: Record | null; + + @ApiProperty({ required: false, nullable: true }) + newValues?: Record | null; + + @ApiProperty() + timestamp!: Date; +} diff --git a/apps/backend/src/modules/incidents/entities/incident.entity.ts b/apps/backend/src/modules/incidents/entities/incident.entity.ts new file mode 100644 index 0000000..eec7e92 --- /dev/null +++ b/apps/backend/src/modules/incidents/entities/incident.entity.ts @@ -0,0 +1,60 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IncidentPriority } from '../enums/incident-priority.enum'; +import { IncidentSeverity } from '../enums/incident-severity.enum'; +import { IncidentStatus } from '../enums/incident-status.enum'; + +export class IncidentEntity { + @ApiProperty() + id!: string; + + @ApiProperty() + title!: string; + + @ApiProperty() + description!: string; + + @ApiProperty({ enum: IncidentStatus }) + status!: IncidentStatus; + + @ApiProperty({ enum: IncidentSeverity }) + severity!: IncidentSeverity; + + @ApiProperty({ enum: IncidentPriority }) + priority!: IncidentPriority; + + @ApiProperty({ required: false, nullable: true }) + category?: string | null; + + @ApiProperty({ required: false, nullable: true }) + organizationId?: string | null; + + @ApiProperty({ required: false, nullable: true }) + assignedUserId?: string | null; + + @ApiProperty({ type: [String] }) + sourceAlertIds!: string[]; + + @ApiProperty({ required: false, nullable: true }) + detectionSource?: string | null; + + @ApiProperty({ type: [String] }) + tags!: string[]; + + @ApiProperty() + deleted!: boolean; + + @ApiProperty({ required: false, nullable: true }) + deletedAt?: Date | null; + + @ApiProperty() + createdAt!: Date; + + @ApiProperty() + updatedAt!: Date; + + @ApiProperty({ required: false, nullable: true }) + resolvedAt?: Date | null; + + @ApiProperty({ required: false, nullable: true }) + closedAt?: Date | null; +} diff --git a/apps/backend/src/modules/incidents/enums/incident-audit-action.enum.ts b/apps/backend/src/modules/incidents/enums/incident-audit-action.enum.ts new file mode 100644 index 0000000..2844565 --- /dev/null +++ b/apps/backend/src/modules/incidents/enums/incident-audit-action.enum.ts @@ -0,0 +1,14 @@ +export enum IncidentAuditAction { + CREATED = 'incident_created', + UPDATED = 'incident_updated', + STATUS_CHANGED = 'status_changed', + OWNER_ASSIGNED = 'owner_assigned', + OWNER_REASSIGNED = 'owner_reassigned', + OWNER_REMOVED = 'owner_removed', + SEVERITY_UPDATED = 'severity_updated', + PRIORITY_UPDATED = 'priority_updated', + RESOLVED = 'resolved', + CLOSED = 'closed', + REOPENED = 'reopened', + ARCHIVED = 'archived', +} diff --git a/apps/backend/src/modules/incidents/enums/incident-priority.enum.ts b/apps/backend/src/modules/incidents/enums/incident-priority.enum.ts new file mode 100644 index 0000000..c95f381 --- /dev/null +++ b/apps/backend/src/modules/incidents/enums/incident-priority.enum.ts @@ -0,0 +1,6 @@ +export enum IncidentPriority { + P1 = 'p1', + P2 = 'p2', + P3 = 'p3', + P4 = 'p4', +} diff --git a/apps/backend/src/modules/incidents/enums/incident-severity.enum.ts b/apps/backend/src/modules/incidents/enums/incident-severity.enum.ts new file mode 100644 index 0000000..fc399f2 --- /dev/null +++ b/apps/backend/src/modules/incidents/enums/incident-severity.enum.ts @@ -0,0 +1,6 @@ +export enum IncidentSeverity { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + CRITICAL = 'critical', +} diff --git a/apps/backend/src/modules/incidents/enums/incident-status.enum.ts b/apps/backend/src/modules/incidents/enums/incident-status.enum.ts new file mode 100644 index 0000000..08d7b90 --- /dev/null +++ b/apps/backend/src/modules/incidents/enums/incident-status.enum.ts @@ -0,0 +1,37 @@ +export enum IncidentStatus { + NEW = 'new', + OPEN = 'open', + ACKNOWLEDGED = 'acknowledged', + INVESTIGATING = 'investigating', + CONTAINED = 'contained', + RESOLVED = 'resolved', + CLOSED = 'closed', + REOPENED = 'reopened', +} + +/** + * Valid forward transitions per status. Any transition not listed here + * (including self-transitions) is rejected by IncidentStatusService. + */ +export const INCIDENT_STATUS_TRANSITIONS: Record = { + [IncidentStatus.NEW]: [IncidentStatus.OPEN, IncidentStatus.CLOSED], + [IncidentStatus.OPEN]: [ + IncidentStatus.ACKNOWLEDGED, + IncidentStatus.INVESTIGATING, + IncidentStatus.CLOSED, + ], + [IncidentStatus.ACKNOWLEDGED]: [IncidentStatus.INVESTIGATING, IncidentStatus.CLOSED], + [IncidentStatus.INVESTIGATING]: [ + IncidentStatus.CONTAINED, + IncidentStatus.RESOLVED, + IncidentStatus.CLOSED, + ], + [IncidentStatus.CONTAINED]: [IncidentStatus.RESOLVED, IncidentStatus.CLOSED], + [IncidentStatus.RESOLVED]: [IncidentStatus.CLOSED, IncidentStatus.REOPENED], + [IncidentStatus.CLOSED]: [IncidentStatus.REOPENED], + [IncidentStatus.REOPENED]: [ + IncidentStatus.OPEN, + IncidentStatus.INVESTIGATING, + IncidentStatus.CLOSED, + ], +}; diff --git a/apps/backend/src/modules/incidents/enums/index.ts b/apps/backend/src/modules/incidents/enums/index.ts new file mode 100644 index 0000000..6190a51 --- /dev/null +++ b/apps/backend/src/modules/incidents/enums/index.ts @@ -0,0 +1,4 @@ +export * from './incident-status.enum'; +export * from './incident-severity.enum'; +export * from './incident-priority.enum'; +export * from './incident-audit-action.enum'; diff --git a/apps/backend/src/modules/incidents/incidents.module.ts b/apps/backend/src/modules/incidents/incidents.module.ts new file mode 100644 index 0000000..fb099f8 --- /dev/null +++ b/apps/backend/src/modules/incidents/incidents.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { IncidentsController } from './controllers/incidents.controller'; +import { IncidentsRepository } from './repositories/incidents.repository'; +import { IncidentAssignmentService } from './services/incident-assignment.service'; +import { IncidentStatusService } from './services/incident-status.service'; +import { IncidentsService } from './services/incidents.service'; + +@Module({ + imports: [ + NotificationsModule, + JwtModule.register({ + secret: process.env.JWT_SECRET, + }), + ], + controllers: [IncidentsController], + providers: [ + IncidentsRepository, + IncidentsService, + IncidentStatusService, + IncidentAssignmentService, + ], + exports: [IncidentsService], +}) +export class IncidentsModule {} diff --git a/apps/backend/src/modules/incidents/interfaces/incident-filter.interface.ts b/apps/backend/src/modules/incidents/interfaces/incident-filter.interface.ts new file mode 100644 index 0000000..4264527 --- /dev/null +++ b/apps/backend/src/modules/incidents/interfaces/incident-filter.interface.ts @@ -0,0 +1,26 @@ +import { IncidentPriority } from '../enums/incident-priority.enum'; +import { IncidentSeverity } from '../enums/incident-severity.enum'; +import { IncidentStatus } from '../enums/incident-status.enum'; + +export interface IncidentFilter { + status?: IncidentStatus; + severity?: IncidentSeverity; + priority?: IncidentPriority; + assignedUserId?: string; + category?: string; + organizationId?: string; + detectionSource?: string; + dateFrom?: Date; + dateTo?: Date; + search?: string; +} + +export interface IncidentSort { + sortBy: 'createdAt' | 'updatedAt' | 'severity' | 'priority' | 'status'; + sortOrder: 'asc' | 'desc'; +} + +export interface IncidentPagination { + page: number; + limit: number; +} diff --git a/apps/backend/src/modules/incidents/interfaces/index.ts b/apps/backend/src/modules/incidents/interfaces/index.ts new file mode 100644 index 0000000..0c0d499 --- /dev/null +++ b/apps/backend/src/modules/incidents/interfaces/index.ts @@ -0,0 +1,2 @@ +export * from './paginated-result.interface'; +export * from './incident-filter.interface'; diff --git a/apps/backend/src/modules/incidents/interfaces/paginated-result.interface.ts b/apps/backend/src/modules/incidents/interfaces/paginated-result.interface.ts new file mode 100644 index 0000000..c80bfb3 --- /dev/null +++ b/apps/backend/src/modules/incidents/interfaces/paginated-result.interface.ts @@ -0,0 +1,9 @@ +export interface PaginatedResult { + items: T[]; + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + }; +} diff --git a/apps/backend/src/modules/incidents/repositories/incidents.repository.ts b/apps/backend/src/modules/incidents/repositories/incidents.repository.ts new file mode 100644 index 0000000..be67d9b --- /dev/null +++ b/apps/backend/src/modules/incidents/repositories/incidents.repository.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../../../database/prisma.service'; +import { IncidentAuditAction } from '../enums/incident-audit-action.enum'; +import { IncidentFilter, IncidentPagination, IncidentSort } from '../interfaces'; +import { PaginatedResult } from '../interfaces/paginated-result.interface'; + +@Injectable() +export class IncidentsRepository { + constructor(private readonly prisma: PrismaService) {} + + create(data: Prisma.IncidentCreateInput) { + return this.prisma.incident.create({ data }); + } + + findById(id: string, includeDeleted = false) { + return this.prisma.incident.findFirst({ + where: { id, ...(includeDeleted ? {} : { deleted: false }) }, + }); + } + + async findMany( + filter: IncidentFilter, + pagination: IncidentPagination, + sort: IncidentSort, + ): Promise>>> { + const where = this.buildWhere(filter); + const { page, limit } = pagination; + + const [items, total] = await Promise.all([ + this.prisma.incident.findMany({ + where, + orderBy: { [sort.sortBy]: sort.sortOrder }, + skip: (page - 1) * limit, + take: limit, + }), + this.prisma.incident.count({ where }), + ]); + + return { + items, + meta: { + total, + page, + limit, + totalPages: Math.max(1, Math.ceil(total / limit)), + }, + }; + } + + update(id: string, data: Prisma.IncidentUpdateInput) { + return this.prisma.incident.update({ where: { id }, data }); + } + + softDelete(id: string) { + return this.prisma.incident.update({ + where: { id }, + data: { deleted: true, deletedAt: new Date() }, + }); + } + + addAuditEntry(entry: { + incidentId: string; + action: IncidentAuditAction; + actorId: string; + actorName?: string; + previousValues?: Record | null; + newValues?: Record | null; + }) { + return this.prisma.incidentAuditLog.create({ + data: { + incidentId: entry.incidentId, + action: entry.action, + actorId: entry.actorId, + actorName: entry.actorName, + previousValues: (entry.previousValues ?? undefined) as Prisma.InputJsonValue | undefined, + newValues: (entry.newValues ?? undefined) as Prisma.InputJsonValue | undefined, + }, + }); + } + + findAuditTrail(incidentId: string) { + return this.prisma.incidentAuditLog.findMany({ + where: { incidentId }, + orderBy: { timestamp: 'asc' }, + }); + } + + private buildWhere(filter: IncidentFilter): Prisma.IncidentWhereInput { + const where: Prisma.IncidentWhereInput = { deleted: false }; + + if (filter.status) where.status = filter.status; + if (filter.severity) where.severity = filter.severity; + if (filter.priority) where.priority = filter.priority; + if (filter.assignedUserId) where.assignedUserId = filter.assignedUserId; + if (filter.category) where.category = filter.category; + if (filter.organizationId) where.organizationId = filter.organizationId; + if (filter.detectionSource) where.detectionSource = filter.detectionSource; + + if (filter.dateFrom || filter.dateTo) { + where.createdAt = { + ...(filter.dateFrom ? { gte: filter.dateFrom } : {}), + ...(filter.dateTo ? { lte: filter.dateTo } : {}), + }; + } + + if (filter.search) { + const term = filter.search; + where.OR = [ + { id: { equals: term } }, + { title: { contains: term, mode: 'insensitive' } }, + { description: { contains: term, mode: 'insensitive' } }, + { assignedUserId: { equals: term } }, + { sourceAlertIds: { has: term } }, + { detectionSource: { contains: term, mode: 'insensitive' } }, + { tags: { has: term } }, + ]; + } + + return where; + } +} diff --git a/apps/backend/src/modules/incidents/services/incident-assignment.service.ts b/apps/backend/src/modules/incidents/services/incident-assignment.service.ts new file mode 100644 index 0000000..dfbe341 --- /dev/null +++ b/apps/backend/src/modules/incidents/services/incident-assignment.service.ts @@ -0,0 +1,65 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { AuthenticatedUser } from '../../../common/interfaces/authenticated-user.interface'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { IncidentAuditAction } from '../enums/incident-audit-action.enum'; +import { IncidentsRepository } from '../repositories/incidents.repository'; + +@Injectable() +export class IncidentAssignmentService { + constructor( + private readonly repository: IncidentsRepository, + private readonly notifications: NotificationsService, + ) {} + + async getAssignee(incidentId: string) { + const incident = await this.repository.findById(incidentId); + if (!incident) { + throw new NotFoundException(`Incident ${incidentId} not found`); + } + return { assignedUserId: incident.assignedUserId }; + } + + async assign( + incidentId: string, + assignedUserId: string | null | undefined, + actor: AuthenticatedUser, + ) { + const incident = await this.repository.findById(incidentId); + if (!incident) { + throw new NotFoundException(`Incident ${incidentId} not found`); + } + + const previousAssignee = incident.assignedUserId; + const nextAssignee = assignedUserId ?? null; + + const action = !previousAssignee + ? IncidentAuditAction.OWNER_ASSIGNED + : !nextAssignee + ? IncidentAuditAction.OWNER_REMOVED + : IncidentAuditAction.OWNER_REASSIGNED; + + const updated = await this.repository.update(incidentId, { + assignedUser: nextAssignee ? { connect: { id: nextAssignee } } : { disconnect: true }, + }); + + await this.repository.addAuditEntry({ + incidentId, + action, + actorId: actor.userId, + actorName: actor.name ?? actor.email, + previousValues: { assignedUserId: previousAssignee }, + newValues: { assignedUserId: nextAssignee }, + }); + + if (nextAssignee) { + await this.notifications.sendAlert({ + title: `Incident ${incidentId} assigned`, + message: `Incident "${incident.title}" was assigned to user ${nextAssignee}`, + severity: incident.severity as 'low' | 'medium' | 'high' | 'critical', + metadata: { incidentId, assignedUserId: nextAssignee }, + }); + } + + return updated; + } +} diff --git a/apps/backend/src/modules/incidents/services/incident-status.service.ts b/apps/backend/src/modules/incidents/services/incident-status.service.ts new file mode 100644 index 0000000..1c51ff8 --- /dev/null +++ b/apps/backend/src/modules/incidents/services/incident-status.service.ts @@ -0,0 +1,80 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { AuthenticatedUser } from '../../../common/interfaces/authenticated-user.interface'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { IncidentAuditAction } from '../enums/incident-audit-action.enum'; +import { INCIDENT_STATUS_TRANSITIONS, IncidentStatus } from '../enums/incident-status.enum'; +import { IncidentsRepository } from '../repositories/incidents.repository'; + +@Injectable() +export class IncidentStatusService { + constructor( + private readonly repository: IncidentsRepository, + private readonly notifications: NotificationsService, + ) {} + + async transition( + incidentId: string, + nextStatus: IncidentStatus, + actor: AuthenticatedUser, + reason?: string, + ) { + const incident = await this.repository.findById(incidentId); + if (!incident) { + throw new NotFoundException(`Incident ${incidentId} not found`); + } + + const currentStatus = incident.status as IncidentStatus; + const allowedNext = INCIDENT_STATUS_TRANSITIONS[currentStatus] ?? []; + + if (currentStatus === nextStatus) { + throw new BadRequestException(`Incident is already in status "${currentStatus}"`); + } + + if (!allowedNext.includes(nextStatus)) { + throw new BadRequestException( + `Cannot transition incident from "${currentStatus}" to "${nextStatus}". Allowed: ${ + allowedNext.length ? allowedNext.join(', ') : 'none (terminal)' + }`, + ); + } + + const now = new Date(); + const updated = await this.repository.update(incidentId, { + status: nextStatus, + ...(nextStatus === IncidentStatus.RESOLVED ? { resolvedAt: now } : {}), + ...(nextStatus === IncidentStatus.CLOSED ? { closedAt: now } : {}), + ...(nextStatus === IncidentStatus.REOPENED ? { resolvedAt: null, closedAt: null } : {}), + }); + + await this.repository.addAuditEntry({ + incidentId, + action: this.actionForTransition(nextStatus), + actorId: actor.userId, + actorName: actor.name ?? actor.email, + previousValues: { status: currentStatus }, + newValues: { status: nextStatus, reason }, + }); + + await this.notifications.sendAlert({ + title: `Incident ${incidentId} status changed`, + message: `Status moved from "${currentStatus}" to "${nextStatus}"${reason ? `: ${reason}` : ''}`, + severity: incident.severity as 'low' | 'medium' | 'high' | 'critical', + metadata: { incidentId, currentStatus, nextStatus }, + }); + + return updated; + } + + private actionForTransition(nextStatus: IncidentStatus): IncidentAuditAction { + switch (nextStatus) { + case IncidentStatus.RESOLVED: + return IncidentAuditAction.RESOLVED; + case IncidentStatus.CLOSED: + return IncidentAuditAction.CLOSED; + case IncidentStatus.REOPENED: + return IncidentAuditAction.REOPENED; + default: + return IncidentAuditAction.STATUS_CHANGED; + } + } +} diff --git a/apps/backend/src/modules/incidents/services/incidents.service.ts b/apps/backend/src/modules/incidents/services/incidents.service.ts new file mode 100644 index 0000000..89a10b1 --- /dev/null +++ b/apps/backend/src/modules/incidents/services/incidents.service.ts @@ -0,0 +1,176 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { AuthenticatedUser } from '../../../common/interfaces/authenticated-user.interface'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { CreateIncidentDto } from '../dto/create-incident.dto'; +import { QueryIncidentsDto } from '../dto/query-incidents.dto'; +import { UpdateIncidentDto } from '../dto/update-incident.dto'; +import { IncidentAuditAction } from '../enums/incident-audit-action.enum'; +import { IncidentPriority } from '../enums/incident-priority.enum'; +import { IncidentSeverity } from '../enums/incident-severity.enum'; +import { IncidentStatus } from '../enums/incident-status.enum'; +import { IncidentsRepository } from '../repositories/incidents.repository'; + +@Injectable() +export class IncidentsService { + constructor( + private readonly repository: IncidentsRepository, + private readonly notifications: NotificationsService, + ) {} + + async create(dto: CreateIncidentDto, actor: AuthenticatedUser) { + const incident = await this.repository.create({ + title: dto.title, + description: dto.description, + severity: dto.severity ?? IncidentSeverity.MEDIUM, + priority: dto.priority ?? IncidentPriority.P3, + category: dto.category, + organizationId: dto.organizationId, + detectionSource: dto.detectionSource, + sourceAlertIds: dto.sourceAlertIds ?? [], + tags: dto.tags ?? [], + status: IncidentStatus.NEW, + ...(dto.assignedUserId ? { assignedUser: { connect: { id: dto.assignedUserId } } } : {}), + }); + + await this.repository.addAuditEntry({ + incidentId: incident.id, + action: IncidentAuditAction.CREATED, + actorId: actor.userId, + actorName: actor.name ?? actor.email, + newValues: { ...dto }, + }); + + await this.notifications.sendAlert({ + title: `New incident created: ${incident.title}`, + message: incident.description, + severity: incident.severity as 'low' | 'medium' | 'high' | 'critical', + metadata: { incidentId: incident.id }, + }); + + return incident; + } + + async findAll(query: QueryIncidentsDto) { + return this.repository.findMany( + { + status: query.status, + severity: query.severity, + priority: query.priority, + assignedUserId: query.assignedUserId, + category: query.category, + organizationId: query.organizationId, + detectionSource: query.detectionSource, + dateFrom: query.dateFrom ? new Date(query.dateFrom) : undefined, + dateTo: query.dateTo ? new Date(query.dateTo) : undefined, + search: query.search, + }, + { page: query.page ?? 1, limit: query.limit ?? 20 }, + { sortBy: query.sortBy ?? 'createdAt', sortOrder: query.sortOrder ?? 'desc' }, + ); + } + + async findOne(id: string) { + const incident = await this.repository.findById(id); + if (!incident) { + throw new NotFoundException(`Incident ${id} not found`); + } + + const auditTrail = await this.repository.findAuditTrail(id); + return { ...incident, auditTrail }; + } + + async update(id: string, dto: UpdateIncidentDto, actor: AuthenticatedUser) { + const incident = await this.repository.findById(id); + if (!incident) { + throw new NotFoundException(`Incident ${id} not found`); + } + + const updated = await this.repository.update(id, { + ...(dto.title !== undefined ? { title: dto.title } : {}), + ...(dto.description !== undefined ? { description: dto.description } : {}), + ...(dto.category !== undefined ? { category: dto.category } : {}), + ...(dto.organizationId !== undefined ? { organizationId: dto.organizationId } : {}), + ...(dto.detectionSource !== undefined ? { detectionSource: dto.detectionSource } : {}), + ...(dto.sourceAlertIds !== undefined ? { sourceAlertIds: dto.sourceAlertIds } : {}), + ...(dto.tags !== undefined ? { tags: dto.tags } : {}), + }); + + await this.repository.addAuditEntry({ + incidentId: id, + action: IncidentAuditAction.UPDATED, + actorId: actor.userId, + actorName: actor.name ?? actor.email, + previousValues: this.pick(incident, Object.keys(dto)), + newValues: { ...dto }, + }); + + return updated; + } + + async updateSeverity(id: string, severity: IncidentSeverity, actor: AuthenticatedUser) { + const incident = await this.repository.findById(id); + if (!incident) { + throw new NotFoundException(`Incident ${id} not found`); + } + + const updated = await this.repository.update(id, { severity }); + + await this.repository.addAuditEntry({ + incidentId: id, + action: IncidentAuditAction.SEVERITY_UPDATED, + actorId: actor.userId, + actorName: actor.name ?? actor.email, + previousValues: { severity: incident.severity }, + newValues: { severity }, + }); + + return updated; + } + + async updatePriority(id: string, priority: IncidentPriority, actor: AuthenticatedUser) { + const incident = await this.repository.findById(id); + if (!incident) { + throw new NotFoundException(`Incident ${id} not found`); + } + + const updated = await this.repository.update(id, { priority }); + + await this.repository.addAuditEntry({ + incidentId: id, + action: IncidentAuditAction.PRIORITY_UPDATED, + actorId: actor.userId, + actorName: actor.name ?? actor.email, + previousValues: { priority: incident.priority }, + newValues: { priority }, + }); + + return updated; + } + + async archive(id: string, actor: AuthenticatedUser) { + const incident = await this.repository.findById(id); + if (!incident) { + throw new NotFoundException(`Incident ${id} not found`); + } + + const updated = await this.repository.softDelete(id); + + await this.repository.addAuditEntry({ + incidentId: id, + action: IncidentAuditAction.ARCHIVED, + actorId: actor.userId, + actorName: actor.name ?? actor.email, + previousValues: { deleted: false }, + newValues: { deleted: true }, + }); + + return updated; + } + + private pick(source: Record, keys: string[]) { + return keys.reduce>((acc, key) => { + acc[key] = source[key]; + return acc; + }, {}); + } +} diff --git a/apps/backend/src/modules/incidents/tests/dto-validation.spec.ts b/apps/backend/src/modules/incidents/tests/dto-validation.spec.ts new file mode 100644 index 0000000..d52ce1e --- /dev/null +++ b/apps/backend/src/modules/incidents/tests/dto-validation.spec.ts @@ -0,0 +1,92 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { CreateIncidentDto } from '../dto/create-incident.dto'; +import { QueryIncidentsDto } from '../dto/query-incidents.dto'; +import { UpdatePriorityDto } from '../dto/update-priority.dto'; +import { UpdateSeverityDto } from '../dto/update-severity.dto'; +import { UpdateStatusDto } from '../dto/update-status.dto'; + +describe('Incidents DTO validation', () => { + describe('CreateIncidentDto', () => { + it('passes with only the required fields', async () => { + const dto = plainToInstance(CreateIncidentDto, { + title: 'Suspicious activity', + description: 'Details of the incident', + }); + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it('fails when title is missing', async () => { + const dto = plainToInstance(CreateIncidentDto, { description: 'desc' }); + const errors = await validate(dto); + expect(errors.some(e => e.property === 'title')).toBe(true); + }); + + it('fails on an invalid severity value', async () => { + const dto = plainToInstance(CreateIncidentDto, { + title: 'T', + description: 'd', + severity: 'catastrophic', + }); + const errors = await validate(dto); + expect(errors.some(e => e.property === 'severity')).toBe(true); + }); + + it('fails when sourceAlertIds exceeds the max array size', async () => { + const dto = plainToInstance(CreateIncidentDto, { + title: 'T', + description: 'd', + sourceAlertIds: Array.from({ length: 51 }, (_, i) => `alert-${i}`), + }); + const errors = await validate(dto); + expect(errors.some(e => e.property === 'sourceAlertIds')).toBe(true); + }); + }); + + describe('UpdateStatusDto', () => { + it('fails on an invalid status enum value', async () => { + const dto = plainToInstance(UpdateStatusDto, { status: 'archived' }); + const errors = await validate(dto); + expect(errors.some(e => e.property === 'status')).toBe(true); + }); + + it('passes with a valid status', async () => { + const dto = plainToInstance(UpdateStatusDto, { status: 'open' }); + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + }); + + describe('UpdateSeverityDto / UpdatePriorityDto', () => { + it('rejects an invalid severity', async () => { + const dto = plainToInstance(UpdateSeverityDto, { severity: 'extreme' }); + expect((await validate(dto)).length).toBeGreaterThan(0); + }); + + it('rejects an invalid priority', async () => { + const dto = plainToInstance(UpdatePriorityDto, { priority: 'p5' }); + expect((await validate(dto)).length).toBeGreaterThan(0); + }); + }); + + describe('QueryIncidentsDto', () => { + it('rejects a limit above the maximum', async () => { + const dto = plainToInstance(QueryIncidentsDto, { limit: 500 }); + const errors = await validate(dto); + expect(errors.some(e => e.property === 'limit')).toBe(true); + }); + + it('rejects an invalid sortBy value', async () => { + const dto = plainToInstance(QueryIncidentsDto, { sortBy: 'title' }); + const errors = await validate(dto); + expect(errors.some(e => e.property === 'sortBy')).toBe(true); + }); + + it('passes with no query params (defaults apply)', async () => { + const dto = plainToInstance(QueryIncidentsDto, {}); + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + }); +}); diff --git a/apps/backend/src/modules/incidents/tests/incident-assignment.service.spec.ts b/apps/backend/src/modules/incidents/tests/incident-assignment.service.spec.ts new file mode 100644 index 0000000..ff88188 --- /dev/null +++ b/apps/backend/src/modules/incidents/tests/incident-assignment.service.spec.ts @@ -0,0 +1,94 @@ +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticatedUser } from '../../../common/interfaces/authenticated-user.interface'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { IncidentAuditAction } from '../enums/incident-audit-action.enum'; +import { IncidentsRepository } from '../repositories/incidents.repository'; +import { IncidentAssignmentService } from '../services/incident-assignment.service'; + +describe('IncidentAssignmentService', () => { + let service: IncidentAssignmentService; + const repository = { + findById: jest.fn(), + update: jest.fn(), + addAuditEntry: jest.fn(), + }; + const notifications = { sendAlert: jest.fn() }; + const actor: AuthenticatedUser = { userId: 'user-1', name: 'Alice', roles: [] as any }; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + IncidentAssignmentService, + { provide: IncidentsRepository, useValue: repository }, + { provide: NotificationsService, useValue: notifications }, + ], + }).compile(); + + service = module.get(IncidentAssignmentService); + }); + + it('throws NotFoundException for a missing incident', async () => { + repository.findById.mockResolvedValue(null); + await expect(service.assign('missing', 'user-2', actor)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('assigns an unassigned incident and notifies', async () => { + repository.findById.mockResolvedValue({ + id: 'inc-1', + title: 'Wallet drain', + assignedUserId: null, + severity: 'high', + }); + repository.update.mockResolvedValue({ id: 'inc-1', assignedUserId: 'user-2' }); + + await service.assign('inc-1', 'user-2', actor); + + expect(repository.update).toHaveBeenCalledWith('inc-1', { + assignedUser: { connect: { id: 'user-2' } }, + }); + expect(repository.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ action: IncidentAuditAction.OWNER_ASSIGNED }), + ); + expect(notifications.sendAlert).toHaveBeenCalled(); + }); + + it('reassigns an already-assigned incident', async () => { + repository.findById.mockResolvedValue({ + id: 'inc-1', + title: 'Wallet drain', + assignedUserId: 'user-2', + severity: 'high', + }); + repository.update.mockResolvedValue({}); + + await service.assign('inc-1', 'user-3', actor); + + expect(repository.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ action: IncidentAuditAction.OWNER_REASSIGNED }), + ); + }); + + it('removes the assignee when assignedUserId is null', async () => { + repository.findById.mockResolvedValue({ + id: 'inc-1', + title: 'Wallet drain', + assignedUserId: 'user-2', + severity: 'high', + }); + repository.update.mockResolvedValue({}); + + await service.assign('inc-1', null, actor); + + expect(repository.update).toHaveBeenCalledWith('inc-1', { + assignedUser: { disconnect: true }, + }); + expect(repository.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ action: IncidentAuditAction.OWNER_REMOVED }), + ); + expect(notifications.sendAlert).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/backend/src/modules/incidents/tests/incident-status.service.spec.ts b/apps/backend/src/modules/incidents/tests/incident-status.service.spec.ts new file mode 100644 index 0000000..d6bac00 --- /dev/null +++ b/apps/backend/src/modules/incidents/tests/incident-status.service.spec.ts @@ -0,0 +1,117 @@ +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticatedUser } from '../../../common/interfaces/authenticated-user.interface'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { IncidentStatus } from '../enums/incident-status.enum'; +import { IncidentsRepository } from '../repositories/incidents.repository'; +import { IncidentStatusService } from '../services/incident-status.service'; + +describe('IncidentStatusService', () => { + let service: IncidentStatusService; + const repository = { + findById: jest.fn(), + update: jest.fn(), + addAuditEntry: jest.fn(), + }; + const notifications = { sendAlert: jest.fn() }; + const actor: AuthenticatedUser = { userId: 'user-1', name: 'Alice', roles: [] as any }; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + IncidentStatusService, + { provide: IncidentsRepository, useValue: repository }, + { provide: NotificationsService, useValue: notifications }, + ], + }).compile(); + + service = module.get(IncidentStatusService); + }); + + it('throws NotFoundException for a missing incident', async () => { + repository.findById.mockResolvedValue(null); + await expect(service.transition('missing', IncidentStatus.OPEN, actor)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('rejects an invalid transition', async () => { + repository.findById.mockResolvedValue({ + id: 'inc-1', + status: IncidentStatus.NEW, + severity: 'medium', + }); + await expect( + service.transition('inc-1', IncidentStatus.RESOLVED, actor), + ).rejects.toBeInstanceOf(BadRequestException); + expect(repository.update).not.toHaveBeenCalled(); + }); + + it('rejects a self-transition', async () => { + repository.findById.mockResolvedValue({ + id: 'inc-1', + status: IncidentStatus.OPEN, + severity: 'medium', + }); + await expect(service.transition('inc-1', IncidentStatus.OPEN, actor)).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it('allows a valid transition, records audit entry and notifies', async () => { + repository.findById.mockResolvedValue({ + id: 'inc-1', + status: IncidentStatus.NEW, + severity: 'high', + }); + repository.update.mockResolvedValue({ id: 'inc-1', status: IncidentStatus.OPEN }); + + const result = await service.transition('inc-1', IncidentStatus.OPEN, actor, 'triage started'); + + expect(repository.update).toHaveBeenCalledWith('inc-1', { status: IncidentStatus.OPEN }); + expect(repository.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ + incidentId: 'inc-1', + actorId: 'user-1', + previousValues: { status: IncidentStatus.NEW }, + newValues: { status: IncidentStatus.OPEN, reason: 'triage started' }, + }), + ); + expect(notifications.sendAlert).toHaveBeenCalled(); + expect(result).toEqual({ id: 'inc-1', status: IncidentStatus.OPEN }); + }); + + it('stamps resolvedAt when transitioning to resolved', async () => { + repository.findById.mockResolvedValue({ + id: 'inc-1', + status: IncidentStatus.INVESTIGATING, + severity: 'medium', + }); + repository.update.mockResolvedValue({}); + + await service.transition('inc-1', IncidentStatus.RESOLVED, actor); + + expect(repository.update).toHaveBeenCalledWith('inc-1', { + status: IncidentStatus.RESOLVED, + resolvedAt: expect.any(Date), + }); + }); + + it('clears resolvedAt/closedAt when reopened', async () => { + repository.findById.mockResolvedValue({ + id: 'inc-1', + status: IncidentStatus.CLOSED, + severity: 'medium', + }); + repository.update.mockResolvedValue({}); + + await service.transition('inc-1', IncidentStatus.REOPENED, actor); + + expect(repository.update).toHaveBeenCalledWith('inc-1', { + status: IncidentStatus.REOPENED, + resolvedAt: null, + closedAt: null, + }); + }); +}); diff --git a/apps/backend/src/modules/incidents/tests/incidents.controller.spec.ts b/apps/backend/src/modules/incidents/tests/incidents.controller.spec.ts new file mode 100644 index 0000000..5102aa1 --- /dev/null +++ b/apps/backend/src/modules/incidents/tests/incidents.controller.spec.ts @@ -0,0 +1,98 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticatedUser } from '../../../common/interfaces/authenticated-user.interface'; +import { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../../common/guards/roles.guard'; +import { IncidentsController } from '../controllers/incidents.controller'; +import { IncidentAssignmentService } from '../services/incident-assignment.service'; +import { IncidentStatusService } from '../services/incident-status.service'; +import { IncidentsService } from '../services/incidents.service'; + +describe('IncidentsController', () => { + let controller: IncidentsController; + + const incidentsService = { + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + update: jest.fn(), + updateSeverity: jest.fn(), + updatePriority: jest.fn(), + archive: jest.fn(), + }; + const statusService = { transition: jest.fn() }; + const assignmentService = { assign: jest.fn() }; + const actor: AuthenticatedUser = { userId: 'user-1', roles: [] as any }; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + controllers: [IncidentsController], + providers: [ + { provide: IncidentsService, useValue: incidentsService }, + { provide: IncidentStatusService, useValue: statusService }, + { provide: IncidentAssignmentService, useValue: assignmentService }, + ], + }) + .overrideGuard(JwtAuthGuard) + .useValue({ canActivate: () => true }) + .overrideGuard(RolesGuard) + .useValue({ canActivate: () => true }) + .compile(); + + controller = module.get(IncidentsController); + }); + + it('delegates create to IncidentsService', async () => { + incidentsService.create.mockResolvedValue({ id: 'inc-1' }); + const dto = { title: 'T', description: 'd' } as any; + const result = await controller.create(dto, actor); + expect(incidentsService.create).toHaveBeenCalledWith(dto, actor); + expect(result).toEqual({ id: 'inc-1' }); + }); + + it('delegates listing to IncidentsService', async () => { + const query = { page: 1, limit: 20 } as any; + await controller.findAll(query); + expect(incidentsService.findAll).toHaveBeenCalledWith(query); + }); + + it('delegates retrieval to IncidentsService', async () => { + await controller.findOne('inc-1'); + expect(incidentsService.findOne).toHaveBeenCalledWith('inc-1'); + }); + + it('delegates update to IncidentsService', async () => { + const dto = { title: 'New' } as any; + await controller.update('inc-1', dto, actor); + expect(incidentsService.update).toHaveBeenCalledWith('inc-1', dto, actor); + }); + + it('delegates status transitions to IncidentStatusService', async () => { + const dto = { status: 'open', reason: 'triage' } as any; + await controller.updateStatus('inc-1', dto, actor); + expect(statusService.transition).toHaveBeenCalledWith('inc-1', 'open', actor, 'triage'); + }); + + it('delegates assignment to IncidentAssignmentService', async () => { + const dto = { assignedUserId: 'user-2' }; + await controller.assign('inc-1', dto, actor); + expect(assignmentService.assign).toHaveBeenCalledWith('inc-1', 'user-2', actor); + }); + + it('delegates severity updates to IncidentsService', async () => { + const dto = { severity: 'critical' } as any; + await controller.updateSeverity('inc-1', dto, actor); + expect(incidentsService.updateSeverity).toHaveBeenCalledWith('inc-1', 'critical', actor); + }); + + it('delegates priority updates to IncidentsService', async () => { + const dto = { priority: 'p1' } as any; + await controller.updatePriority('inc-1', dto, actor); + expect(incidentsService.updatePriority).toHaveBeenCalledWith('inc-1', 'p1', actor); + }); + + it('delegates archive to IncidentsService', async () => { + await controller.archive('inc-1', actor); + expect(incidentsService.archive).toHaveBeenCalledWith('inc-1', actor); + }); +}); diff --git a/apps/backend/src/modules/incidents/tests/incidents.repository.spec.ts b/apps/backend/src/modules/incidents/tests/incidents.repository.spec.ts new file mode 100644 index 0000000..05c79b4 --- /dev/null +++ b/apps/backend/src/modules/incidents/tests/incidents.repository.spec.ts @@ -0,0 +1,115 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../database/prisma.service'; +import { IncidentAuditAction } from '../enums/incident-audit-action.enum'; +import { IncidentsRepository } from '../repositories/incidents.repository'; + +describe('IncidentsRepository', () => { + let repository: IncidentsRepository; + const mockPrisma = { + incident: { + create: jest.fn(), + findFirst: jest.fn(), + findMany: jest.fn(), + count: jest.fn(), + update: jest.fn(), + }, + incidentAuditLog: { + create: jest.fn(), + findMany: jest.fn(), + }, + }; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [IncidentsRepository, { provide: PrismaService, useValue: mockPrisma }], + }).compile(); + + repository = module.get(IncidentsRepository); + }); + + it('creates an incident via prisma', async () => { + mockPrisma.incident.create.mockResolvedValue({ id: 'inc-1' }); + const result = await repository.create({ title: 't', description: 'd' } as any); + expect(mockPrisma.incident.create).toHaveBeenCalledWith({ + data: { title: 't', description: 'd' }, + }); + expect(result).toEqual({ id: 'inc-1' }); + }); + + it('excludes soft-deleted incidents by default on findById', async () => { + mockPrisma.incident.findFirst.mockResolvedValue(null); + await repository.findById('inc-1'); + expect(mockPrisma.incident.findFirst).toHaveBeenCalledWith({ + where: { id: 'inc-1', deleted: false }, + }); + }); + + it('includes soft-deleted incidents when requested', async () => { + mockPrisma.incident.findFirst.mockResolvedValue(null); + await repository.findById('inc-1', true); + expect(mockPrisma.incident.findFirst).toHaveBeenCalledWith({ where: { id: 'inc-1' } }); + }); + + it('builds pagination metadata from total count', async () => { + mockPrisma.incident.findMany.mockResolvedValue([{ id: '1' }, { id: '2' }]); + mockPrisma.incident.count.mockResolvedValue(45); + + const result = await repository.findMany( + { status: 'open' as any }, + { page: 2, limit: 20 }, + { sortBy: 'createdAt', sortOrder: 'desc' }, + ); + + expect(mockPrisma.incident.findMany).toHaveBeenCalledWith({ + where: { deleted: false, status: 'open' }, + orderBy: { createdAt: 'desc' }, + skip: 20, + take: 20, + }); + expect(result.meta).toEqual({ total: 45, page: 2, limit: 20, totalPages: 3 }); + }); + + it('applies a full-text search OR filter', async () => { + mockPrisma.incident.findMany.mockResolvedValue([]); + mockPrisma.incident.count.mockResolvedValue(0); + + await repository.findMany( + { search: '0xabc' }, + { page: 1, limit: 20 }, + { sortBy: 'createdAt', sortOrder: 'desc' }, + ); + + const where = mockPrisma.incident.findMany.mock.calls[0][0].where; + expect(where.OR).toBeDefined(); + expect(where.OR.length).toBeGreaterThan(0); + }); + + it('soft-deletes by setting deleted flag and timestamp', async () => { + mockPrisma.incident.update.mockResolvedValue({ id: 'inc-1', deleted: true }); + await repository.softDelete('inc-1'); + expect(mockPrisma.incident.update).toHaveBeenCalledWith({ + where: { id: 'inc-1' }, + data: { deleted: true, deletedAt: expect.any(Date) }, + }); + }); + + it('writes an audit entry', async () => { + mockPrisma.incidentAuditLog.create.mockResolvedValue({ id: 'audit-1' }); + await repository.addAuditEntry({ + incidentId: 'inc-1', + action: IncidentAuditAction.CREATED, + actorId: 'user-1', + }); + expect(mockPrisma.incidentAuditLog.create).toHaveBeenCalledWith({ + data: { + incidentId: 'inc-1', + action: IncidentAuditAction.CREATED, + actorId: 'user-1', + actorName: undefined, + previousValues: undefined, + newValues: undefined, + }, + }); + }); +}); diff --git a/apps/backend/src/modules/incidents/tests/incidents.service.spec.ts b/apps/backend/src/modules/incidents/tests/incidents.service.spec.ts new file mode 100644 index 0000000..c07287e --- /dev/null +++ b/apps/backend/src/modules/incidents/tests/incidents.service.spec.ts @@ -0,0 +1,169 @@ +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticatedUser } from '../../../common/interfaces/authenticated-user.interface'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { IncidentAuditAction } from '../enums/incident-audit-action.enum'; +import { IncidentPriority } from '../enums/incident-priority.enum'; +import { IncidentSeverity } from '../enums/incident-severity.enum'; +import { IncidentStatus } from '../enums/incident-status.enum'; +import { IncidentsRepository } from '../repositories/incidents.repository'; +import { IncidentsService } from '../services/incidents.service'; + +describe('IncidentsService', () => { + let service: IncidentsService; + const repository = { + create: jest.fn(), + findById: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + softDelete: jest.fn(), + addAuditEntry: jest.fn(), + findAuditTrail: jest.fn(), + }; + const notifications = { sendAlert: jest.fn() }; + const actor: AuthenticatedUser = { userId: 'user-1', name: 'Alice', roles: [] as any }; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + IncidentsService, + { provide: IncidentsRepository, useValue: repository }, + { provide: NotificationsService, useValue: notifications }, + ], + }).compile(); + + service = module.get(IncidentsService); + }); + + describe('create', () => { + it('creates an incident with defaults, audits it, and notifies', async () => { + repository.create.mockResolvedValue({ + id: 'inc-1', + title: 'Test', + description: 'desc', + severity: IncidentSeverity.MEDIUM, + }); + + const result = await service.create({ title: 'Test', description: 'desc' } as any, actor); + + expect(repository.create).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Test', + severity: IncidentSeverity.MEDIUM, + priority: IncidentPriority.P3, + status: IncidentStatus.NEW, + }), + ); + expect(repository.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ action: IncidentAuditAction.CREATED, incidentId: 'inc-1' }), + ); + expect(notifications.sendAlert).toHaveBeenCalled(); + expect(result.id).toBe('inc-1'); + }); + + it('connects the assignee when assignedUserId is provided', async () => { + repository.create.mockResolvedValue({ id: 'inc-1', severity: 'low' }); + await service.create( + { title: 'T', description: 'd', assignedUserId: 'user-9' } as any, + actor, + ); + expect(repository.create).toHaveBeenCalledWith( + expect.objectContaining({ assignedUser: { connect: { id: 'user-9' } } }), + ); + }); + }); + + describe('findOne', () => { + it('throws NotFoundException when incident does not exist', async () => { + repository.findById.mockResolvedValue(null); + await expect(service.findOne('missing')).rejects.toBeInstanceOf(NotFoundException); + }); + + it('returns the incident with its audit trail attached', async () => { + repository.findById.mockResolvedValue({ id: 'inc-1', title: 'T' }); + repository.findAuditTrail.mockResolvedValue([{ id: 'audit-1' }]); + + const result = await service.findOne('inc-1'); + + expect(result).toEqual({ id: 'inc-1', title: 'T', auditTrail: [{ id: 'audit-1' }] }); + }); + }); + + describe('findAll', () => { + it('delegates filtering/pagination/sorting to the repository', async () => { + repository.findMany.mockResolvedValue({ + items: [], + meta: { total: 0, page: 1, limit: 20, totalPages: 1 }, + }); + + await service.findAll({ + status: IncidentStatus.OPEN, + page: 2, + limit: 10, + sortBy: 'severity', + sortOrder: 'asc', + } as any); + + expect(repository.findMany).toHaveBeenCalledWith( + expect.objectContaining({ status: IncidentStatus.OPEN }), + { page: 2, limit: 10 }, + { sortBy: 'severity', sortOrder: 'asc' }, + ); + }); + }); + + describe('updateSeverity / updatePriority', () => { + it('updates severity and writes an audit entry', async () => { + repository.findById.mockResolvedValue({ id: 'inc-1', severity: IncidentSeverity.LOW }); + repository.update.mockResolvedValue({ id: 'inc-1', severity: IncidentSeverity.CRITICAL }); + + await service.updateSeverity('inc-1', IncidentSeverity.CRITICAL, actor); + + expect(repository.update).toHaveBeenCalledWith('inc-1', { + severity: IncidentSeverity.CRITICAL, + }); + expect(repository.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ action: IncidentAuditAction.SEVERITY_UPDATED }), + ); + }); + + it('updates priority and writes an audit entry', async () => { + repository.findById.mockResolvedValue({ id: 'inc-1', priority: IncidentPriority.P4 }); + repository.update.mockResolvedValue({ id: 'inc-1', priority: IncidentPriority.P1 }); + + await service.updatePriority('inc-1', IncidentPriority.P1, actor); + + expect(repository.update).toHaveBeenCalledWith('inc-1', { priority: IncidentPriority.P1 }); + expect(repository.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ action: IncidentAuditAction.PRIORITY_UPDATED }), + ); + }); + + it('throws NotFoundException for a missing incident on severity update', async () => { + repository.findById.mockResolvedValue(null); + await expect( + service.updateSeverity('missing', IncidentSeverity.HIGH, actor), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('archive', () => { + it('soft-deletes the incident and writes an audit entry', async () => { + repository.findById.mockResolvedValue({ id: 'inc-1' }); + repository.softDelete.mockResolvedValue({ id: 'inc-1', deleted: true }); + + await service.archive('inc-1', actor); + + expect(repository.softDelete).toHaveBeenCalledWith('inc-1'); + expect(repository.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ action: IncidentAuditAction.ARCHIVED }), + ); + }); + + it('throws NotFoundException when archiving a missing incident', async () => { + repository.findById.mockResolvedValue(null); + await expect(service.archive('missing', actor)).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index f42f5d1..bd77e40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,10 @@ "dependencies": { "@nestjs/common": "^11.1.27", "@nestjs/core": "^11.1.27", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.1.28", + "@nestjs/swagger": "^11.4.6", "@nestjs/typeorm": "^11.0.1", "@opentelemetry/api": "^1.9.1", "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", @@ -23,10 +27,15 @@ "@prisma/client": "^7.8.0", "@types/react": "^19.2.17", "axios": "^1.17.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", "ethers": "^6.17.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "pdfkit": "^0.19.1", "pg": "^8.21.0", "react": "^19.2.7", + "rxjs": "^7.8.2", "typeorm": "^1.0.0", "typescript": "^6.0.3" }, @@ -39,6 +48,7 @@ "@testing-library/user-event": "^14.6.1", "@types/jest": "^30.0.0", "@types/node": "^26.0.0", + "@types/passport-jwt": "^4.0.1", "@types/pdfkit": "^0.17.6", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", @@ -2278,6 +2288,12 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", @@ -2367,6 +2383,125 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", + "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0 || ^0.15.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz", + "integrity": "sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.2.0", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/swagger": { + "version": "11.4.6", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.6.tgz", + "integrity": "sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.1", + "js-yaml": "5.2.1", + "lodash": "4.18.1", + "path-to-regexp": "8.4.2", + "swagger-ui-dist": "5.32.8" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/swagger/node_modules/js-yaml": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz", + "integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, "node_modules/@nestjs/testing": { "version": "11.1.27", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.27.tgz", @@ -3425,6 +3560,13 @@ } } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -3647,6 +3789,59 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.2.tgz", + "integrity": "sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3726,6 +3921,16 @@ "parse5": "^7.0.0" } }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -3733,6 +3938,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", @@ -3749,6 +3960,38 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/pdfkit": { "version": "0.17.6", "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.6.tgz", @@ -3759,6 +4002,20 @@ "@types/node": "*" } }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -3767,6 +4024,27 @@ "csstype": "^3.2.2" } }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3781,6 +4059,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4311,6 +4595,44 @@ "win32" ] }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -4448,11 +4770,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -4662,6 +4989,43 @@ "integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==", "devOptional": true }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", @@ -4760,13 +5124,38 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/c12": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", @@ -4807,6 +5196,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4958,6 +5363,23 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", + "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -5095,12 +5517,49 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "devOptional": true }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/conventional-changelog-angular": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", @@ -5153,6 +5612,41 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -5399,6 +5893,15 @@ "node": ">=0.10" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5510,6 +6013,21 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/effect": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", @@ -5554,6 +6072,15 @@ "node": ">=14" } }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -5651,6 +6178,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -5944,6 +6477,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ethers": { "version": "6.17.0", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", @@ -6055,8 +6597,76 @@ "jest-mock": "30.4.1", "jest-util": "30.4.1" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/exsolve": { @@ -6243,6 +6853,27 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6349,12 +6980,30 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/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/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6841,6 +7490,26 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -6913,7 +7582,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "devOptional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -7042,7 +7710,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -7052,6 +7719,15 @@ "dev": true, "license": "ISC" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7164,6 +7840,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -8563,6 +9245,49 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8607,6 +9332,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.9.tgz", + "integrity": "sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==", + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -8874,7 +9605,6 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -8883,6 +9613,18 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, "node_modules/lodash.isfunction": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", @@ -8890,11 +9632,28 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, "node_modules/lodash.kebabcase": { @@ -8925,6 +9684,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", @@ -9104,6 +9869,15 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/meow": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", @@ -9117,6 +9891,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9275,6 +10061,47 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mysql2": { "version": "3.15.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", @@ -9330,6 +10157,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -9400,17 +10236,49 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "devOptional": true }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -9550,6 +10418,51 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9635,6 +10548,11 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "devOptional": true }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pdfkit": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.19.1.tgz", @@ -10078,6 +10996,19 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -10112,6 +11043,22 @@ } ] }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10143,6 +11090,34 @@ "node": ">=8" } }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/rc9": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", @@ -10342,7 +11317,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -10577,6 +11551,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -10612,7 +11602,7 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "peer": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } @@ -10621,7 +11611,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -10641,8 +11630,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/saxes": { "version": "6.0.0", @@ -10664,17 +11652,67 @@ "devOptional": true, "peer": true }, - "node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/seq-queue": { @@ -10683,6 +11721,31 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", "devOptional": true }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10704,6 +11767,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -10875,17 +12010,33 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "devOptional": true }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -11125,6 +12276,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.32.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz", + "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -11285,6 +12445,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/token-types": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", @@ -11468,6 +12637,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typeorm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-1.0.0.tgz", @@ -11641,6 +12872,15 @@ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -11724,9 +12964,17 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -11767,6 +13015,24 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -11983,7 +13249,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -13654,6 +14919,11 @@ "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" }, + "@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==" + }, "@napi-rs/wasm-runtime": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", @@ -13688,6 +14958,62 @@ "uid": "2.0.2" } }, + "@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "requires": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + } + }, + "@nestjs/mapped-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", + "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==", + "requires": {} + }, + "@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "requires": {} + }, + "@nestjs/platform-express": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz", + "integrity": "sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==", + "requires": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.2.0", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1" + } + }, + "@nestjs/swagger": { + "version": "11.4.6", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.6.tgz", + "integrity": "sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==", + "requires": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.1", + "js-yaml": "5.2.1", + "lodash": "4.18.1", + "path-to-regexp": "8.4.2", + "swagger-ui-dist": "5.32.8" + }, + "dependencies": { + "js-yaml": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz", + "integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==", + "requires": { + "argparse": "^2.0.1" + } + } + } + }, "@nestjs/testing": { "version": "11.1.27", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.27.tgz", @@ -14370,6 +15696,11 @@ "devOptional": true, "requires": {} }, + "@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==" + }, "@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -14540,6 +15871,54 @@ "@babel/types": "^7.28.2" } }, + "@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "@types/express-serve-static-core": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.2.tgz", + "integrity": "sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -14605,12 +15984,26 @@ "parse5": "^7.0.0" } }, + "@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "requires": { + "@types/ms": "*", + "@types/node": "*" + } + }, "@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, + "@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, "@types/node": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", @@ -14625,6 +16018,35 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "requires": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*" + } + }, "@types/pdfkit": { "version": "0.17.6", "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.6.tgz", @@ -14634,6 +16056,18 @@ "@types/node": "*" } }, + "@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, "@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -14642,6 +16076,25 @@ "csstype": "^3.2.2" } }, + "@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "requires": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -14654,6 +16107,11 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==" + }, "@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -14930,6 +16388,30 @@ "dev": true, "optional": true }, + "accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "requires": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -15015,11 +16497,15 @@ } } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "aria-query": { "version": "5.3.0", @@ -15163,6 +16649,29 @@ "integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==", "devOptional": true }, + "body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "requires": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "dependencies": { + "content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==" + } + } + }, "brace-expansion": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", @@ -15228,11 +16737,28 @@ "node-int64": "^0.4.0" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "c12": { "version": "3.3.4", @@ -15263,6 +16789,15 @@ "function-bind": "^1.1.2" } }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -15348,6 +16883,21 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==" }, + "class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "class-validator": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", + "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", + "requires": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, "cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -15443,12 +16993,33 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "devOptional": true }, + "content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==" + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, "conventional-changelog-angular": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", @@ -15485,6 +17056,25 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + }, + "cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -15646,6 +17236,11 @@ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", "devOptional": true }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, "dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -15725,6 +17320,19 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "effect": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", @@ -15758,6 +17366,11 @@ "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", "devOptional": true }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -15819,6 +17432,11 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -16005,6 +17623,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, "ethers": { "version": "6.17.0", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", @@ -16090,6 +17713,56 @@ "jest-util": "30.4.1" } }, + "express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "requires": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -16216,6 +17889,19 @@ "to-regex-range": "^5.0.1" } }, + "finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "requires": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -16286,11 +17972,21 @@ "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, "forwarded-parse": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==" }, + "fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -16620,6 +18316,18 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "requires": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + } + }, "http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -16669,7 +18377,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "devOptional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } @@ -16741,8 +18448,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", @@ -16750,6 +18456,11 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -16821,6 +18532,11 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, + "is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -17789,6 +19505,42 @@ "through": ">=2.2.7 <3" } }, + "jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "requires": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + } + }, + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "requires": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -17820,6 +19572,11 @@ "type-check": "~0.4.0" } }, + "libphonenumber-js": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.9.tgz", + "integrity": "sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==" + }, "lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -17976,25 +19733,48 @@ "lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, "lodash.isfunction": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", "dev": true }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, "lodash.kebabcase": { "version": "4.1.1", @@ -18020,6 +19800,11 @@ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", @@ -18140,12 +19925,22 @@ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" }, + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, "meow": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", "dev": true }, + "merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -18249,6 +20044,33 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "dependencies": { + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + } + } + }, "mysql2": { "version": "3.15.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", @@ -18287,6 +20109,11 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -18338,17 +20165,34 @@ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" + }, "ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "devOptional": true }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "requires": { "wrappy": "1" } @@ -18441,6 +20285,35 @@ "entities": "^6.0.0" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "requires": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -18500,6 +20373,11 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "devOptional": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "pdfkit": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.19.1.tgz", @@ -18805,6 +20683,15 @@ "long": "^5.3.2" } }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, "proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -18822,6 +20709,15 @@ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "devOptional": true }, + "qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "requires": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -18834,6 +20730,22 @@ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, + "range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==" + }, + "raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "requires": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + } + }, "rc9": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", @@ -18982,7 +20894,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -19133,6 +21044,18 @@ "glob": "^7.1.3" } }, + "router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "requires": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + } + }, "rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -19152,7 +21075,6 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "peer": true, "requires": { "tslib": "^2.1.0" } @@ -19160,14 +21082,12 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "saxes": { "version": "6.0.0", @@ -19188,8 +21108,40 @@ "semver": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "dev": true + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==" + }, + "send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "requires": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "requires": { + "mime-db": "^1.54.0" + } + } + } }, "seq-queue": { "version": "0.0.5", @@ -19197,6 +21149,22 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", "devOptional": true }, + "serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "requires": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -19212,6 +21180,50 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "devOptional": true }, + "side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -19329,17 +21341,26 @@ } } }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + }, "std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "devOptional": true }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "requires": { "safe-buffer": "~5.2.0" } @@ -19496,6 +21517,14 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "swagger-ui-dist": { + "version": "5.32.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz", + "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==", + "requires": { + "@scarf/scarf": "=1.4.0" + } + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -19614,6 +21643,11 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, "token-types": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", @@ -19712,6 +21746,41 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "requires": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "dependencies": { + "content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==" + }, + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "typeorm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-1.0.0.tgz", @@ -19785,6 +21854,11 @@ } } }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, "unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -19838,8 +21912,12 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "v8-to-istanbul": { "version": "9.3.0", @@ -19869,6 +21947,16 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, "w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -20017,8 +22105,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "write-file-atomic": { "version": "5.0.1", diff --git a/package.json b/package.json index cce9a27..780f1d1 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,10 @@ "dependencies": { "@nestjs/common": "^11.1.27", "@nestjs/core": "^11.1.27", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.1.28", + "@nestjs/swagger": "^11.4.6", "@nestjs/typeorm": "^11.0.1", "@opentelemetry/api": "^1.9.1", "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", @@ -66,10 +70,15 @@ "@prisma/client": "^7.8.0", "@types/react": "^19.2.17", "axios": "^1.17.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", "ethers": "^6.17.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "pdfkit": "^0.19.1", "pg": "^8.21.0", "react": "^19.2.7", + "rxjs": "^7.8.2", "typeorm": "^1.0.0", "typescript": "^6.0.3" }, @@ -91,6 +100,7 @@ "@testing-library/user-event": "^14.6.1", "@types/jest": "^30.0.0", "@types/node": "^26.0.0", + "@types/passport-jwt": "^4.0.1", "@types/pdfkit": "^0.17.6", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", diff --git a/prisma/migrations/20260719120000_add_incidents/migration.sql b/prisma/migrations/20260719120000_add_incidents/migration.sql new file mode 100644 index 0000000..f6531ba --- /dev/null +++ b/prisma/migrations/20260719120000_add_incidents/migration.sql @@ -0,0 +1,61 @@ +-- CreateTable +CREATE TABLE "incidents" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'new', + "severity" TEXT NOT NULL DEFAULT 'medium', + "priority" TEXT NOT NULL DEFAULT 'p3', + "category" TEXT, + "organizationId" TEXT, + "assignedUserId" TEXT, + "sourceAlertIds" TEXT[] DEFAULT ARRAY[]::TEXT[], + "detectionSource" TEXT, + "tags" TEXT[] DEFAULT ARRAY[]::TEXT[], + "deleted" BOOLEAN NOT NULL DEFAULT false, + "deletedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "resolvedAt" TIMESTAMP(3), + "closedAt" TIMESTAMP(3), + + CONSTRAINT "incidents_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "incident_audit_logs" ( + "id" TEXT NOT NULL, + "incidentId" TEXT NOT NULL, + "action" TEXT NOT NULL, + "actorId" TEXT NOT NULL, + "actorName" TEXT, + "previousValues" JSONB, + "newValues" JSONB, + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "incident_audit_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "incidents_status_idx" ON "incidents"("status"); + +-- CreateIndex +CREATE INDEX "incidents_severity_idx" ON "incidents"("severity"); + +-- CreateIndex +CREATE INDEX "incidents_priority_idx" ON "incidents"("priority"); + +-- CreateIndex +CREATE INDEX "incidents_assignedUserId_idx" ON "incidents"("assignedUserId"); + +-- CreateIndex +CREATE INDEX "incidents_organizationId_idx" ON "incidents"("organizationId"); + +-- CreateIndex +CREATE INDEX "incident_audit_logs_incidentId_idx" ON "incident_audit_logs"("incidentId"); + +-- AddForeignKey +ALTER TABLE "incidents" ADD CONSTRAINT "incidents_assignedUserId_fkey" FOREIGN KEY ("assignedUserId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "incident_audit_logs" ADD CONSTRAINT "incident_audit_logs_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "incidents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9e92f95..39bab3c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,6 +15,7 @@ model User { notificationPreferences NotificationPreference[] auditLogs AuditLog[] apiKeys ApiKey[] + assignedIncidents Incident[] @@map("users") } @@ -173,4 +174,55 @@ model NoteAuditLog { @@index([noteId]) @@map("note_audit_logs") +} + +// --------------------------------------------------------------------------- +// Incident Management – issue #120 +// --------------------------------------------------------------------------- + +model Incident { + id String @id @default(cuid()) + title String + description String + status String @default("new") // new|open|acknowledged|investigating|contained|resolved|closed|reopened + severity String @default("medium") // low|medium|high|critical + priority String @default("p3") // p1|p2|p3|p4 + category String? + organizationId String? + assignedUserId String? + sourceAlertIds String[] @default([]) + detectionSource String? + tags String[] @default([]) + deleted Boolean @default(false) + deletedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + resolvedAt DateTime? + closedAt DateTime? + + assignedUser User? @relation(fields: [assignedUserId], references: [id]) + auditEntries IncidentAuditLog[] + + @@index([status]) + @@index([severity]) + @@index([priority]) + @@index([assignedUserId]) + @@index([organizationId]) + @@map("incidents") +} + +model IncidentAuditLog { + id String @id @default(cuid()) + incidentId String + action String // incident_created|status_changed|owner_assigned|owner_reassigned|owner_removed|severity_updated|priority_updated|incident_updated|resolved|closed|reopened + actorId String + actorName String? + previousValues Json? + newValues Json? + timestamp DateTime @default(now()) + + incident Incident @relation(fields: [incidentId], references: [id]) + + @@index([incidentId]) + @@map("incident_audit_logs") } \ No newline at end of file