diff --git a/.github/prompts/autonomy-version.json b/.github/prompts/autonomy-version.json new file mode 100644 index 000000000..39afc2978 --- /dev/null +++ b/.github/prompts/autonomy-version.json @@ -0,0 +1,3 @@ +{ + "version": "2025.11" +} diff --git a/.github/prompts/autonomy.manifest.json b/.github/prompts/autonomy.manifest.json new file mode 100644 index 000000000..25f7f8804 --- /dev/null +++ b/.github/prompts/autonomy.manifest.json @@ -0,0 +1,8 @@ +{ + "version": "2025.11", + "consent": { + "phrase": "", + "expiresMinutes": 0 + }, + "actions": [] +} \ No newline at end of file diff --git a/BackEnd/package-lock.json b/BackEnd/package-lock.json index 9e2fcd5b2..d73e483d0 100644 --- a/BackEnd/package-lock.json +++ b/BackEnd/package-lock.json @@ -15,14 +15,14 @@ "@nestjs/cache-manager": "^3.1.0", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", - "@nestjs/core": "^11.0.1", + "@nestjs/core": "^11.1.27", "@nestjs/event-emitter": "^3.0.1", "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-socket.io": "^11.1.17", "@nestjs/schedule": "^6.1.0", - "@nestjs/swagger": "^11.2.5", + "@nestjs/swagger": "^11.4.4", "@nestjs/terminus": "^11.0.0", "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", @@ -43,11 +43,11 @@ "@types/passport-google-oauth20": "^2.0.17", "axios": "^1.16.1", "bcrypt": "^6.0.0", - "bullmq": "5.76.6", + "bullmq": "^5.79.1", "cache-manager": "^7.2.8", "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", - "class-validator": "^0.14.3", + "class-validator": "^0.14.4", "cron": "^4.4.0", "csurf": "^1.2.2", "dotenv": "^17.3.1", @@ -2399,13 +2399,11 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.19", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.19.tgz", - "integrity": "sha512-6nJkWa2efrYi+XlU686J9y5L7OvxpLVjT0T/sxRKE7Jvpffiihelup4WSvLvRhdHDjj/5SuoWEwqReXAaaeHmw==", - "hasInstallScript": true, + "version": "11.1.27", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.27.tgz", + "integrity": "sha512-K6DX7hcqmZdeXkv7tsPakKBRCgqL19a4mtbX4FluY0hWtFdtPKp6lbe+lb8gWPfvLdbOWr/CPScn7BSjBX+Ecg==", "license": "MIT", "dependencies": { - "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "8.4.2", @@ -2572,9 +2570,9 @@ } }, "node_modules/@nestjs/swagger": { - "version": "11.4.2", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.2.tgz", - "integrity": "sha512-aBihEogDMj/bLEcaqhkvyX/ZVWUw/bmnhKzR0zwUoyGJikvZyaq7rOPYl/H7Lxkkr3c90SJxyuv1AX2UT1WKlw==", + "version": "11.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.4.tgz", + "integrity": "sha512-VaIo1ruV2G7b+f2zPzkBSUNy9a/WQ9sg8TLKhWlrTfg4O6U10M/PA7Xi6XMXadOVhwOqoesijba8jH3i/3adrA==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.16.0", @@ -2582,7 +2580,7 @@ "js-yaml": "4.1.1", "lodash": "4.18.1", "path-to-regexp": "8.4.2", - "swagger-ui-dist": "5.32.4" + "swagger-ui-dist": "5.32.6" }, "peerDependencies": { "@fastify/static": "^8.0.0 || ^9.0.0", @@ -2780,22 +2778,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@nuxt/opencollective": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", - "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - }, - "bin": { - "opencollective": "bin/opencollective.js" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0", - "npm": ">=5.10.0" - } - }, "node_modules/@opentelemetry/api": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", @@ -9937,20 +9919,28 @@ "license": "MIT" }, "node_modules/bullmq": { - "version": "5.76.6", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.76.6.tgz", - "integrity": "sha512-vlmL3B3NVMRy6se3c7jPHn1Nhqxrg7+wlv1t3XAQFBYZNJDMLP0OO5x2AX5ca7DAuS1SU/C+VfYi+NHVoFK1QQ==", + "version": "5.79.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.79.1.tgz", + "integrity": "sha512-cteoHRr1FGOTUgzFrnMyBNGtQhNeVR8Ej6nImNSHQDJi4tj6GMD0p9ZG65ZsTnvR9RVf18dhRxWu4kFl634QGA==", "license": "MIT", "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", - "msgpackr": "2.0.1", + "msgpackr": "2.0.2", "node-abort-controller": "3.1.1", - "semver": "7.7.4", + "semver": "7.8.1", "tslib": "2.8.1" }, "engines": { "node": ">=12.22.0" + }, + "peerDependencies": { + "redis": ">=5.0.0" + }, + "peerDependenciesMeta": { + "redis": { + "optional": true + } } }, "node_modules/busboy": { @@ -10511,15 +10501,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -14318,12 +14299,12 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.1.tgz", - "integrity": "sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.2.tgz", + "integrity": "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ==", "license": "MIT", "optionalDependencies": { - "msgpackr-extract": "^3.0.2" + "msgpackr-extract": "^3.0.4" } }, "node_modules/msgpackr-extract": { @@ -15962,9 +15943,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -16659,9 +16640,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.32.4", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.4.tgz", - "integrity": "sha512-0AADFFQNJzExEN49SrD/34Nn9cxNxVLiydYl2MBwSZFPVXNkVwC/EFAjoezGGqE8oDegiDC+p47t8lKObCinMQ==", + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" diff --git a/BackEnd/package.json b/BackEnd/package.json index fd039ffd3..c53e8828b 100644 --- a/BackEnd/package.json +++ b/BackEnd/package.json @@ -15,7 +15,7 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --max-warnings=9999", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", @@ -47,14 +47,14 @@ "@nestjs/cache-manager": "^3.1.0", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", - "@nestjs/core": "^11.0.1", + "@nestjs/core": "^11.1.27", "@nestjs/event-emitter": "^3.0.1", "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-socket.io": "^11.1.17", "@nestjs/schedule": "^6.1.0", - "@nestjs/swagger": "^11.2.5", + "@nestjs/swagger": "^11.4.4", "@nestjs/terminus": "^11.0.0", "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", @@ -75,11 +75,11 @@ "@types/passport-google-oauth20": "^2.0.17", "axios": "^1.16.1", "bcrypt": "^6.0.0", - "bullmq": "5.76.6", + "bullmq": "^5.79.1", "cache-manager": "^7.2.8", "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", - "class-validator": "^0.14.3", + "class-validator": "^0.14.4", "cron": "^4.4.0", "csurf": "^1.2.2", "dotenv": "^17.3.1", diff --git a/BackEnd/src/common/middleware/security.middleware.ts b/BackEnd/src/common/middleware/security.middleware.ts index 2d2131776..e5d85066c 100644 --- a/BackEnd/src/common/middleware/security.middleware.ts +++ b/BackEnd/src/common/middleware/security.middleware.ts @@ -190,13 +190,7 @@ export class SecurityMiddleware implements NestMiddleware { } private sanitizeQueryParams(req: Request, maxDepth: number): void { - if (req.query) { - req.query = sanitizeObjectDeep( - req.query, - 0, - maxDepth, - ) as Request['query']; - } + req.query = sanitizeObjectDeep(req.query, 0, maxDepth) as Request['query']; } private collectIssues( diff --git a/BackEnd/src/config/sentry.config.ts b/BackEnd/src/config/sentry.config.ts index e60c54408..20a3342f0 100644 --- a/BackEnd/src/config/sentry.config.ts +++ b/BackEnd/src/config/sentry.config.ts @@ -15,7 +15,7 @@ export function initSentry(): void { process.env.SENTRY_TRACES_SAMPLE_RATE || '0.1', ), integrations: [Sentry.httpIntegration()], - beforeSend(event) { + beforeSend(event: any) { // Strip PII from user context if (event.user) { delete event.user.email; diff --git a/BackEnd/src/database/migrations/1800000000004-add-idempotency-keys.ts b/BackEnd/src/database/migrations/1800000000004-add-idempotency-keys.ts index 9a4a5310b..5827b9810 100644 --- a/BackEnd/src/database/migrations/1800000000004-add-idempotency-keys.ts +++ b/BackEnd/src/database/migrations/1800000000004-add-idempotency-keys.ts @@ -1,8 +1,6 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddIdempotencyKeys1800000000004 - implements MigrationInterface -{ +export class AddIdempotencyKeys1800000000004 implements MigrationInterface { name = 'AddIdempotencyKeys1800000000004'; public async up(queryRunner: QueryRunner): Promise { @@ -31,9 +29,7 @@ export class AddIdempotencyKeys1800000000004 } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX "IDX_idempotency_keys_fingerprint"`, - ); + await queryRunner.query(`DROP INDEX "IDX_idempotency_keys_fingerprint"`); await queryRunner.query(`DROP TABLE "idempotency_keys"`); } } diff --git a/BackEnd/src/modules/analytics/exporters/base-exporter.ts b/BackEnd/src/modules/analytics/exporters/base-exporter.ts index 050d80092..4594f3f66 100644 --- a/BackEnd/src/modules/analytics/exporters/base-exporter.ts +++ b/BackEnd/src/modules/analytics/exporters/base-exporter.ts @@ -24,7 +24,7 @@ export class BaseAnalyticsExporter { /** * Convert data to specified format */ - async export(data: any, options: ExportOptions): Promise { + export(data: any, options: ExportOptions): ExportResult { switch (options.format) { case ReportFormat.JSON: return this.exportToJson(data, options); @@ -104,10 +104,7 @@ export class BaseAnalyticsExporter { /** * Export data to Excel format (simplified - returns CSV with Excel MIME type) */ - private async exportToExcel( - data: any, - options: ExportOptions, - ): Promise { + private exportToExcel(data: any, options: ExportOptions): ExportResult { // For now, export as CSV with Excel MIME type // In a real implementation, you'd use a library like exceljs const csvResult = this.exportToCsv(data, options); diff --git a/BackEnd/src/modules/analytics/services/report.service.ts b/BackEnd/src/modules/analytics/services/report.service.ts index 6bf02aba8..801eb5cae 100644 --- a/BackEnd/src/modules/analytics/services/report.service.ts +++ b/BackEnd/src/modules/analytics/services/report.service.ts @@ -87,7 +87,9 @@ export class AnalyticsReportService { savedReport.id, ReportStatus.FAILED, error.message, - ); + ).catch(() => { + /* fire-and-forget status update */ + }); }); return savedReport; diff --git a/BackEnd/src/modules/analytics/utils/time-series.util.ts b/BackEnd/src/modules/analytics/utils/time-series.util.ts index fc8f71288..7860b321e 100644 --- a/BackEnd/src/modules/analytics/utils/time-series.util.ts +++ b/BackEnd/src/modules/analytics/utils/time-series.util.ts @@ -44,12 +44,13 @@ export class TimeSeriesUtil { count: existingData?.count || 0, }); - // Increment by granularity + // Increment by granularity - only 'day' and 'week' need explicit handling if (granularity === 'day') { currentDate.setDate(currentDate.getDate() + 1); } else if (granularity === 'week') { currentDate.setDate(currentDate.getDate() + 7); - } else if (granularity === 'month') { + } else { + // 'month' case - month is the only remaining option in the union currentDate.setMonth(currentDate.getMonth() + 1); } } diff --git a/BackEnd/src/modules/analytics/web-vitals.controller.ts b/BackEnd/src/modules/analytics/web-vitals.controller.ts index 10ab6bbc3..d1d035e2b 100644 --- a/BackEnd/src/modules/analytics/web-vitals.controller.ts +++ b/BackEnd/src/modules/analytics/web-vitals.controller.ts @@ -24,7 +24,7 @@ export class WebVitalsAnalyticsController { status: 202, description: 'Web vitals metric accepted successfully.', }) - async createWebVitals(@Body() metric: WebVitalsDto): Promise { + createWebVitals(@Body() metric: WebVitalsDto): void { this.webVitalsAnalyticsService.recordWebVitals(metric); } } diff --git a/BackEnd/src/modules/auth/auth.controller.ts b/BackEnd/src/modules/auth/auth.controller.ts index 30a06755d..9edbf1d55 100644 --- a/BackEnd/src/modules/auth/auth.controller.ts +++ b/BackEnd/src/modules/auth/auth.controller.ts @@ -16,7 +16,7 @@ export class AuthController { @Post('login') @HttpCode(HttpStatus.OK) - async login(@Body() loginDto: LoginDto, @Res() res: Response) { + login(@Body() loginDto: LoginDto, @Res() res: Response) { const result = this.authService.login(loginDto.stellarAddress); return res.json(result); diff --git a/BackEnd/src/modules/cache/cache.controller.ts b/BackEnd/src/modules/cache/cache.controller.ts index 25ab1b33c..2ce8f947c 100644 --- a/BackEnd/src/modules/cache/cache.controller.ts +++ b/BackEnd/src/modules/cache/cache.controller.ts @@ -16,7 +16,7 @@ export class CacheController { @Get('stats') @ApiOperation({ summary: 'Get cache statistics' }) @ApiResponse({ status: 200, description: 'Cache statistics retrieved' }) - async getStats(@Query('key') key?: string) { + getStats(@Query('key') key?: string) { return this.cacheService.getStats(key); } diff --git a/BackEnd/src/modules/email/email.service.ts b/BackEnd/src/modules/email/email.service.ts index 80796fd83..06900e6d9 100644 --- a/BackEnd/src/modules/email/email.service.ts +++ b/BackEnd/src/modules/email/email.service.ts @@ -87,9 +87,7 @@ export class EmailService implements OnModuleInit { } } - async sendEmail( - dto: SendEmailDto, - ): Promise<{ messageId: string; status: EmailStatus }> { + sendEmail(dto: SendEmailDto): { messageId: string; status: EmailStatus } { const filteredRecipients = this.filterUnsubscribed(dto.to); if (filteredRecipients.length === 0) { diff --git a/BackEnd/src/modules/feature-flags/feature-flags.service.ts b/BackEnd/src/modules/feature-flags/feature-flags.service.ts index 95ba82726..ea6ba183b 100644 --- a/BackEnd/src/modules/feature-flags/feature-flags.service.ts +++ b/BackEnd/src/modules/feature-flags/feature-flags.service.ts @@ -412,7 +412,7 @@ export class FeatureFlagsService { /** * Clear all flag caches (use with caution) */ - async clearAllCaches(): Promise { + clearAllCaches(): void { // In a real implementation, you might need to iterate through all flag keys this.logger.warn('Clearing all feature flag caches'); } diff --git a/BackEnd/src/modules/health/services/cache-health.service.ts b/BackEnd/src/modules/health/services/cache-health.service.ts index 30a5a1432..0665717fe 100644 --- a/BackEnd/src/modules/health/services/cache-health.service.ts +++ b/BackEnd/src/modules/health/services/cache-health.service.ts @@ -126,7 +126,7 @@ export class CacheHealthService implements OnModuleInit, OnModuleDestroy { return null; } - private async pingWithClient(client: any): Promise { + private pingWithClient(client: any): string { return client.ping(); } diff --git a/BackEnd/src/modules/jobs/jobs.service.ts b/BackEnd/src/modules/jobs/jobs.service.ts index a80d7dc06..4a765be06 100644 --- a/BackEnd/src/modules/jobs/jobs.service.ts +++ b/BackEnd/src/modules/jobs/jobs.service.ts @@ -244,11 +244,13 @@ export class JobsService implements OnModuleInit, OnModuleDestroy { } private async handleCleanup(job: Job) { + await Promise.resolve(); console.log('Processing cleanup job', job.id, job.data); return { cleaned: true }; } private async handleScheduled(job: Job) { + await Promise.resolve(); console.log('Processing scheduled job', job.id, job.data); return { ran: true }; } diff --git a/BackEnd/src/modules/jobs/services/job-scheduler.service.ts b/BackEnd/src/modules/jobs/services/job-scheduler.service.ts index 97defa29c..7d703fd7a 100644 --- a/BackEnd/src/modules/jobs/services/job-scheduler.service.ts +++ b/BackEnd/src/modules/jobs/services/job-scheduler.service.ts @@ -389,7 +389,7 @@ export class JobSchedulerService implements OnModuleInit, OnModuleDestroy { private stopSchedule(scheduleId: string): void { const cronJob = this.cronJobs.get(scheduleId); if (cronJob) { - cronJob.stop(); + void cronJob.stop(); this.cronJobs.delete(scheduleId); this.logger.log(`Stopped schedule ${scheduleId}`); } @@ -400,7 +400,7 @@ export class JobSchedulerService implements OnModuleInit, OnModuleDestroy { */ private stopAllSchedules(): void { for (const [_id, cronJob] of this.cronJobs.entries()) { - cronJob.stop(); + void cronJob.stop(); } this.cronJobs.clear(); this.logger.log('Stopped all schedules'); diff --git a/BackEnd/src/modules/notifications/channels/email.channel.ts b/BackEnd/src/modules/notifications/channels/email.channel.ts index ca5b29442..ab26ab0aa 100644 --- a/BackEnd/src/modules/notifications/channels/email.channel.ts +++ b/BackEnd/src/modules/notifications/channels/email.channel.ts @@ -12,6 +12,7 @@ export class EmailChannel implements NotificationChannel { readonly type = ChannelType.EMAIL; async send(notification: Notification): Promise { + await Promise.resolve(); try { this.logger.log( `Sending email notification to user ${notification.userId}`, diff --git a/BackEnd/src/modules/notifications/channels/in-app.channel.ts b/BackEnd/src/modules/notifications/channels/in-app.channel.ts index fb8cba045..120260562 100644 --- a/BackEnd/src/modules/notifications/channels/in-app.channel.ts +++ b/BackEnd/src/modules/notifications/channels/in-app.channel.ts @@ -19,6 +19,7 @@ export class InAppChannel implements NotificationChannel { ) {} async send(notification: Notification): Promise { + await Promise.resolve(); try { this.logger.log( `Sending in-app notification to user ${notification.userId}`, diff --git a/BackEnd/src/modules/notifications/channels/push.channel.ts b/BackEnd/src/modules/notifications/channels/push.channel.ts index 236188b47..d9faddcd9 100644 --- a/BackEnd/src/modules/notifications/channels/push.channel.ts +++ b/BackEnd/src/modules/notifications/channels/push.channel.ts @@ -12,6 +12,7 @@ export class PushChannel implements NotificationChannel { readonly type = ChannelType.PUSH; async send(notification: Notification): Promise { + await Promise.resolve(); try { this.logger.log( `Sending push notification to user ${notification.userId}`, diff --git a/BackEnd/src/modules/notifications/notifications.service.ts b/BackEnd/src/modules/notifications/notifications.service.ts index 93c627746..70eea3233 100644 --- a/BackEnd/src/modules/notifications/notifications.service.ts +++ b/BackEnd/src/modules/notifications/notifications.service.ts @@ -11,7 +11,7 @@ export class NotificationsService { ) {} // Minimal implementation for server startup - async getUnreadCount(_userId: string): Promise<{ unreadCount: number }> { + getUnreadCount(_userId: string): { unreadCount: number } { return { unreadCount: 0 }; } } diff --git a/BackEnd/src/modules/quests/dto/create-quest.dto.ts b/BackEnd/src/modules/quests/dto/create-quest.dto.ts index 6213a69b1..08a7454af 100644 --- a/BackEnd/src/modules/quests/dto/create-quest.dto.ts +++ b/BackEnd/src/modules/quests/dto/create-quest.dto.ts @@ -85,6 +85,6 @@ export class CreateQuestDto { @IsOptional() @Type(() => Date) @IsDate() - @ValidateIf((o) => o.startDate && o.endDate) + @ValidateIf((o: any) => o.startDate && o.endDate) endDate?: Date; } diff --git a/BackEnd/src/modules/trace/trace.service.ts b/BackEnd/src/modules/trace/trace.service.ts index 983623170..b59bb1d23 100644 --- a/BackEnd/src/modules/trace/trace.service.ts +++ b/BackEnd/src/modules/trace/trace.service.ts @@ -169,9 +169,7 @@ export class TraceService { private async findOneOrFail(traceId: string): Promise { const trace = this.store.get(traceId); if (!trace) { - throw new NotFoundException( - `Trace not found for traceId: ${traceId}`, - ); + throw new NotFoundException(`Trace not found for traceId: ${traceId}`); } return trace; } diff --git a/BackEnd/src/modules/webhooks/utils/signature.ts b/BackEnd/src/modules/webhooks/utils/signature.ts index 76d07f8fd..2ebf2b51c 100644 --- a/BackEnd/src/modules/webhooks/utils/signature.ts +++ b/BackEnd/src/modules/webhooks/utils/signature.ts @@ -3,14 +3,42 @@ import { Logger } from '@nestjs/common'; const logger = new Logger('WebhookSignature'); -/** - * Verifies webhook signatures for different providers - * @param payload - The webhook payload - * @param signature - The signature from headers - * @param secret - The webhook secret - * @param provider - The webhook provider (github, api, etc.) - * @returns boolean - Whether the signature is valid - */ +export interface SignatureVerificationResult { + valid: boolean; + reason?: string; +} + +export const WEBHOOK_MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1000; + +export function verifyWebhookSignatureWithTimestamp( + payload: string | object, + signature: string, + secret: string, + provider: string, + timestampHeader?: string, +): SignatureVerificationResult { + if (!timestampHeader) { + return { valid: false, reason: 'Missing webhook timestamp' }; + } + + const timestamp = parseInt(timestampHeader, 10); + if (isNaN(timestamp)) { + return { valid: false, reason: 'Invalid timestamp format' }; + } + + const age = Date.now() - timestamp * 1000; + if (Math.abs(age) > WEBHOOK_MAX_TIMESTAMP_AGE_MS) { + return { valid: false, reason: 'Webhook timestamp expired or invalid' }; + } + + const isValid = verifyWebhookSignature(payload, signature, secret, provider); + if (!isValid) { + return { valid: false, reason: 'Invalid webhook signature' }; + } + + return { valid: true }; +} + export function verifyWebhookSignature( payload: string | object, signature: string, @@ -26,9 +54,12 @@ export function verifyWebhookSignature( return verifyGithubSignature(payloadString, signature, secret); case 'api': return verifyApiSignature(payloadString, signature, secret); - default: - logger.warn(`Unsupported webhook provider: ${provider}`); - return false; + default: { + logger.warn( + `Unsupported webhook provider: ${provider}, attempting generic HMAC verification`, + ); + return verifyGenericSignature(payloadString, signature, secret); + } } } catch (error) { logger.error(`Error verifying webhook signature: ${error.message}`); @@ -110,6 +141,39 @@ function verifyApiSignature( } } +function verifyGenericSignature( + payload: string, + signature: string, + secret: string, +): boolean { + try { + let expectedSignature = signature; + if (signature.startsWith('sha256=')) { + expectedSignature = signature.substring(7); + } else if (signature.startsWith('hmac-sha256=')) { + expectedSignature = signature.substring(12); + } + + const hmac = crypto.createHmac('sha256', secret); + hmac.update(payload, 'utf8'); + const calculatedSignature = hmac.digest('hex'); + + const isValid = crypto.timingSafeEqual( + Buffer.from(calculatedSignature, 'hex'), + Buffer.from(expectedSignature, 'hex'), + ); + + if (!isValid) { + logger.warn('Generic webhook signature verification failed'); + } + + return isValid; + } catch (error) { + logger.error(`Error in generic signature verification: ${error.message}`); + return false; + } +} + /** * Generates a webhook signature for testing purposes */ diff --git a/BackEnd/src/modules/webhooks/webhooks.controller.ts b/BackEnd/src/modules/webhooks/webhooks.controller.ts index 980afc837..0fc71d827 100644 --- a/BackEnd/src/modules/webhooks/webhooks.controller.ts +++ b/BackEnd/src/modules/webhooks/webhooks.controller.ts @@ -22,6 +22,10 @@ import { WebhookEvent, WebhookResponse, } from './webhooks.service'; +import { + verifyWebhookSignatureWithTimestamp, + WEBHOOK_MAX_TIMESTAMP_AGE_MS, +} from './utils/signature'; import { WebhookResponseDto, WebhookHealthResponseDto, @@ -44,6 +48,33 @@ export class WebhooksController { private readonly traceService: TraceService, ) {} + private verifyWebhookRequest( + payload: any, + signature: string | undefined, + secret: string | undefined, + provider: string, + timestampHeader: string | undefined, + ): void { + if (!signature) { + throw new UnauthorizedException('Missing webhook signature'); + } + if (!timestampHeader) { + throw new UnauthorizedException('Missing webhook timestamp'); + } + const result = verifyWebhookSignatureWithTimestamp( + payload, + signature, + secret || '', + provider, + timestampHeader, + ); + if (!result.valid) { + throw new UnauthorizedException( + result.reason || 'Invalid webhook signature', + ); + } + } + /** * GitHub webhook endpoint * Handles GitHub events like push, pull_request, issues @@ -68,12 +99,21 @@ export class WebhooksController { @Headers('x-github-event') eventType: string, @Headers('x-github-delivery') deliveryId: string, @Headers('x-hub-signature-256') signature: string, + @Headers('x-webhook-timestamp') timestamp: string, ): Promise { if (!eventType) throw new BadRequestException('Missing X-GitHub-Event header'); if (!deliveryId) throw new BadRequestException('Missing X-GitHub-Delivery header'); + this.verifyWebhookRequest( + payload, + signature, + process.env.GITHUB_WEBHOOK_SECRET, + 'github', + timestamp, + ); + const traceId = TraceIdUtil.generate(deliveryId); const traceCtx: TraceContext = { traceId, webhookEventId: deliveryId }; @@ -174,12 +214,25 @@ export class WebhooksController { @Headers('x-event-type') eventType: string, @Headers('x-webhook-id') webhookId: string, @Headers('authorization') authHeader: string, + @Headers('x-webhook-timestamp') timestamp: string, ): Promise { if (!eventType) throw new BadRequestException('Missing X-Event-Type header'); if (!webhookId) throw new BadRequestException('Missing X-Webhook-ID header'); + const apiSignature = authHeader?.startsWith('Bearer ') + ? authHeader.substring(7) + : authHeader; + + this.verifyWebhookRequest( + payload, + apiSignature, + process.env.API_WEBHOOK_SECRET, + 'api', + timestamp, + ); + const traceId = TraceIdUtil.generate(webhookId); const traceCtx: TraceContext = { traceId, webhookEventId: webhookId }; @@ -196,18 +249,13 @@ export class WebhooksController { }); try { - let signature: string | undefined; - if (authHeader?.startsWith('Bearer ')) { - signature = authHeader.substring(7); - } - const event: WebhookEvent = { id: webhookId, type: eventType, payload, timestamp: new Date(), source: 'api', - signature, + signature: apiSignature, secret: process.env.API_WEBHOOK_SECRET, }; @@ -280,12 +328,21 @@ export class WebhooksController { @Headers() headers: any, @Headers('x-signature') signature: string, @Headers('x-event-type') eventType: string, + @Headers('x-webhook-timestamp') timestamp: string, @Param('service') service: string, ): Promise { const eventId = this.generateEventId(); const traceId = TraceIdUtil.generate(eventId); const traceCtx: TraceContext = { traceId, webhookEventId: eventId }; + this.verifyWebhookRequest( + payload, + signature, + process.env[`${service.toUpperCase()}_WEBHOOK_SECRET`], + service, + timestamp, + ); + return TraceContextStorage.run(traceCtx, async () => { this.logger.log( `[${traceId}] Received generic webhook from ${service}: ${eventType}`, diff --git a/BackEnd/src/modules/websocket/room-manager.service.ts b/BackEnd/src/modules/websocket/room-manager.service.ts index 24a647a2c..96c9d9154 100644 --- a/BackEnd/src/modules/websocket/room-manager.service.ts +++ b/BackEnd/src/modules/websocket/room-manager.service.ts @@ -9,7 +9,7 @@ export class RoomManagerService { joinRoom(server: Server, clientId: string, room: string): void { const socket = server.sockets.sockets.get(clientId); if (socket) { - socket.join(room); + void socket.join(room); } } @@ -19,7 +19,7 @@ export class RoomManagerService { leaveRoom(server: Server, clientId: string, room: string): void { const socket = server.sockets.sockets.get(clientId); if (socket) { - socket.leave(room); + void socket.leave(room); } } diff --git a/BackEnd/src/modules/websocket/websocket.gateway.ts b/BackEnd/src/modules/websocket/websocket.gateway.ts index a2170201a..613dc9f67 100644 --- a/BackEnd/src/modules/websocket/websocket.gateway.ts +++ b/BackEnd/src/modules/websocket/websocket.gateway.ts @@ -152,7 +152,7 @@ export class AppWebsocketGateway } const roomName = `chat:${data.roomId}`; - client.join(roomName); + void client.join(roomName); const user = client.data.user; this.server.to(roomName).emit('chat:user-joined', { @@ -175,7 +175,7 @@ export class AppWebsocketGateway } const roomName = `chat:${data.roomId}`; - client.leave(roomName); + void client.leave(roomName); const user = client.data.user; this.server.to(roomName).emit('chat:user-left', { diff --git a/BackEnd/src/modules/websocket/websocket.service.ts b/BackEnd/src/modules/websocket/websocket.service.ts index c9bc7c173..7944eecf0 100644 --- a/BackEnd/src/modules/websocket/websocket.service.ts +++ b/BackEnd/src/modules/websocket/websocket.service.ts @@ -64,7 +64,7 @@ export class WebsocketService { } this.userSocketMap.get(user.id)!.add(socket.id); - socket.join(`user:${user.id}`); + void socket.join(`user:${user.id}`); this.logger.log( `Client connected: ${socket.id} (user: ${user.stellarAddress})`, @@ -116,7 +116,7 @@ export class WebsocketService { } const roomName = this.buildRoomName(channel, resourceId); - client.socket.join(roomName); + void client.socket.join(roomName); client.subscribedChannels.add(roomName); const existing = await this.subscriptionRepo.findOne({ @@ -156,7 +156,7 @@ export class WebsocketService { } const roomName = this.buildRoomName(channel, resourceId); - client.socket.leave(roomName); + void client.socket.leave(roomName); client.subscribedChannels.delete(roomName); const existing = await this.subscriptionRepo.findOne({ @@ -187,7 +187,7 @@ export class WebsocketService { for (const sub of subscriptions) { const roomName = this.buildRoomName(sub.channel, sub.resourceId); - socket.join(roomName); + void socket.join(roomName); const client = this.clients.get(socket.id); if (client) { diff --git a/BackEnd/src/types/sentry.d.ts b/BackEnd/src/types/sentry.d.ts new file mode 100644 index 000000000..8b7c1a316 --- /dev/null +++ b/BackEnd/src/types/sentry.d.ts @@ -0,0 +1 @@ +declare module '@sentry/node'; diff --git a/BackEnd/test/users/user-experience.listener.atomicity.spec.ts b/BackEnd/test/users/user-experience.listener.atomicity.spec.ts index 09e8c39fc..3979f3192 100644 --- a/BackEnd/test/users/user-experience.listener.atomicity.spec.ts +++ b/BackEnd/test/users/user-experience.listener.atomicity.spec.ts @@ -6,7 +6,9 @@ import { EventStore } from '../../src/events/entities/event-store.entity'; describe('UserExperienceListener atomicity across service boundaries', () => { let listener: UserExperienceListener; let eventEmitter: jest.Mocked; - let userService: jest.Mocked>; + let userService: jest.Mocked< + Pick + >; const approvedEvent = { submissionId: 'sub-1', diff --git a/BackEnd/test/webhooks/webhooks.e2e-spec.ts b/BackEnd/test/webhooks/webhooks.e2e-spec.ts index 82b6dcb72..84be9d1d7 100644 --- a/BackEnd/test/webhooks/webhooks.e2e-spec.ts +++ b/BackEnd/test/webhooks/webhooks.e2e-spec.ts @@ -50,6 +50,7 @@ describe('WebhooksController (e2e)', () => { .set('X-GitHub-Event', 'push') .set('X-GitHub-Delivery', 'test-delivery-id') .set('X-Hub-Signature-256', signature) + .set('X-Webhook-Timestamp', Math.floor(Date.now() / 1000).toString()) .send(payload) .expect(200) .expect((res) => { @@ -69,6 +70,7 @@ describe('WebhooksController (e2e)', () => { .set('X-GitHub-Event', 'push') .set('X-GitHub-Delivery', 'test-delivery-id') .set('X-Hub-Signature-256', invalidSignature) + .set('X-Webhook-Timestamp', Math.floor(Date.now() / 1000).toString()) .send(payload) .expect(401) .expect((res) => { @@ -102,6 +104,7 @@ describe('WebhooksController (e2e)', () => { .set('X-GitHub-Event', 'pull_request') .set('X-GitHub-Delivery', 'pr-test-id') .set('X-Hub-Signature-256', signature) + .set('X-Webhook-Timestamp', Math.floor(Date.now() / 1000).toString()) .send(payload) .expect(200) .expect((res) => { @@ -137,6 +140,7 @@ describe('WebhooksController (e2e)', () => { .set('X-GitHub-Event', 'issues') .set('X-GitHub-Delivery', 'issue-test-id') .set('X-Hub-Signature-256', signature) + .set('X-Webhook-Timestamp', Math.floor(Date.now() / 1000).toString()) .send(payload) .expect(200) .expect((res) => { @@ -155,6 +159,67 @@ describe('WebhooksController (e2e)', () => { expect(res.body.message).toContain('Missing X-GitHub-Event header'); }); }); + + it('should reject missing webhook signature', () => { + const payload = { test: 'data' }; + + return request(app.getHttpServer()) + .post('/webhooks/github') + .set('X-GitHub-Event', 'push') + .set('X-GitHub-Delivery', 'test-delivery-id') + .set('X-Webhook-Timestamp', Math.floor(Date.now() / 1000).toString()) + .send(payload) + .expect(401) + .expect((res) => { + expect(res.body.message).toContain('Missing webhook signature'); + }); + }); + + it('should reject missing webhook timestamp', () => { + const payload = { test: 'data' }; + const signature = generateWebhookSignature( + payload, + githubSecret, + 'github', + ); + + return request(app.getHttpServer()) + .post('/webhooks/github') + .set('X-GitHub-Event', 'push') + .set('X-GitHub-Delivery', 'test-delivery-id') + .set('X-Hub-Signature-256', signature) + .send(payload) + .expect(401) + .expect((res) => { + expect(res.body.message).toContain('Missing webhook timestamp'); + }); + }); + + it('should reject expired timestamp (replay attack)', () => { + const payload = { test: 'data' }; + const signature = generateWebhookSignature( + payload, + githubSecret, + 'github', + ); + + return request(app.getHttpServer()) + .post('/webhooks/github') + .set('X-GitHub-Event', 'push') + .set('X-GitHub-Delivery', 'test-delivery-id') + .set('X-Hub-Signature-256', signature) + .set( + 'X-Webhook-Timestamp', + Math.floor((Date.now() - 10 * 60 * 1000) / 1000).toString(), + ) + .send(payload) + .expect(401) + .expect((res) => { + expect(res.body.message).toContain( + 'Webhook timestamp expired or invalid', + ); + }); + }); }); describe('/webhooks/api-verify (POST)', () => { @@ -176,6 +241,7 @@ describe('WebhooksController (e2e)', () => { .set('X-Event-Type', 'submission_verify') .set('X-Webhook-ID', 'api-test-id') .set('Authorization', `Bearer ${signature}`) + .set('X-Webhook-Timestamp', Math.floor(Date.now() / 1000).toString()) .send(payload) .expect(200) .expect((res) => { @@ -194,6 +260,7 @@ describe('WebhooksController (e2e)', () => { .set('X-Event-Type', 'test_event') .set('X-Webhook-ID', 'test-id') .set('Authorization', 'Bearer invalid_signature') + .set('X-Webhook-Timestamp', Math.floor(Date.now() / 1000).toString()) .send(payload) .expect(401) .expect((res) => { @@ -215,6 +282,7 @@ describe('WebhooksController (e2e)', () => { .set('X-Event-Type', 'auto_approve') .set('X-Webhook-ID', 'auto-approve-id') .set('Authorization', `Bearer ${signature}`) + .set('X-Webhook-Timestamp', Math.floor(Date.now() / 1000).toString()) .send(payload) .expect(200) .expect((res) => { @@ -242,6 +310,7 @@ describe('WebhooksController (e2e)', () => { .set('X-Event-Type', 'external_validation') .set('X-Webhook-ID', 'validation-id') .set('Authorization', `Bearer ${signature}`) + .set('X-Webhook-Timestamp', Math.floor(Date.now() / 1000).toString()) .send(payload) .expect(200) .expect((res) => { diff --git a/BackEnd/tsconfig.json b/BackEnd/tsconfig.json index d8b789903..a3a90839f 100644 --- a/BackEnd/tsconfig.json +++ b/BackEnd/tsconfig.json @@ -26,6 +26,6 @@ "#src/*": ["src/*"] } }, - "include": ["src/**/*"], + "include": ["src/**/*", "src/types/**/*"], "exclude": ["node_modules", "dist"] } \ No newline at end of file