From a1f494454e3826be4256a0b7b6a4b43872d997db Mon Sep 17 00:00:00 2001 From: whitezaddy Date: Fri, 26 Jun 2026 14:51:32 +0100 Subject: [PATCH 1/4] feat: expose observability dashboard metrics for testnet monitoring --- .../src/audit/audit.controller.spec.ts | 26 ++++++ app/backend/src/audit/audit.controller.ts | 22 ++++- app/backend/src/audit/audit.module.ts | 2 + app/backend/src/audit/audit.service.ts | 93 +++++++++++++------ app/backend/src/audit/metrics.interceptor.ts | 37 ++++++++ app/backend/src/audit/metrics.module.ts | 9 ++ app/backend/src/audit/metrics.service.ts | 62 +++++++++++++ 7 files changed, 222 insertions(+), 29 deletions(-) create mode 100644 app/backend/src/audit/metrics.interceptor.ts create mode 100644 app/backend/src/audit/metrics.module.ts create mode 100644 app/backend/src/audit/metrics.service.ts diff --git a/app/backend/src/audit/audit.controller.spec.ts b/app/backend/src/audit/audit.controller.spec.ts index b7bc0548..280b435c 100644 --- a/app/backend/src/audit/audit.controller.spec.ts +++ b/app/backend/src/audit/audit.controller.spec.ts @@ -1,10 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuditController } from './audit.controller'; import { AuditService } from './audit.service'; +import { MetricsService } from '../metrics/metrics.service'; describe('AuditController', () => { let controller: AuditController; let service: AuditService; + let metricsService: MetricsService; const mockExportResult = { data: [ @@ -30,11 +32,19 @@ describe('AuditController', () => { exportLogs: jest.fn().mockResolvedValue(mockExportResult), buildCsv: jest.fn().mockReturnValue('id,actorHash,...\nlog-1,...'), }; + + const mockMetricsService = { + getMetrics: jest.fn().mockResolvedValue('# HELP ...'), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [AuditController], providers: [ + { + provide: MetricsService, + useValue: mockMetricsService, + }, { provide: AuditService, useValue: mockAuditService, @@ -44,6 +54,7 @@ describe('AuditController', () => { controller = module.get(AuditController); service = module.get(AuditService); + metricsService = module.get(MetricsService); }); it('should be defined', () => { @@ -117,4 +128,19 @@ describe('AuditController', () => { }); }); }); + + describe('getMetrics', () => { + it('should call metricsService.getMetrics and set headers', async () => { + const res = { + set: jest.fn(), + send: jest.fn(), + } as any; + + await controller.getMetrics(res); + + expect(metricsService.getMetrics).toHaveBeenCalled(); + expect(res.set).toHaveBeenCalledWith('Content-Type', 'text/plain'); + expect(res.send).toHaveBeenCalledWith('# HELP ...'); + }); + }); }); diff --git a/app/backend/src/audit/audit.controller.ts b/app/backend/src/audit/audit.controller.ts index 94126efe..856a2272 100644 --- a/app/backend/src/audit/audit.controller.ts +++ b/app/backend/src/audit/audit.controller.ts @@ -1,4 +1,11 @@ -import { Controller, Get, Query, Res, Version } from '@nestjs/common'; +import { + Controller, + Get, + Query, + Res, + UseInterceptors, + Version, +} from '@nestjs/common'; import { Response } from 'express'; import { AuditService, AuditQuery, ExportAuditQuery } from './audit.service'; import { @@ -9,12 +16,15 @@ import { ApiUnauthorizedResponse, ApiBearerAuth, } from '@nestjs/swagger'; +import { MetricsInterceptor } from '../metrics/metrics.interceptor'; +import { MetricsService } from '../metrics/metrics.service'; @ApiTags('Audit') @ApiBearerAuth('JWT-auth') @Controller('audit') +@UseInterceptors(MetricsInterceptor) export class AuditController { - constructor(private readonly auditService: AuditService) {} + constructor(private readonly auditService: AuditService, private metricsService: MetricsService) {} @Get() @Version('1') @@ -130,4 +140,12 @@ export class AuditController { return result; } + + @Get('metrics') + @ApiOperation({ summary: 'Get service metrics' }) + @ApiOkResponse({ description: 'Prometheus metrics exported successfully.' }) + async getMetrics(@Res() res: Response) { + res.set('Content-Type', 'text/plain'); + res.send(await this.metricsService.getMetrics()); + } } diff --git a/app/backend/src/audit/audit.module.ts b/app/backend/src/audit/audit.module.ts index 8e64cdd1..f36c64c3 100644 --- a/app/backend/src/audit/audit.module.ts +++ b/app/backend/src/audit/audit.module.ts @@ -1,9 +1,11 @@ import { Module, Global } from '@nestjs/common'; import { AuditService } from './audit.service'; import { AuditController } from './audit.controller'; +import { MetricsModule } from '../metrics/metrics.module'; @Global() @Module({ + imports: [MetricsModule], providers: [AuditService], controllers: [AuditController], exports: [AuditService], diff --git a/app/backend/src/audit/audit.service.ts b/app/backend/src/audit/audit.service.ts index dad30538..eb55995f 100644 --- a/app/backend/src/audit/audit.service.ts +++ b/app/backend/src/audit/audit.service.ts @@ -4,6 +4,7 @@ import { Type } from 'class-transformer'; import { IsInt, IsOptional, Max, Min } from 'class-validator'; import { PrismaService } from '../prisma/prisma.service'; +import { MetricsService } from '../metrics/metrics.service'; import { Prisma } from '@prisma/client'; export interface AuditLogParams { @@ -65,22 +66,37 @@ export interface ExportAuditResult { @Injectable() export class AuditService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + private metrics: MetricsService, + ) {} anonymize(value: string): string { return createHash('sha256').update(value).digest('hex').slice(0, 16); } async record(params: AuditLogParams) { - return this.prisma.auditLog.create({ - data: { - actorId: params.actorId, - entity: params.entity, - entityId: params.entityId, - action: params.action, - metadata: (params.metadata as Prisma.InputJsonValue) ?? {}, - }, + const end = this.metrics.dbQueryDuration.startTimer({ + operation: 'create', + entity: 'AuditLog', }); + try { + const result = await this.prisma.auditLog.create({ + data: { + actorId: params.actorId, + entity: params.entity, + entityId: params.entityId, + action: params.action, + metadata: (params.metadata as Prisma.InputJsonValue) ?? {}, + }, + }); + end(); + return result; + } catch (error) { + this.metrics.dbErrorsTotal.inc({ operation: 'create', entity: 'AuditLog' }); + end(); + throw error; + } } async findLogs(query: AuditQuery) { @@ -101,15 +117,27 @@ export class AuditService { if (query.endTime) where.timestamp.lte = new Date(query.endTime); } - const [rows, total] = await this.prisma.$transaction([ - this.prisma.auditLog.findMany({ - where, - orderBy: { timestamp: 'desc' }, - skip, - take: limit, - }), - this.prisma.auditLog.count({ where }), - ]); + const end = this.metrics.dbQueryDuration.startTimer({ + operation: 'findMany', + entity: 'AuditLog', + }); + try { + const [rows, total] = await this.prisma.$transaction([ + this.prisma.auditLog.findMany({ + where, + orderBy: { timestamp: 'desc' }, + skip, + take: limit, + }), + this.prisma.auditLog.count({ where }), + ]); + end(); + return { data: rows, total, page, limit }; + } catch (error) { + this.metrics.dbErrorsTotal.inc({ operation: 'findMany', entity: 'AuditLog' }); + end(); + throw error; + } return { data: rows, total, page, limit }; } @@ -137,15 +165,26 @@ export class AuditService { if (query.to) where.timestamp.lte = new Date(query.to); } - const [rows, total] = await this.prisma.$transaction([ - this.prisma.auditLog.findMany({ - where, - orderBy: { timestamp: 'desc' }, - skip, - take: limit, - }), - this.prisma.auditLog.count({ where }), - ]); + const end = this.metrics.dbQueryDuration.startTimer({ + operation: 'export', + entity: 'AuditLog', + }); + try { + const [rows, total] = await this.prisma.$transaction([ + this.prisma.auditLog.findMany({ + where, + orderBy: { timestamp: 'desc' }, + skip, + take: limit, + }), + this.prisma.auditLog.count({ where }), + ]); + end(); + } catch (error) { + this.metrics.dbErrorsTotal.inc({ operation: 'export', entity: 'AuditLog' }); + end(); + throw error; + } const data: AnonymizedAuditLog[] = rows.map(row => ({ id: row.id, diff --git a/app/backend/src/audit/metrics.interceptor.ts b/app/backend/src/audit/metrics.interceptor.ts new file mode 100644 index 00000000..209d2273 --- /dev/null +++ b/app/backend/src/audit/metrics.interceptor.ts @@ -0,0 +1,37 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { Request, Response } from 'express'; +import { MetricsService } from './metrics.service'; + +@Injectable() +export class MetricsInterceptor implements NestInterceptor { + constructor(private metricsService: MetricsService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const httpContext = context.switchToHttp(); + const request = httpContext.getRequest(); + const response = httpContext.getResponse(); + const startTime = Date.now(); + + return next.handle().pipe( + tap(() => { + const duration = (Date.now() - startTime) / 1000; + const route = request.route?.path ?? request.path; + const statusCode = response.statusCode; + + this.metricsService.httpRequestDuration.observe( + { method: request.method, route, status_code: statusCode }, + duration, + ); + + this.metricsService.httpRequestsTotal.inc({ method: request.method, route, status_code: statusCode }); + }), + ); + } +} \ No newline at end of file diff --git a/app/backend/src/audit/metrics.module.ts b/app/backend/src/audit/metrics.module.ts new file mode 100644 index 00000000..bcb2be2a --- /dev/null +++ b/app/backend/src/audit/metrics.module.ts @@ -0,0 +1,9 @@ +import { Module, Global } from '@nestjs/common'; +import { MetricsService } from './metrics.service'; + +@Global() +@Module({ + providers: [MetricsService], + exports: [MetricsService], +}) +export class MetricsModule {} \ No newline at end of file diff --git a/app/backend/src/audit/metrics.service.ts b/app/backend/src/audit/metrics.service.ts new file mode 100644 index 00000000..88b32209 --- /dev/null +++ b/app/backend/src/audit/metrics.service.ts @@ -0,0 +1,62 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { + Counter, + Histogram, + Registry, + collectDefaultMetrics, +} from 'prom-client'; + +@Injectable() +export class MetricsService implements OnModuleInit { + private readonly registry = new Registry(); + + public httpRequestDuration: Histogram; + public httpRequestsTotal: Counter; + public dbQueryDuration: Histogram; + public dbErrorsTotal: Counter; + + constructor() { + this.registry.setDefaultLabels({ + app: 'soter-backend', + }); + } + + onModuleInit() { + collectDefaultMetrics({ register: this.registry }); + this.registerCustomMetrics(); + } + + private registerCustomMetrics() { + this.httpRequestDuration = new Histogram({ + name: 'http_request_duration_seconds', + help: 'Duration of HTTP requests in seconds', + labelNames: ['method', 'route', 'status_code'], + registers: [this.registry], + }); + + this.httpRequestsTotal = new Counter({ + name: 'http_requests_total', + help: 'Total number of HTTP requests', + labelNames: ['method', 'route', 'status_code'], + registers: [this.registry], + }); + + this.dbQueryDuration = new Histogram({ + name: 'db_query_duration_seconds', + help: 'Duration of database queries in seconds', + labelNames: ['operation', 'entity'], + registers: [this.registry], + }); + + this.dbErrorsTotal = new Counter({ + name: 'db_errors_total', + help: 'Total number of database query errors', + labelNames: ['operation', 'entity'], + registers: [this.registry], + }); + } + + getMetrics() { + return this.registry.metrics(); + } +} \ No newline at end of file From 6eedd76d8ea9d0680098946ab94acf9b93bd03a6 Mon Sep 17 00:00:00 2001 From: whitezaddy Date: Fri, 26 Jun 2026 14:57:27 +0100 Subject: [PATCH 2/4] feat: implement idempotent webhook endpoint with HMAC verification for AI service callbacks --- app/backend/src/audit/ai-verification.dto.ts | 31 ++++++++ app/backend/src/audit/config.ts | 5 ++ app/backend/src/audit/hmac.guard.spec.ts | 79 +++++++++++++++++++ app/backend/src/audit/hmac.guard.ts | 43 ++++++++++ .../src/audit/webhook.controller.spec.ts | 64 +++++++++++++++ app/backend/src/audit/webhook.controller.ts | 44 +++++++++++ app/backend/src/audit/webhook.module.ts | 9 +++ 7 files changed, 275 insertions(+) create mode 100644 app/backend/src/audit/ai-verification.dto.ts create mode 100644 app/backend/src/audit/config.ts create mode 100644 app/backend/src/audit/hmac.guard.spec.ts create mode 100644 app/backend/src/audit/hmac.guard.ts create mode 100644 app/backend/src/audit/webhook.controller.spec.ts create mode 100644 app/backend/src/audit/webhook.controller.ts create mode 100644 app/backend/src/audit/webhook.module.ts diff --git a/app/backend/src/audit/ai-verification.dto.ts b/app/backend/src/audit/ai-verification.dto.ts new file mode 100644 index 00000000..16b25c34 --- /dev/null +++ b/app/backend/src/audit/ai-verification.dto.ts @@ -0,0 +1,31 @@ +import { + IsString, + IsNotEmpty, + IsEnum, + IsObject, + IsUUID, +} from 'class-validator'; + +export enum VerificationStatus { + COMPLETED = 'completed', + FAILED = 'failed', +} + +export class AiVerificationPayloadDto { + @IsUUID('4') + idempotencyKey: string; + + @IsString() + @IsNotEmpty() + sessionId: string; + + @IsString() + @IsNotEmpty() + stepId: string; + + @IsEnum(VerificationStatus) + status: VerificationStatus; + + @IsObject() + output: Record; +} \ No newline at end of file diff --git a/app/backend/src/audit/config.ts b/app/backend/src/audit/config.ts new file mode 100644 index 00000000..fe4b9ac7 --- /dev/null +++ b/app/backend/src/audit/config.ts @@ -0,0 +1,5 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('app', () => ({ + aiWebhookSecret: process.env.AI_WEBHOOK_SECRET, +})); \ No newline at end of file diff --git a/app/backend/src/audit/hmac.guard.spec.ts b/app/backend/src/audit/hmac.guard.spec.ts new file mode 100644 index 00000000..0182cf54 --- /dev/null +++ b/app/backend/src/audit/hmac.guard.spec.ts @@ -0,0 +1,79 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import * as crypto from 'crypto'; +import { HmacGuard } from './hmac.guard'; +import appConfig from '../config/config'; + +describe('HmacGuard', () => { + let guard: HmacGuard; + const secret = 'test-secret'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [appConfig], + }), + ], + providers: [HmacGuard], + }) + .overrideProvider(appConfig.KEY) + .useValue({ aiWebhookSecret: secret }) + .compile(); + + guard = module.get(HmacGuard); + }); + + const createMockContext = (headers: any, rawBody: Buffer | null) => + ({ + switchToHttp: () => ({ + getRequest: () => ({ + headers, + rawBody, + }), + }), + } as unknown as ExecutionContext); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('should throw UnauthorizedException if signature header is missing', () => { + const context = createMockContext({}, Buffer.from('')); + expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); + }); + + it('should throw an error if rawBody is not available', () => { + const context = createMockContext( + { 'x-signature-hmac-sha256': 'any-sig' }, + null, + ); + expect(() => guard.canActivate(context)).toThrow( + 'Raw body not available. Ensure `rawBody: true` is set in NestFactory.', + ); + }); + + it('should throw UnauthorizedException for an invalid signature', () => { + const body = JSON.stringify({ data: 'test' }); + const context = createMockContext( + { 'x-signature-hmac-sha256': 'invalid-signature' }, + Buffer.from(body), + ); + expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); + }); + + it('should return true for a valid signature', () => { + const body = JSON.stringify({ data: 'test' }); + const hmac = crypto.createHmac('sha256', secret); + const signature = hmac.update(body).digest('hex'); + + const context = createMockContext( + { 'x-signature-hmac-sha256': signature }, + Buffer.from(body), + ); + + const result = guard.canActivate(context); + expect(result).toBe(true); + }); +}); \ No newline at end of file diff --git a/app/backend/src/audit/hmac.guard.ts b/app/backend/src/audit/hmac.guard.ts new file mode 100644 index 00000000..ace10039 --- /dev/null +++ b/app/backend/src/audit/hmac.guard.ts @@ -0,0 +1,43 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, + Inject, +} from '@nestjs/common'; +import { ConfigType } from '@nestjs/config'; +import { Request } from 'express'; +import * as crypto from 'crypto'; +import appConfig from '../config/config'; + +@Injectable() +export class HmacGuard implements CanActivate { + constructor( + @Inject(appConfig.KEY) + private readonly config: ConfigType, + ) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const signature = request.headers['x-signature-hmac-sha256'] as string; + + if (!signature) { + throw new UnauthorizedException('Missing signature header'); + } + + if (!request.rawBody) { + throw new Error( + 'Raw body not available. Ensure `rawBody: true` is set in NestFactory.', + ); + } + + const hmac = crypto.createHmac('sha256', this.config.aiWebhookSecret); + const digest = hmac.update(request.rawBody).digest('hex'); + + if (digest !== signature) { + throw new UnauthorizedException('Invalid signature'); + } + + return true; + } +} \ No newline at end of file diff --git a/app/backend/src/audit/webhook.controller.spec.ts b/app/backend/src/audit/webhook.controller.spec.ts new file mode 100644 index 00000000..35f50e43 --- /dev/null +++ b/app/backend/src/audit/webhook.controller.spec.ts @@ -0,0 +1,64 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { WebhookController } from './webhook.controller'; +import { SessionService } from '../session/session.service'; +import { HmacGuard } from './hmac.guard'; +import { AiVerificationPayloadDto, VerificationStatus } from './dto/ai-verification.dto'; + +describe('WebhookController', () => { + let controller: WebhookController; + let sessionService: SessionService; + + const mockSessionService = { + submitToStep: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [WebhookController], + providers: [ + { + provide: SessionService, + useValue: mockSessionService, + }, + ], + }) + .overrideGuard(HmacGuard) + .useValue({ canActivate: () => true }) // Mock the guard to always pass + .compile(); + + controller = module.get(WebhookController); + sessionService = module.get(SessionService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('handleAiVerification', () => { + it('should call sessionService.submitToStep with the correct parameters', async () => { + const payload: AiVerificationPayloadDto = { + idempotencyKey: 'd9e1b233-8083-4a25-8236-c69a997c306a', + sessionId: 'session-123', + stepId: 'step-456', + status: VerificationStatus.COMPLETED, + output: { verificationScore: 0.95 }, + }; + + mockSessionService.submitToStep.mockResolvedValue({ isIdempotent: false }); + + const result = await controller.handleAiVerification(payload); + + expect(sessionService.submitToStep).toHaveBeenCalledWith( + payload.sessionId, + payload.stepId, + { + submissionKey: payload.idempotencyKey, + payload: payload.output, + }, + payload.status, + ); + + expect(result).toEqual({ status: 'received', isIdempotent: false }); + }); + }); +}); \ No newline at end of file diff --git a/app/backend/src/audit/webhook.controller.ts b/app/backend/src/audit/webhook.controller.ts new file mode 100644 index 00000000..da2f4c55 --- /dev/null +++ b/app/backend/src/audit/webhook.controller.ts @@ -0,0 +1,44 @@ +import { + Controller, + Post, + Body, + UseGuards, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiOkResponse, ApiUnauthorizedResponse, ApiHeader } from '@nestjs/swagger'; +import { SessionService } from '../session/session.service'; +import { HmacGuard } from './hmac.guard'; +import { AiVerificationPayloadDto } from './dto/ai-verification.dto'; + +@ApiTags('Webhooks') +@Controller('webhooks') +export class WebhookController { + constructor(private readonly sessionService: SessionService) {} + + @Post('ai-verification') + @UseGuards(HmacGuard) + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Receive AI verification results', + description: + 'A secure endpoint for AI services to post back verification results. Requires HMAC signature.', + }) + @ApiHeader({ + name: 'X-Signature-Hmac-Sha256', + description: 'HMAC-SHA256 signature of the raw request body.', + required: true, + }) + @ApiOkResponse({ description: 'Webhook received and processed successfully.' }) + @ApiUnauthorizedResponse({ description: 'Invalid or missing HMAC signature.' }) + async handleAiVerification(@Body() payload: AiVerificationPayloadDto) { + const result = await this.sessionService.submitToStep( + payload.sessionId, + payload.stepId, + { submissionKey: payload.idempotencyKey, payload: payload.output }, + payload.status, + ); + + return { status: 'received', isIdempotent: result.isIdempotent }; + } +} \ No newline at end of file diff --git a/app/backend/src/audit/webhook.module.ts b/app/backend/src/audit/webhook.module.ts new file mode 100644 index 00000000..712e4534 --- /dev/null +++ b/app/backend/src/audit/webhook.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { SessionModule } from '../session/session.module'; +import { WebhookController } from './webhook.controller'; + +@Module({ + imports: [SessionModule], + controllers: [WebhookController], +}) +export class WebhookModule {} \ No newline at end of file From da2105c6e9e0eb508f7d29a8f259de4ff0aecffe Mon Sep 17 00:00:00 2001 From: whitezaddy Date: Fri, 26 Jun 2026 15:08:03 +0100 Subject: [PATCH 3/4] feat: Updated implement idempotent webhook endpoint with HMAC verification for AI service callbacks --- app/backend/src/ai-verification.dto.ts | 30 ++++++ app/backend/src/app.module.ts | 4 +- app/backend/src/hmac-auth.guard.ts | 58 ++++++++++ app/backend/src/main.ts | 9 ++ app/backend/src/webhooks.controller.spec.ts | 59 ++++++++++ app/backend/src/webhooks.controller.ts | 34 ++++++ app/backend/src/webhooks.module.ts | 12 +++ app/backend/src/webhooks.service.spec.ts | 114 ++++++++++++++++++++ app/backend/src/webhooks.service.ts | 73 +++++++++++++ 9 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 app/backend/src/ai-verification.dto.ts create mode 100644 app/backend/src/hmac-auth.guard.ts create mode 100644 app/backend/src/webhooks.controller.spec.ts create mode 100644 app/backend/src/webhooks.controller.ts create mode 100644 app/backend/src/webhooks.module.ts create mode 100644 app/backend/src/webhooks.service.spec.ts create mode 100644 app/backend/src/webhooks.service.ts diff --git a/app/backend/src/ai-verification.dto.ts b/app/backend/src/ai-verification.dto.ts new file mode 100644 index 00000000..8c895611 --- /dev/null +++ b/app/backend/src/ai-verification.dto.ts @@ -0,0 +1,30 @@ +import { + IsString, + IsNotEmpty, + IsUUID, + IsEnum, + IsObject, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export enum VerificationStatus { + VERIFIED = 'verified', + REJECTED = 'rejected', + NEEDS_REVIEW = 'needs_review', +} + +export class AiVerificationPayloadDto { + @IsUUID() + eventId: string; + + @IsString() + @IsNotEmpty() + sessionId: string; + + @IsEnum(VerificationStatus) + status: VerificationStatus; + + @IsObject() + details: Record; +} \ No newline at end of file diff --git a/app/backend/src/app.module.ts b/app/backend/src/app.module.ts index 2fee3e5b..3e116fe1 100644 --- a/app/backend/src/app.module.ts +++ b/app/backend/src/app.module.ts @@ -5,7 +5,7 @@ import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; import { ScheduleModule } from '@nestjs/schedule'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; - + import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AidModule } from './aid/aid.module'; @@ -47,6 +47,7 @@ import { SandboxModule } from './sandbox/sandbox.module'; import { CacheModule } from './common/cache/cache.module'; import { CacheResponseInterceptor } from './common/interceptors/cache-response.interceptor'; +import { WebhooksModule } from './webhooks/webhooks.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -116,6 +117,7 @@ import { CacheResponseInterceptor } from './common/interceptors/cache-response.i EntityLinkingModule, DeploymentMetadataModule, SandboxModule, + WebhooksModule, RedisModule.forRootAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ diff --git a/app/backend/src/hmac-auth.guard.ts b/app/backend/src/hmac-auth.guard.ts new file mode 100644 index 00000000..69cfe2f2 --- /dev/null +++ b/app/backend/src/hmac-auth.guard.ts @@ -0,0 +1,58 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; +import * as crypto from 'crypto'; + +@Injectable() +export class HmacAuthGuard implements CanActivate { + private readonly logger = new Logger(HmacAuthGuard.name); + private readonly secret: Buffer; + + constructor(private configService: ConfigService) { + const secretKey = this.configService.get('AI_WEBHOOK_SECRET'); + if (!secretKey) { + throw new Error('AI_WEBHOOK_SECRET is not configured.'); + } + this.secret = Buffer.from(secretKey, 'utf8'); + } + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const signature = request.header('X-Signature-256'); + + if (!signature) { + this.logger.warn('Missing X-Signature-256 header'); + throw new UnauthorizedException('Missing signature'); + } + + if (!request.rawBody) { + this.logger.error( + 'rawBody is not available on the request. Ensure rawBody middleware is used.', + ); + throw new UnauthorizedException('Invalid request configuration'); + } + + const expectedSignature = `sha256=${crypto + .createHmac('sha256', this.secret) + .update(request.rawBody) + .digest('hex')}`; + + const isValid = crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature), + ); + + if (!isValid) { + this.logger.warn('Invalid HMAC signature'); + throw new UnauthorizedException('Invalid signature'); + } + + return true; + } +} \ No newline at end of file diff --git a/app/backend/src/main.ts b/app/backend/src/main.ts index 41ad6b0a..92c84c47 100644 --- a/app/backend/src/main.ts +++ b/app/backend/src/main.ts @@ -41,6 +41,15 @@ async function bootstrap() { // Enable shutdown hooks app.enableShutdownHooks(); + // Add raw body parsing for webhook signature verification + app.use(json({ + limit: '10mb', + verify: (req: any, res, buf) => { + req.rawBody = buf; + }, + })); + app.use(urlencoded({ extended: true, limit: '10mb' })); + const configService = app.get(ConfigService); // Security middleware (order matters) diff --git a/app/backend/src/webhooks.controller.spec.ts b/app/backend/src/webhooks.controller.spec.ts new file mode 100644 index 00000000..b859400b --- /dev/null +++ b/app/backend/src/webhooks.controller.spec.ts @@ -0,0 +1,59 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { WebhooksController } from './webhooks.controller'; +import { WebhooksService } from './webhooks.service'; +import { HmacAuthGuard } from './guards/hmac-auth.guard'; +import { AiVerificationPayloadDto, VerificationStatus } from './dto/ai-verification.dto'; +import { ConfigService } from '@nestjs/config'; + +describe('WebhooksController', () => { + let controller: WebhooksController; + let service: WebhooksService; + + const mockWebhooksService = { + processAiVerification: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [WebhooksController], + providers: [ + { + provide: WebhooksService, + useValue: mockWebhooksService, + }, + // Mock ConfigService for the guard + { + provide: ConfigService, + useValue: { + get: jest.fn().mockReturnValue('test-secret'), + }, + }, + ], + }) + .overrideGuard(HmacAuthGuard) + .useValue({ canActivate: () => true }) // Mock the guard to always pass + .compile(); + + controller = module.get(WebhooksController); + service = module.get(WebhooksService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('handleAiVerification', () => { + it('should call the service with the correct payload', async () => { + const payload: AiVerificationPayloadDto = { + eventId: 'evt_123', + sessionId: 'sess_456', + status: VerificationStatus.VERIFIED, + details: { score: 0.9 }, + }; + + await controller.handleAiVerification(payload); + + expect(service.processAiVerification).toHaveBeenCalledWith(payload); + }); + }); +}); \ No newline at end of file diff --git a/app/backend/src/webhooks.controller.ts b/app/backend/src/webhooks.controller.ts new file mode 100644 index 00000000..505d823e --- /dev/null +++ b/app/backend/src/webhooks.controller.ts @@ -0,0 +1,34 @@ +import { + Controller, + Post, + Body, + UseGuards, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { WebhooksService } from './webhooks.service'; +import { HmacAuthGuard } from './guards/hmac-auth.guard'; +import { AiVerificationPayloadDto } from './dto/ai-verification.dto'; +import { ApiTags, ApiOperation, ApiResponse, ApiHeader } from '@nestjs/swagger'; + +@ApiTags('Webhooks') +@Controller('webhooks') +export class WebhooksController { + constructor(private readonly webhooksService: WebhooksService) {} + + @Post('ai-verification') + @UseGuards(HmacAuthGuard) + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Receive AI verification results' }) + @ApiHeader({ + name: 'X-Signature-256', + description: 'HMAC SHA256 signature of the request body.', + required: true, + }) + @ApiResponse({ status: 200, description: 'Webhook processed successfully.' }) + @ApiResponse({ status: 401, description: 'Invalid signature.' }) + @ApiResponse({ status: 409, description: 'Event already processed.' }) + async handleAiVerification(@Body() payload: AiVerificationPayloadDto) { + return this.webhooksService.processAiVerification(payload); + } +} \ No newline at end of file diff --git a/app/backend/src/webhooks.module.ts b/app/backend/src/webhooks.module.ts new file mode 100644 index 00000000..b48cfabb --- /dev/null +++ b/app/backend/src/webhooks.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { WebhooksController } from './webhooks.controller'; +import { WebhooksService } from './webhooks.service'; +import { SessionModule } from '../session/session.module'; +import { PrismaModule } from '../prisma/prisma.module'; + +@Module({ + imports: [SessionModule, PrismaModule], + controllers: [WebhooksController], + providers: [WebhooksService], +}) +export class WebhooksModule {} \ No newline at end of file diff --git a/app/backend/src/webhooks.service.spec.ts b/app/backend/src/webhooks.service.spec.ts new file mode 100644 index 00000000..cdf77a08 --- /dev/null +++ b/app/backend/src/webhooks.service.spec.ts @@ -0,0 +1,114 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { WebhooksService } from './webhooks.service'; +import { SessionService } from '../session/session.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { AiVerificationPayloadDto, VerificationStatus } from './dto/ai-verification.dto'; +import { ConflictException, NotFoundException } from '@nestjs/common'; +import { SessionStatus, StepStatus } from '@prisma/client'; + +describe('WebhooksService', () => { + let service: WebhooksService; + let prisma: PrismaService; + let sessionService: SessionService; + + const mockPrisma = { + webhookEvent: { + findUnique: jest.fn(), + create: jest.fn(), + }, + $transaction: jest.fn().mockImplementation(callback => callback(mockPrisma)), + }; + + const mockSessionService = { + getSession: jest.fn(), + submitToStep: jest.fn(), + }; + + const payload: AiVerificationPayloadDto = { + eventId: 'evt_123', + sessionId: 'sess_456', + status: VerificationStatus.VERIFIED, + details: { score: 0.9 }, + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + WebhooksService, + { provide: PrismaService, useValue: mockPrisma }, + { provide: SessionService, useValue: mockSessionService }, + ], + }).compile(); + + service = module.get(WebhooksService); + prisma = module.get(PrismaService); + sessionService = module.get(SessionService); + + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('processAiVerification', () => { + it('should throw ConflictException if event is already processed', async () => { + mockPrisma.webhookEvent.findUnique.mockResolvedValue({ id: '1' }); + + await expect(service.processAiVerification(payload)).rejects.toThrow( + ConflictException, + ); + }); + + it('should throw NotFoundException if session is not found or not active', async () => { + mockPrisma.webhookEvent.findUnique.mockResolvedValue(null); + mockSessionService.getSession.mockResolvedValue(null); + + await expect(service.processAiVerification(payload)).rejects.toThrow( + NotFoundException, + ); + }); + + it('should throw NotFoundException if a suitable step is not found', async () => { + mockPrisma.webhookEvent.findUnique.mockResolvedValue(null); + mockSessionService.getSession.mockResolvedValue({ + id: 'sess_456', + status: SessionStatus.pending, + steps: [{ stepName: 'other_step', status: StepStatus.pending }], + }); + + await expect(service.processAiVerification(payload)).rejects.toThrow( + NotFoundException, + ); + }); + + it('should process the webhook successfully', async () => { + const stepId = 'step_789'; + mockPrisma.webhookEvent.findUnique.mockResolvedValue(null); + mockSessionService.getSession.mockResolvedValue({ + id: 'sess_456', + status: SessionStatus.pending, + steps: [ + { + id: stepId, + stepName: 'identity_verification', + status: StepStatus.in_progress, + }, + ], + }); + + const result = await service.processAiVerification(payload); + + expect(prisma.webhookEvent.create).toHaveBeenCalled(); + expect(sessionService.submitToStep).toHaveBeenCalledWith( + payload.sessionId, + stepId, + { + submissionKey: payload.eventId, + payload: { status: payload.status, details: payload.details }, + }, + ); + expect(result).toEqual({ status: 'success', eventId: payload.eventId }); + }); + }); +}); \ No newline at end of file diff --git a/app/backend/src/webhooks.service.ts b/app/backend/src/webhooks.service.ts new file mode 100644 index 00000000..3d087d5c --- /dev/null +++ b/app/backend/src/webhooks.service.ts @@ -0,0 +1,73 @@ +import { + Injectable, + Logger, + ConflictException, + NotFoundException, +} from '@nestjs/common'; +import { SessionService } from '../session/session.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { AiVerificationPayloadDto } from './dto/ai-verification.dto'; +import { SessionStatus, StepStatus } from '@prisma/client'; + +@Injectable() +export class WebhooksService { + private readonly logger = new Logger(WebhooksService.name); + + constructor( + private readonly sessionService: SessionService, + private readonly prisma: PrismaService, + ) {} + + async processAiVerification(payload: AiVerificationPayloadDto) { + const { eventId, sessionId, status, details } = payload; + + // 1. Idempotency Check + const existingEvent = await this.prisma.webhookEvent.findUnique({ + where: { eventId }, + }); + + if (existingEvent) { + this.logger.log(`Webhook event ${eventId} already processed. Skipping.`); + throw new ConflictException('Event already processed'); + } + + // 2. Find the relevant session and step + const session = await this.sessionService.getSession(sessionId); + if (!session || session.status !== SessionStatus.pending) { + throw new NotFoundException(`Active session ${sessionId} not found.`); + } + + const verificationStep = session.steps.find( + step => + step.stepName === 'identity_verification' && + step.status === StepStatus.in_progress, + ); + + if (!verificationStep) { + throw new NotFoundException( + `Pending identity_verification step not found for session ${sessionId}.`, + ); + } + + // 3. Record the event and submit to the session step + await this.prisma.$transaction(async tx => { + await tx.webhookEvent.create({ + data: { + eventId, + payload: payload as any, + source: 'ai_service', + }, + }); + + // The submitToStep method in the provided context takes three arguments. + // We will adapt the call to match it. + await this.sessionService.submitToStep(sessionId, verificationStep.id, { + submissionKey: eventId, // Use eventId for idempotency in the session + payload: { status, details }, + }); + }); + + this.logger.log(`Successfully processed AI verification for event ${eventId}`); + return { status: 'success', eventId }; + } +} \ No newline at end of file From eec29ed6def66b21cc8d192790d79cd232b46479 Mon Sep 17 00:00:00 2001 From: whitezaddy Date: Sat, 27 Jun 2026 22:38:01 +0100 Subject: [PATCH 4/4] style(backend): resolve unused variable and import lint errors --- app/backend/src/ai-verification.dto.ts | 4 +- app/backend/src/app.module.ts | 2 +- app/backend/src/audit/ai-verification.dto.ts | 2 +- .../src/audit/audit.controller.spec.ts | 2 +- app/backend/src/audit/audit.controller.ts | 19 +- app/backend/src/audit/audit.service.ts | 41 +- app/backend/src/audit/config.ts | 2 +- app/backend/src/audit/hmac.guard.spec.ts | 4 +- app/backend/src/audit/hmac.guard.ts | 2 +- app/backend/src/audit/metrics.interceptor.ts | 8 +- app/backend/src/audit/metrics.module.ts | 2 +- app/backend/src/audit/metrics.service.ts | 2 +- .../src/audit/webhook.controller.spec.ts | 11 +- app/backend/src/audit/webhook.controller.ts | 18 +- app/backend/src/audit/webhook.module.ts | 2 +- .../cache-response.interceptor.spec.ts | 62 ++- .../cache-response.interceptor.ts | 10 +- app/backend/src/hmac-auth.guard.ts | 2 +- app/backend/src/main.ts | 14 +- app/backend/src/webhooks.controller.spec.ts | 7 +- app/backend/src/webhooks.controller.ts | 4 +- app/backend/src/webhooks.module.ts | 2 +- app/backend/src/webhooks.service.spec.ts | 11 +- app/backend/src/webhooks.service.ts | 10 +- pnpm-lock.yaml | 466 ++++++++++-------- 25 files changed, 404 insertions(+), 305 deletions(-) diff --git a/app/backend/src/ai-verification.dto.ts b/app/backend/src/ai-verification.dto.ts index 8c895611..fc151d1b 100644 --- a/app/backend/src/ai-verification.dto.ts +++ b/app/backend/src/ai-verification.dto.ts @@ -4,9 +4,7 @@ import { IsUUID, IsEnum, IsObject, - ValidateNested, } from 'class-validator'; -import { Type } from 'class-transformer'; export enum VerificationStatus { VERIFIED = 'verified', @@ -27,4 +25,4 @@ export class AiVerificationPayloadDto { @IsObject() details: Record; -} \ No newline at end of file +} diff --git a/app/backend/src/app.module.ts b/app/backend/src/app.module.ts index 3e116fe1..2aca195e 100644 --- a/app/backend/src/app.module.ts +++ b/app/backend/src/app.module.ts @@ -5,7 +5,7 @@ import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; import { ScheduleModule } from '@nestjs/schedule'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; - + import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AidModule } from './aid/aid.module'; diff --git a/app/backend/src/audit/ai-verification.dto.ts b/app/backend/src/audit/ai-verification.dto.ts index 16b25c34..014fa4e3 100644 --- a/app/backend/src/audit/ai-verification.dto.ts +++ b/app/backend/src/audit/ai-verification.dto.ts @@ -28,4 +28,4 @@ export class AiVerificationPayloadDto { @IsObject() output: Record; -} \ No newline at end of file +} diff --git a/app/backend/src/audit/audit.controller.spec.ts b/app/backend/src/audit/audit.controller.spec.ts index 280b435c..206fdaed 100644 --- a/app/backend/src/audit/audit.controller.spec.ts +++ b/app/backend/src/audit/audit.controller.spec.ts @@ -32,7 +32,7 @@ describe('AuditController', () => { exportLogs: jest.fn().mockResolvedValue(mockExportResult), buildCsv: jest.fn().mockReturnValue('id,actorHash,...\nlog-1,...'), }; - + const mockMetricsService = { getMetrics: jest.fn().mockResolvedValue('# HELP ...'), }; diff --git a/app/backend/src/audit/audit.controller.ts b/app/backend/src/audit/audit.controller.ts index 856a2272..27b431fd 100644 --- a/app/backend/src/audit/audit.controller.ts +++ b/app/backend/src/audit/audit.controller.ts @@ -1,11 +1,4 @@ -import { - Controller, - Get, - Query, - Res, - UseInterceptors, - Version, -} from '@nestjs/common'; +import { Controller, Get, Query, Res, Version } from '@nestjs/common'; import { Response } from 'express'; import { AuditService, AuditQuery, ExportAuditQuery } from './audit.service'; import { @@ -16,15 +9,16 @@ import { ApiUnauthorizedResponse, ApiBearerAuth, } from '@nestjs/swagger'; -import { MetricsInterceptor } from '../metrics/metrics.interceptor'; import { MetricsService } from '../metrics/metrics.service'; @ApiTags('Audit') @ApiBearerAuth('JWT-auth') @Controller('audit') -@UseInterceptors(MetricsInterceptor) export class AuditController { - constructor(private readonly auditService: AuditService, private metricsService: MetricsService) {} + constructor( + private readonly auditService: AuditService, + private metricsService: MetricsService, + ) {} @Get() @Version('1') @@ -146,6 +140,7 @@ export class AuditController { @ApiOkResponse({ description: 'Prometheus metrics exported successfully.' }) async getMetrics(@Res() res: Response) { res.set('Content-Type', 'text/plain'); - res.send(await this.metricsService.getMetrics()); + const metrics = await this.metricsService.getMetrics(); + res.send(metrics); } } diff --git a/app/backend/src/audit/audit.service.ts b/app/backend/src/audit/audit.service.ts index eb55995f..8ab36ec5 100644 --- a/app/backend/src/audit/audit.service.ts +++ b/app/backend/src/audit/audit.service.ts @@ -93,7 +93,10 @@ export class AuditService { end(); return result; } catch (error) { - this.metrics.dbErrorsTotal.inc({ operation: 'create', entity: 'AuditLog' }); + this.metrics.dbErrorsTotal.inc({ + operation: 'create', + entity: 'AuditLog', + }); end(); throw error; } @@ -134,12 +137,13 @@ export class AuditService { end(); return { data: rows, total, page, limit }; } catch (error) { - this.metrics.dbErrorsTotal.inc({ operation: 'findMany', entity: 'AuditLog' }); + this.metrics.dbErrorsTotal.inc({ + operation: 'findMany', + entity: 'AuditLog', + }); end(); throw error; } - - return { data: rows, total, page, limit }; } async exportLogs(query: ExportAuditQuery): Promise { @@ -180,23 +184,26 @@ export class AuditService { this.prisma.auditLog.count({ where }), ]); end(); + + const data: AnonymizedAuditLog[] = rows.map(row => ({ + id: row.id, + actorHash: this.anonymize(row.actorId), + entity: row.entity, + entityHash: this.anonymize(row.entityId), + action: row.action, + timestamp: row.timestamp, + metadata: row.metadata, + })); + + return { data, total, page, limit }; } catch (error) { - this.metrics.dbErrorsTotal.inc({ operation: 'export', entity: 'AuditLog' }); + this.metrics.dbErrorsTotal.inc({ + operation: 'export', + entity: 'AuditLog', + }); end(); throw error; } - - const data: AnonymizedAuditLog[] = rows.map(row => ({ - id: row.id, - actorHash: this.anonymize(row.actorId), - entity: row.entity, - entityHash: this.anonymize(row.entityId), - action: row.action, - timestamp: row.timestamp, - metadata: row.metadata, - })); - - return { data, total, page, limit }; } buildCsv(rows: AnonymizedAuditLog[]): string { diff --git a/app/backend/src/audit/config.ts b/app/backend/src/audit/config.ts index fe4b9ac7..72574eab 100644 --- a/app/backend/src/audit/config.ts +++ b/app/backend/src/audit/config.ts @@ -2,4 +2,4 @@ import { registerAs } from '@nestjs/config'; export default registerAs('app', () => ({ aiWebhookSecret: process.env.AI_WEBHOOK_SECRET, -})); \ No newline at end of file +})); diff --git a/app/backend/src/audit/hmac.guard.spec.ts b/app/backend/src/audit/hmac.guard.spec.ts index 0182cf54..e9b40a29 100644 --- a/app/backend/src/audit/hmac.guard.spec.ts +++ b/app/backend/src/audit/hmac.guard.spec.ts @@ -33,7 +33,7 @@ describe('HmacGuard', () => { rawBody, }), }), - } as unknown as ExecutionContext); + }) as unknown as ExecutionContext; it('should be defined', () => { expect(guard).toBeDefined(); @@ -76,4 +76,4 @@ describe('HmacGuard', () => { const result = guard.canActivate(context); expect(result).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/app/backend/src/audit/hmac.guard.ts b/app/backend/src/audit/hmac.guard.ts index ace10039..3ee115ca 100644 --- a/app/backend/src/audit/hmac.guard.ts +++ b/app/backend/src/audit/hmac.guard.ts @@ -40,4 +40,4 @@ export class HmacGuard implements CanActivate { return true; } -} \ No newline at end of file +} diff --git a/app/backend/src/audit/metrics.interceptor.ts b/app/backend/src/audit/metrics.interceptor.ts index 209d2273..4913ecb6 100644 --- a/app/backend/src/audit/metrics.interceptor.ts +++ b/app/backend/src/audit/metrics.interceptor.ts @@ -30,8 +30,12 @@ export class MetricsInterceptor implements NestInterceptor { duration, ); - this.metricsService.httpRequestsTotal.inc({ method: request.method, route, status_code: statusCode }); + this.metricsService.httpRequestsTotal.inc({ + method: request.method, + route, + status_code: statusCode, + }); }), ); } -} \ No newline at end of file +} diff --git a/app/backend/src/audit/metrics.module.ts b/app/backend/src/audit/metrics.module.ts index bcb2be2a..1d385177 100644 --- a/app/backend/src/audit/metrics.module.ts +++ b/app/backend/src/audit/metrics.module.ts @@ -6,4 +6,4 @@ import { MetricsService } from './metrics.service'; providers: [MetricsService], exports: [MetricsService], }) -export class MetricsModule {} \ No newline at end of file +export class MetricsModule {} diff --git a/app/backend/src/audit/metrics.service.ts b/app/backend/src/audit/metrics.service.ts index 88b32209..8eed86c4 100644 --- a/app/backend/src/audit/metrics.service.ts +++ b/app/backend/src/audit/metrics.service.ts @@ -59,4 +59,4 @@ export class MetricsService implements OnModuleInit { getMetrics() { return this.registry.metrics(); } -} \ No newline at end of file +} diff --git a/app/backend/src/audit/webhook.controller.spec.ts b/app/backend/src/audit/webhook.controller.spec.ts index 35f50e43..aefdb042 100644 --- a/app/backend/src/audit/webhook.controller.spec.ts +++ b/app/backend/src/audit/webhook.controller.spec.ts @@ -2,7 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { WebhookController } from './webhook.controller'; import { SessionService } from '../session/session.service'; import { HmacGuard } from './hmac.guard'; -import { AiVerificationPayloadDto, VerificationStatus } from './dto/ai-verification.dto'; +import { + AiVerificationPayloadDto, + VerificationStatus, +} from './dto/ai-verification.dto'; describe('WebhookController', () => { let controller: WebhookController; @@ -44,7 +47,9 @@ describe('WebhookController', () => { output: { verificationScore: 0.95 }, }; - mockSessionService.submitToStep.mockResolvedValue({ isIdempotent: false }); + mockSessionService.submitToStep.mockResolvedValue({ + isIdempotent: false, + }); const result = await controller.handleAiVerification(payload); @@ -61,4 +66,4 @@ describe('WebhookController', () => { expect(result).toEqual({ status: 'received', isIdempotent: false }); }); }); -}); \ No newline at end of file +}); diff --git a/app/backend/src/audit/webhook.controller.ts b/app/backend/src/audit/webhook.controller.ts index da2f4c55..c99c7444 100644 --- a/app/backend/src/audit/webhook.controller.ts +++ b/app/backend/src/audit/webhook.controller.ts @@ -6,7 +6,13 @@ import { HttpCode, HttpStatus, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiOkResponse, ApiUnauthorizedResponse, ApiHeader } from '@nestjs/swagger'; +import { + ApiTags, + ApiOperation, + ApiOkResponse, + ApiUnauthorizedResponse, + ApiHeader, +} from '@nestjs/swagger'; import { SessionService } from '../session/session.service'; import { HmacGuard } from './hmac.guard'; import { AiVerificationPayloadDto } from './dto/ai-verification.dto'; @@ -29,8 +35,12 @@ export class WebhookController { description: 'HMAC-SHA256 signature of the raw request body.', required: true, }) - @ApiOkResponse({ description: 'Webhook received and processed successfully.' }) - @ApiUnauthorizedResponse({ description: 'Invalid or missing HMAC signature.' }) + @ApiOkResponse({ + description: 'Webhook received and processed successfully.', + }) + @ApiUnauthorizedResponse({ + description: 'Invalid or missing HMAC signature.', + }) async handleAiVerification(@Body() payload: AiVerificationPayloadDto) { const result = await this.sessionService.submitToStep( payload.sessionId, @@ -41,4 +51,4 @@ export class WebhookController { return { status: 'received', isIdempotent: result.isIdempotent }; } -} \ No newline at end of file +} diff --git a/app/backend/src/audit/webhook.module.ts b/app/backend/src/audit/webhook.module.ts index 712e4534..34ab8736 100644 --- a/app/backend/src/audit/webhook.module.ts +++ b/app/backend/src/audit/webhook.module.ts @@ -6,4 +6,4 @@ import { WebhookController } from './webhook.controller'; imports: [SessionModule], controllers: [WebhookController], }) -export class WebhookModule {} \ No newline at end of file +export class WebhookModule {} diff --git a/app/backend/src/common/interceptors/cache-response.interceptor.spec.ts b/app/backend/src/common/interceptors/cache-response.interceptor.spec.ts index 1ec2d310..89bb494c 100644 --- a/app/backend/src/common/interceptors/cache-response.interceptor.spec.ts +++ b/app/backend/src/common/interceptors/cache-response.interceptor.spec.ts @@ -64,19 +64,19 @@ describe('CacheResponseInterceptor', () => { }; }); - it('should skip caching when no metadata is present', (done) => { + it('should skip caching when no metadata is present', done => { jest.spyOn(reflector, 'get').mockReturnValue(undefined); interceptor .intercept(mockExecutionContext, mockCallHandler) - .subscribe((result) => { + .subscribe(result => { expect(result).toEqual({ data: 'test' }); expect(redisService.get).not.toHaveBeenCalled(); done(); }); }); - it('should return cached response on cache hit', (done) => { + it('should return cached response on cache hit', done => { const cacheOptions = { ttl: 300 }; const cachedData = { data: 'cached' }; @@ -85,7 +85,7 @@ describe('CacheResponseInterceptor', () => { interceptor .intercept(mockExecutionContext, mockCallHandler) - .subscribe((result) => { + .subscribe(result => { expect(result).toEqual(cachedData); expect(redisService.get).toHaveBeenCalled(); expect(mockCallHandler.handle).not.toHaveBeenCalled(); @@ -93,7 +93,7 @@ describe('CacheResponseInterceptor', () => { }); }); - it('should execute handler and cache result on cache miss', (done) => { + it('should execute handler and cache result on cache miss', done => { const cacheOptions = { ttl: 300 }; const handlerData = { data: 'fresh' }; @@ -104,11 +104,11 @@ describe('CacheResponseInterceptor', () => { interceptor .intercept(mockExecutionContext, mockCallHandler) - .subscribe((result) => { + .subscribe(result => { expect(result).toEqual(handlerData); expect(redisService.get).toHaveBeenCalled(); expect(mockCallHandler.handle).toHaveBeenCalled(); - + // Set is called asynchronously, give it a moment setTimeout(() => { expect(redisService.set).toHaveBeenCalledWith( @@ -121,7 +121,7 @@ describe('CacheResponseInterceptor', () => { }); }); - it('should use custom key generator when provided', (done) => { + it('should use custom key generator when provided', done => { const customKey = 'custom:key:123'; const cacheOptions = { ttl: 300, @@ -132,21 +132,19 @@ describe('CacheResponseInterceptor', () => { jest.spyOn(redisService, 'get').mockResolvedValue(null); jest.spyOn(redisService, 'set').mockResolvedValue(undefined); - interceptor - .intercept(mockExecutionContext, mockCallHandler) - .subscribe({ - next: () => { - expect(cacheOptions.keyGenerator).toHaveBeenCalled(); - expect(redisService.get).toHaveBeenCalledWith( - expect.stringContaining(customKey), - ); - done(); - }, - error: (err) => done(err), - }); + interceptor.intercept(mockExecutionContext, mockCallHandler).subscribe({ + next: () => { + expect(cacheOptions.keyGenerator).toHaveBeenCalled(); + expect(redisService.get).toHaveBeenCalledWith( + expect.stringContaining(customKey), + ); + done(); + }, + error: err => done(err), + }); }); - it('should include query params in cache key', (done) => { + it('should include query params in cache key', done => { const cacheOptions = { ttl: 300 }; mockExecutionContext.switchToHttp = jest.fn().mockReturnValue({ getRequest: jest.fn().mockReturnValue({ @@ -162,18 +160,16 @@ describe('CacheResponseInterceptor', () => { jest.spyOn(redisService, 'get').mockResolvedValue(null); jest.spyOn(redisService, 'set').mockResolvedValue(undefined); - interceptor - .intercept(mockExecutionContext, mockCallHandler) - .subscribe({ - next: () => { - expect(redisService.get).toHaveBeenCalled(); - // Key should be different due to query params - const cacheKey = (redisService.get as jest.Mock).mock.calls[0][0]; - expect(cacheKey).toBeTruthy(); - done(); - }, - error: (err) => done(err), - }); + interceptor.intercept(mockExecutionContext, mockCallHandler).subscribe({ + next: () => { + expect(redisService.get).toHaveBeenCalled(); + // Key should be different due to query params + const cacheKey = (redisService.get as jest.Mock).mock.calls[0][0]; + expect(cacheKey).toBeTruthy(); + done(); + }, + error: err => done(err), + }); }); }); }); diff --git a/app/backend/src/common/interceptors/cache-response.interceptor.ts b/app/backend/src/common/interceptors/cache-response.interceptor.ts index 430706b8..23e68e36 100644 --- a/app/backend/src/common/interceptors/cache-response.interceptor.ts +++ b/app/backend/src/common/interceptors/cache-response.interceptor.ts @@ -41,7 +41,7 @@ export class CacheResponseInterceptor implements NestInterceptor { // Try to retrieve from cache return from(this.redisService.get(cacheKey)).pipe( - switchMap((cachedResponse) => { + switchMap(cachedResponse => { if (cachedResponse !== null) { this.logger.debug(`Cache HIT: ${cacheKey}`); return of(cachedResponse); @@ -51,7 +51,7 @@ export class CacheResponseInterceptor implements NestInterceptor { // Cache miss: execute handler and cache the result return next.handle().pipe( - tap((response) => { + tap(response => { // Fire-and-forget cache set (don't await in tap) void this.redisService .set(cacheKey, response, options.ttl) @@ -60,7 +60,7 @@ export class CacheResponseInterceptor implements NestInterceptor { `Cached response for key: ${cacheKey} (TTL: ${options.ttl}s)`, ); }) - .catch((err) => { + .catch(err => { this.logger.warn( `Failed to cache response for key ${cacheKey}: ${String(err)}`, ); @@ -119,13 +119,13 @@ export class CacheResponseInterceptor implements NestInterceptor { } if (Array.isArray(obj)) { - return obj.map((item) => this.sortObject(item)); + return obj.map(item => this.sortObject(item)); } const sorted: any = {}; Object.keys(obj) .sort() - .forEach((key) => { + .forEach(key => { sorted[key] = this.sortObject(obj[key]); }); diff --git a/app/backend/src/hmac-auth.guard.ts b/app/backend/src/hmac-auth.guard.ts index 69cfe2f2..2c13849f 100644 --- a/app/backend/src/hmac-auth.guard.ts +++ b/app/backend/src/hmac-auth.guard.ts @@ -55,4 +55,4 @@ export class HmacAuthGuard implements CanActivate { return true; } -} \ No newline at end of file +} diff --git a/app/backend/src/main.ts b/app/backend/src/main.ts index 92c84c47..d6335211 100644 --- a/app/backend/src/main.ts +++ b/app/backend/src/main.ts @@ -42,12 +42,14 @@ async function bootstrap() { app.enableShutdownHooks(); // Add raw body parsing for webhook signature verification - app.use(json({ - limit: '10mb', - verify: (req: any, res, buf) => { - req.rawBody = buf; - }, - })); + app.use( + json({ + limit: '10mb', + verify: (req: any, res, buf) => { + req.rawBody = buf; + }, + }), + ); app.use(urlencoded({ extended: true, limit: '10mb' })); const configService = app.get(ConfigService); diff --git a/app/backend/src/webhooks.controller.spec.ts b/app/backend/src/webhooks.controller.spec.ts index b859400b..fc19fefb 100644 --- a/app/backend/src/webhooks.controller.spec.ts +++ b/app/backend/src/webhooks.controller.spec.ts @@ -2,7 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { WebhooksController } from './webhooks.controller'; import { WebhooksService } from './webhooks.service'; import { HmacAuthGuard } from './guards/hmac-auth.guard'; -import { AiVerificationPayloadDto, VerificationStatus } from './dto/ai-verification.dto'; +import { + AiVerificationPayloadDto, + VerificationStatus, +} from './dto/ai-verification.dto'; import { ConfigService } from '@nestjs/config'; describe('WebhooksController', () => { @@ -56,4 +59,4 @@ describe('WebhooksController', () => { expect(service.processAiVerification).toHaveBeenCalledWith(payload); }); }); -}); \ No newline at end of file +}); diff --git a/app/backend/src/webhooks.controller.ts b/app/backend/src/webhooks.controller.ts index 505d823e..a498b821 100644 --- a/app/backend/src/webhooks.controller.ts +++ b/app/backend/src/webhooks.controller.ts @@ -17,7 +17,7 @@ export class WebhooksController { constructor(private readonly webhooksService: WebhooksService) {} @Post('ai-verification') - @UseGuards(HmacAuthGuard) + @UseGuards(HmacAuthGuard) // Correctly typed guard @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Receive AI verification results' }) @ApiHeader({ @@ -31,4 +31,4 @@ export class WebhooksController { async handleAiVerification(@Body() payload: AiVerificationPayloadDto) { return this.webhooksService.processAiVerification(payload); } -} \ No newline at end of file +} diff --git a/app/backend/src/webhooks.module.ts b/app/backend/src/webhooks.module.ts index b48cfabb..5a6edd80 100644 --- a/app/backend/src/webhooks.module.ts +++ b/app/backend/src/webhooks.module.ts @@ -9,4 +9,4 @@ import { PrismaModule } from '../prisma/prisma.module'; controllers: [WebhooksController], providers: [WebhooksService], }) -export class WebhooksModule {} \ No newline at end of file +export class WebhooksModule {} diff --git a/app/backend/src/webhooks.service.spec.ts b/app/backend/src/webhooks.service.spec.ts index cdf77a08..9034db31 100644 --- a/app/backend/src/webhooks.service.spec.ts +++ b/app/backend/src/webhooks.service.spec.ts @@ -2,7 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { WebhooksService } from './webhooks.service'; import { SessionService } from '../session/session.service'; import { PrismaService } from '../prisma/prisma.service'; -import { AiVerificationPayloadDto, VerificationStatus } from './dto/ai-verification.dto'; +import { + AiVerificationPayloadDto, + VerificationStatus, +} from './dto/ai-verification.dto'; import { ConflictException, NotFoundException } from '@nestjs/common'; import { SessionStatus, StepStatus } from '@prisma/client'; @@ -16,7 +19,9 @@ describe('WebhooksService', () => { findUnique: jest.fn(), create: jest.fn(), }, - $transaction: jest.fn().mockImplementation(callback => callback(mockPrisma)), + $transaction: jest + .fn() + .mockImplementation(callback => callback(mockPrisma)), }; const mockSessionService = { @@ -111,4 +116,4 @@ describe('WebhooksService', () => { expect(result).toEqual({ status: 'success', eventId: payload.eventId }); }); }); -}); \ No newline at end of file +}); diff --git a/app/backend/src/webhooks.service.ts b/app/backend/src/webhooks.service.ts index 3d087d5c..1da28c95 100644 --- a/app/backend/src/webhooks.service.ts +++ b/app/backend/src/webhooks.service.ts @@ -7,7 +7,7 @@ import { import { SessionService } from '../session/session.service'; import { PrismaService } from '../prisma/prisma.service'; import { AiVerificationPayloadDto } from './dto/ai-verification.dto'; -import { SessionStatus, StepStatus } from '@prisma/client'; +import { Prisma, SessionStatus, StepStatus } from '@prisma/client'; @Injectable() export class WebhooksService { @@ -54,7 +54,7 @@ export class WebhooksService { await tx.webhookEvent.create({ data: { eventId, - payload: payload as any, + payload: payload as unknown as Prisma.InputJsonValue, source: 'ai_service', }, }); @@ -67,7 +67,9 @@ export class WebhooksService { }); }); - this.logger.log(`Successfully processed AI verification for event ${eventId}`); + this.logger.log( + `Successfully processed AI verification for event ${eventId}`, + ); return { status: 'success', eventId }; } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 681d4327..5d67e34d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,9 @@ importers: openai: specifier: ^6.33.0 version: 6.34.0(ws@8.19.0)(zod@4.3.6) + pg: + specifier: ^8.22.0 + version: 8.22.0 pino: specifier: ^10.3.0 version: 10.3.1 @@ -153,6 +156,9 @@ importers: prom-client: specifier: ^15.1.3 version: 15.1.3 + redis: + specifier: ^6.0.1 + version: 6.0.1(@opentelemetry/api@1.9.0) reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -179,8 +185,8 @@ importers: specifier: ^5.0.0 version: 5.0.6 '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 + specifier: ^29.5.12 + version: 29.5.14 '@types/multer': specifier: ^2.1.0 version: 2.1.0 @@ -212,11 +218,11 @@ importers: specifier: ^8.13.1 version: 8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.10.1))(ioredis@5.10.1) jest: - specifier: ^30.0.0 - version: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) jest-mock-extended: specifier: ^4.0.0 - version: 4.0.0(@jest/globals@30.4.1)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3) + version: 4.0.0(@jest/globals@30.4.1)(jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3) prettier: specifier: ^3.4.2 version: 3.8.1 @@ -230,8 +236,8 @@ importers: specifier: ^7.2.2 version: 7.2.2 ts-jest: - specifier: ^29.2.5 - version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3) + specifier: ^29.4.11 + version: 29.4.11(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.2 version: 9.5.4(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.30(@swc/helpers@0.5.15))) @@ -349,7 +355,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: ^16.2.1 - version: 16.2.4(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + version: 16.2.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) jest: specifier: ^30.3.0 version: 30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)) @@ -3004,6 +3010,12 @@ packages: peerDependencies: '@redis/client': ^5.12.1 + '@redis/bloom@6.0.1': + resolution: {integrity: sha512-6ys5hhea+47n7o97ZFI4GvdzTQk/arIsXZgH159l6IVtJ4rZaB+KVdAfwvIxlmGA7z+NNlO8UxjTeQrenqjZcQ==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.0.1 + '@redis/client@5.12.1': resolution: {integrity: sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==} engines: {node: '>= 18.19.0'} @@ -3016,24 +3028,54 @@ packages: '@opentelemetry/api': optional: true + '@redis/client@6.0.1': + resolution: {integrity: sha512-SwYl64hKHE/NeO2VSSG1y/4zIm0cNepyOZtQrOpLiNRHmH2FdWBOecNzsLiXCQdFCF9MCyoPXwAbaG2iMO0A7Q==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@node-rs/xxhash': ^1.1.0 + '@opentelemetry/api': '>=1 <2' + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true + '@opentelemetry/api': + optional: true + '@redis/json@5.12.1': resolution: {integrity: sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==} engines: {node: '>= 18.19.0'} peerDependencies: '@redis/client': ^5.12.1 + '@redis/json@6.0.1': + resolution: {integrity: sha512-KD1OztCYh7O9TkKMU9qZcFIKoudIGqmgXsOhQVq5A3REGrnl+wg0kporQFQCO+fcxe/nhvDgmBtXrm3diPGczA==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.0.1 + '@redis/search@5.12.1': resolution: {integrity: sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==} engines: {node: '>= 18.19.0'} peerDependencies: '@redis/client': ^5.12.1 + '@redis/search@6.0.1': + resolution: {integrity: sha512-G09OujS3eOtQnP7kZC5eZTiazwgeimlo6Pf3vHnE1jO7rfqrtmMI0R1/ZXfzoW8p9vB4QiH538aEsWaHKd8l5w==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.0.1 + '@redis/time-series@5.12.1': resolution: {integrity: sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==} engines: {node: '>= 18.19.0'} peerDependencies: '@redis/client': ^5.12.1 + '@redis/time-series@6.0.1': + resolution: {integrity: sha512-8aLGSDtCpnPTLD7lEiHHmuDCFppctLdT8geFIDf/7LWV9y8Vre6RB+aBZrgkeo3X1oPmTt1IbVAQVxsuJvkODw==} + engines: {node: '>= 20.0.0'} + peerDependencies: + '@redis/client': ^6.0.1 + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -3082,6 +3124,7 @@ packages: '@stellar/stellar-base@14.1.0': resolution: {integrity: sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==} engines: {node: '>=20.0.0'} + deprecated: This package is now rolled into @stellar/stellar-sdk. Please use @stellar/stellar-sdk to continue receiving updates and support. '@stellar/stellar-sdk@14.6.1': resolution: {integrity: sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==} @@ -7402,6 +7445,9 @@ packages: pg-connection-string@2.13.0: resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==} + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -7414,6 +7460,9 @@ packages: pg-protocol@1.14.0: resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==} + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} @@ -7427,6 +7476,15 @@ packages: pg-native: optional: true + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} @@ -7862,6 +7920,10 @@ packages: resolution: {integrity: sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==} engines: {node: '>= 18.19.0'} + redis@6.0.1: + resolution: {integrity: sha512-54FoTBdFw10Y602pShvk8CGJlSH55nY+CNAZaVk8YdxY3rENihdYm2lXrujrtupYTHyrVSZUxOdeStNQbNvnQg==} + engines: {node: '>= 20.0.0'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -9347,15 +9409,15 @@ snapshots: '@babel/core@7.29.0': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helpers': 7.29.2 '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -9368,7 +9430,7 @@ snapshots: '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -9431,7 +9493,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.28.5': dependencies: '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -9453,7 +9515,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@babel/helper-plugin-utils@7.28.6': {} @@ -9478,7 +9540,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -9494,16 +9556,16 @@ snapshots: '@babel/helper-wrap-function@7.28.6': dependencies: - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helpers@7.29.2': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 '@babel/highlight@7.25.9': dependencies: @@ -9514,7 +9576,7 @@ snapshots: '@babel/parser@7.29.2': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@babel/parser@7.29.7': dependencies: @@ -9949,12 +10011,12 @@ snapshots: '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.29.2 '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -10154,7 +10216,7 @@ snapshots: resolve: 1.22.11 resolve-from: 5.0.0 resolve.exports: 2.0.3 - semver: 7.7.4 + semver: 7.8.1 send: 0.19.2 slugify: 1.6.8 source-map-support: 0.5.21 @@ -10188,7 +10250,7 @@ snapshots: getenv: 2.0.0 glob: 13.0.6 resolve-from: 5.0.0 - semver: 7.7.4 + semver: 7.8.1 slash: 3.0.0 slugify: 1.6.8 xcode: 3.0.1 @@ -10210,7 +10272,7 @@ snapshots: require-from-string: 2.0.2 resolve-from: 5.0.0 resolve-workspace-root: 2.0.1 - semver: 7.7.4 + semver: 7.8.1 slugify: 1.6.8 sucrase: 3.35.1 transitivePeerDependencies: @@ -10262,7 +10324,7 @@ snapshots: minimatch: 9.0.9 p-limit: 3.1.0 resolve-from: 5.0.0 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - supports-color @@ -10274,7 +10336,7 @@ snapshots: jimp-compact: 0.16.1 parse-png: 2.1.0 resolve-from: 5.0.0 - semver: 7.7.4 + semver: 7.8.1 '@expo/image-utils@0.8.13(typescript@5.9.3)': dependencies: @@ -10284,7 +10346,7 @@ snapshots: getenv: 2.0.0 jimp-compact: 0.16.1 parse-png: 2.1.0 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - supports-color - typescript @@ -10375,7 +10437,7 @@ snapshots: debug: 4.4.3 expo: 54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) resolve-from: 5.0.0 - semver: 7.7.4 + semver: 7.8.1 xml2js: 0.6.0 transitivePeerDependencies: - supports-color @@ -10733,7 +10795,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -10742,7 +10804,7 @@ snapshots: '@jest/console@30.3.0': dependencies: '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 jest-message-util: 30.3.0 jest-util: 30.3.0 @@ -10764,14 +10826,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 25.9.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@25.9.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -10827,41 +10889,6 @@ snapshots: - supports-color - ts-node - '@jest/core@30.3.0(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3))': - dependencies: - '@jest/console': 30.3.0 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 - '@types/node': 22.19.15 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 4.4.0 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-changed-files: 30.3.0 - jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) - jest-haste-map: 30.3.0 - jest-message-util: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-resolve-dependencies: 30.3.0 - jest-runner: 30.3.0 - jest-runtime: 30.3.0 - jest-snapshot: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 - jest-watcher: 30.3.0 - pretty-format: 30.3.0 - slash: 3.0.0 - transitivePeerDependencies: - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - '@jest/core@30.4.2(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@25.9.1)(typescript@6.0.3))': dependencies: '@jest/console': 30.4.1 @@ -10921,14 +10948,14 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-mock: 29.7.0 '@jest/environment@30.3.0': dependencies: '@jest/fake-timers': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-mock: 30.3.0 '@jest/environment@30.4.1': @@ -10975,7 +11002,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -10984,7 +11011,7 @@ snapshots: dependencies: '@jest/types': 30.3.0 '@sinonjs/fake-timers': 15.1.1 - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-message-util: 30.3.0 jest-mock: 30.3.0 jest-util: 30.3.0 @@ -11029,12 +11056,12 @@ snapshots: '@jest/pattern@30.0.1': dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-regex-util: 30.0.1 '@jest/pattern@30.4.0': dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-regex-util: 30.4.0 '@jest/reporters@29.7.0': @@ -11045,7 +11072,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -11074,7 +11101,7 @@ snapshots: '@jest/transform': 30.3.0 '@jest/types': 30.3.0 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit-x: 0.2.2 @@ -11265,7 +11292,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.19.15 + '@types/node': 25.9.1 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -12245,7 +12272,7 @@ snapshots: metro: 0.83.5 metro-config: 0.83.5 metro-core: 0.83.5 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - bufferutil - supports-color @@ -12351,24 +12378,46 @@ snapshots: dependencies: '@redis/client': 5.12.1(@opentelemetry/api@1.9.0) + '@redis/bloom@6.0.1(@redis/client@6.0.1(@opentelemetry/api@1.9.0))': + dependencies: + '@redis/client': 6.0.1(@opentelemetry/api@1.9.0) + '@redis/client@5.12.1(@opentelemetry/api@1.9.0)': dependencies: cluster-key-slot: 1.1.2 optionalDependencies: '@opentelemetry/api': 1.9.0 + '@redis/client@6.0.1(@opentelemetry/api@1.9.0)': + dependencies: + cluster-key-slot: 1.1.2 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@redis/json@5.12.1(@redis/client@5.12.1(@opentelemetry/api@1.9.0))': dependencies: '@redis/client': 5.12.1(@opentelemetry/api@1.9.0) + '@redis/json@6.0.1(@redis/client@6.0.1(@opentelemetry/api@1.9.0))': + dependencies: + '@redis/client': 6.0.1(@opentelemetry/api@1.9.0) + '@redis/search@5.12.1(@redis/client@5.12.1(@opentelemetry/api@1.9.0))': dependencies: '@redis/client': 5.12.1(@opentelemetry/api@1.9.0) + '@redis/search@6.0.1(@redis/client@6.0.1(@opentelemetry/api@1.9.0))': + dependencies: + '@redis/client': 6.0.1(@opentelemetry/api@1.9.0) + '@redis/time-series@5.12.1(@redis/client@5.12.1(@opentelemetry/api@1.9.0))': dependencies: '@redis/client': 5.12.1(@opentelemetry/api@1.9.0) + '@redis/time-series@6.0.1(@redis/client@6.0.1(@opentelemetry/api@1.9.0))': + dependencies: + '@redis/client': 6.0.1(@opentelemetry/api@1.9.0) + '@rtsao/scc@1.1.0': {} '@scarf/scarf@1.4.0': {} @@ -12651,24 +12700,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/body-parser@1.19.6': dependencies: @@ -12712,7 +12761,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 '@types/http-errors@2.0.5': {} @@ -12742,13 +12791,13 @@ snapshots: '@types/jsdom@20.0.1': dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 '@types/jsdom@21.1.7': dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -12915,7 +12964,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.57.1 debug: 4.4.3 minimatch: 10.2.4 - semver: 7.7.4 + semver: 7.8.1 tinyglobby: 0.2.15 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -14030,7 +14079,7 @@ snapshots: chrome-launcher@0.15.2: dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -14041,7 +14090,7 @@ snapshots: chromium-edge-launcher@0.2.0: dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -14624,13 +14673,13 @@ snapshots: - supports-color - typescript - eslint-config-next@16.2.4(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + eslint-config-next@16.2.4(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 16.2.4 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.6.1)) @@ -14667,7 +14716,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -14720,6 +14769,33 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.4(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -15744,7 +15820,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.1 is-callable@1.2.7: {} @@ -15866,7 +15942,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -15879,7 +15955,7 @@ snapshots: '@babel/parser': 7.29.2 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - supports-color @@ -15951,7 +16027,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 @@ -15977,7 +16053,7 @@ snapshots: '@jest/expect': 30.3.0 '@jest/test-result': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 @@ -16061,25 +16137,6 @@ snapshots: - supports-color - ts-node - jest-cli@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)): - dependencies: - '@jest/core': 30.3.0(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 - chalk: 4.1.2 - exit-x: 0.2.2 - import-local: 3.2.0 - jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) - jest-util: 30.3.0 - jest-validate: 30.3.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jest-cli@30.4.2(@types/node@25.9.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@25.9.1)(typescript@6.0.3)): dependencies: '@jest/core': 30.4.2(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@25.9.1)(typescript@6.0.3)) @@ -16130,39 +16187,38 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@25.9.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.3.0 - '@jest/types': 30.3.0 - babel-jest: 30.3.0(@babel/core@7.29.0) + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) chalk: 4.1.2 - ci-info: 4.4.0 + ci-info: 3.9.0 deepmerge: 4.3.1 - glob: 10.5.0 + glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 30.3.0 - jest-docblock: 30.2.0 - jest-environment-node: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-runner: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 parse-json: 5.2.0 - pretty-format: 30.3.0 + pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.19.37 - ts-node: 10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3) + '@types/node': 25.9.1 + ts-node: 10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)): + jest-config@30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -16188,13 +16244,13 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.19.15 + '@types/node': 20.19.37 ts-node: 10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)): + jest-config@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -16221,7 +16277,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.19.15 - ts-node: 10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3) + ts-node: 10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -16354,7 +16410,7 @@ snapshots: '@jest/environment': 30.3.0 '@jest/fake-timers': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-mock: 30.3.0 jest-util: 30.3.0 jest-validate: 30.3.0 @@ -16402,7 +16458,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.19.15 + '@types/node': 25.9.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -16417,7 +16473,7 @@ snapshots: jest-haste-map@30.3.0: dependencies: '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -16482,7 +16538,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -16517,23 +16573,23 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 - jest-mock-extended@4.0.0(@jest/globals@30.4.1)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): + jest-mock-extended@4.0.0(@jest/globals@30.4.1)(jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): dependencies: '@jest/globals': 30.4.1 - jest: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) + jest: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) ts-essentials: 10.1.1(typescript@5.9.3) typescript: 5.9.3 jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-util: 29.7.0 jest-mock@30.3.0: dependencies: '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-util: 30.3.0 jest-mock@30.4.1: @@ -16622,7 +16678,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -16648,7 +16704,7 @@ snapshots: '@jest/test-result': 30.3.0 '@jest/transform': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 @@ -16704,7 +16760,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 @@ -16731,7 +16787,7 @@ snapshots: '@jest/test-result': 30.3.0 '@jest/transform': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 cjs-module-lexer: 2.2.0 collect-v8-coverage: 1.0.3 @@ -16779,10 +16835,10 @@ snapshots: jest-snapshot@29.7.0: dependencies: '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 + '@babel/generator': 7.29.7 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 @@ -16797,7 +16853,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - supports-color @@ -16822,7 +16878,7 @@ snapshots: jest-message-util: 30.3.0 jest-util: 30.3.0 pretty-format: 30.3.0 - semver: 7.7.4 + semver: 7.8.1 synckit: 0.11.12 transitivePeerDependencies: - supports-color @@ -16848,7 +16904,7 @@ snapshots: jest-message-util: 30.4.1 jest-util: 30.4.1 pretty-format: 30.4.1 - semver: 7.7.4 + semver: 7.8.1 synckit: 0.11.12 transitivePeerDependencies: - supports-color @@ -16856,7 +16912,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -16865,7 +16921,7 @@ snapshots: jest-util@30.3.0: dependencies: '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 @@ -16874,7 +16930,7 @@ snapshots: jest-util@30.4.1: dependencies: '@jest/types': 30.4.1 - '@types/node': 22.19.15 + '@types/node': 25.9.1 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 @@ -16928,7 +16984,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 25.9.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -16939,7 +16995,7 @@ snapshots: dependencies: '@jest/test-result': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 22.19.15 + '@types/node': 25.9.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -16959,20 +17015,20 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@30.3.0: dependencies: - '@types/node': 22.19.15 + '@types/node': 25.9.1 '@ungap/structured-clone': 1.3.0 jest-util: 30.3.0 merge-stream: 2.0.0 @@ -17011,19 +17067,6 @@ snapshots: - supports-color - ts-node - jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)): - dependencies: - '@jest/core': 30.3.0(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) - '@jest/types': 30.3.0 - import-local: 3.2.0 - jest-cli: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jest@30.4.2(@types/node@25.9.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@25.9.1)(typescript@6.0.3)): dependencies: '@jest/core': 30.4.2(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@25.9.1)(typescript@6.0.3)) @@ -17309,7 +17352,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.1 make-error@1.3.6: {} @@ -17552,7 +17595,7 @@ snapshots: metro-transform-plugins@0.83.5: dependencies: '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 + '@babel/generator': 7.29.7 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 flow-enums-runtime: 0.0.6 @@ -17583,9 +17626,9 @@ snapshots: metro-transform-worker@0.83.5: dependencies: '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 + '@babel/generator': 7.29.7 '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 flow-enums-runtime: 0.0.6 metro: 0.83.5 metro-babel-transformer: 0.83.5 @@ -17906,7 +17949,7 @@ snapshots: dependencies: hosted-git-info: 7.0.2 proc-log: 4.2.0 - semver: 7.7.4 + semver: 7.8.1 validate-npm-package-name: 5.0.1 npm-run-path@4.0.1: @@ -18148,14 +18191,22 @@ snapshots: pg-connection-string@2.13.0: {} + pg-connection-string@2.14.0: {} + pg-int8@1.0.1: {} pg-pool@3.14.0(pg@8.21.0): dependencies: pg: 8.21.0 + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + pg-protocol@1.14.0: {} + pg-protocol@1.15.0: {} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -18174,6 +18225,16 @@ snapshots: optionalDependencies: pg-cloudflare: 1.4.0 + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + pgpass@1.0.5: dependencies: split2: 4.2.0 @@ -18679,6 +18740,17 @@ snapshots: - '@node-rs/xxhash' - '@opentelemetry/api' + redis@6.0.1(@opentelemetry/api@1.9.0): + dependencies: + '@redis/bloom': 6.0.1(@redis/client@6.0.1(@opentelemetry/api@1.9.0)) + '@redis/client': 6.0.1(@opentelemetry/api@1.9.0) + '@redis/json': 6.0.1(@redis/client@6.0.1(@opentelemetry/api@1.9.0)) + '@redis/search': 6.0.1(@redis/client@6.0.1(@opentelemetry/api@1.9.0)) + '@redis/time-series': 6.0.1(@redis/client@6.0.1(@opentelemetry/api@1.9.0)) + transitivePeerDependencies: + - '@node-rs/xxhash' + - '@opentelemetry/api' + reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: @@ -18969,7 +19041,7 @@ snapshots: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.7.4 + semver: 7.8.1 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -19455,18 +19527,18 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.11(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@30.4.2(@types/node@25.9.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@25.9.1)(typescript@6.0.3)))(typescript@6.0.3): + ts-jest@29.4.11(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.9 - jest: 30.4.2(@types/node@25.9.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@25.9.1)(typescript@6.0.3)) + jest: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.8.1 type-fest: 4.41.0 - typescript: 6.0.3 + typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.0 @@ -19475,18 +19547,18 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 30.4.1 - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.11(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@30.4.2(@types/node@25.9.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@25.9.1)(typescript@6.0.3)))(typescript@6.0.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) + handlebars: 4.7.9 + jest: 30.4.2(@types/node@25.9.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@25.9.1)(typescript@6.0.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.4 + semver: 7.8.1 type-fest: 4.41.0 - typescript: 5.9.3 + typescript: 6.0.3 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.0 @@ -19495,12 +19567,12 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 30.4.1 - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)) + jest: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -19515,12 +19587,12 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 30.4.1 - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.4.1)(jest@30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)(typescript@5.9.3)) + jest: 30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6