From 9d740267af9fa1cf947eb5b6a42ec5ff953ad4ee Mon Sep 17 00:00:00 2001 From: Delightech28 Date: Tue, 28 Apr 2026 03:58:19 +0100 Subject: [PATCH 1/2] feat: add resumable evidence uploads with chunking and session management --- app/backend/prisma/schema.prisma | 35 +++++ app/backend/src/app.module.ts | 2 + .../uploads/dto/create-upload-session.dto.ts | 20 +++ .../src/uploads/dto/finalize-upload.dto.ts | 7 + app/backend/src/uploads/uploads.controller.ts | 47 +++++++ app/backend/src/uploads/uploads.module.ts | 12 ++ app/backend/src/uploads/uploads.service.ts | 123 ++++++++++++++++++ pnpm-lock.yaml | 72 ++++++---- 8 files changed, 292 insertions(+), 26 deletions(-) create mode 100644 app/backend/src/uploads/dto/create-upload-session.dto.ts create mode 100644 app/backend/src/uploads/dto/finalize-upload.dto.ts create mode 100644 app/backend/src/uploads/uploads.controller.ts create mode 100644 app/backend/src/uploads/uploads.module.ts create mode 100644 app/backend/src/uploads/uploads.service.ts diff --git a/app/backend/prisma/schema.prisma b/app/backend/prisma/schema.prisma index 5b78a881..4a15f5ea 100644 --- a/app/backend/prisma/schema.prisma +++ b/app/backend/prisma/schema.prisma @@ -151,3 +151,38 @@ model ApiKey { @@index([ngoId]) } + +enum UploadSessionStatus { + pending + completed + failed + expired +} + +model UploadSession { + id String @id @default(cuid()) + ownerId String + filename String + contentType String + totalSize Int + status UploadSessionStatus @default(pending) + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + chunks UploadChunk[] + + @@index([status, expiresAt]) + @@index([ownerId]) +} + +model UploadChunk { + id String @id @default(cuid()) + uploadSessionId String + uploadSession UploadSession @relation(fields: [uploadSessionId], references: [id]) + chunkIndex Int + size Int + createdAt DateTime @default(now()) + + @@unique([uploadSessionId, chunkIndex]) +} diff --git a/app/backend/src/app.module.ts b/app/backend/src/app.module.ts index 2a4edde3..a1359527 100644 --- a/app/backend/src/app.module.ts +++ b/app/backend/src/app.module.ts @@ -33,6 +33,7 @@ import { AllExceptionsFilter } from './common/filters/http-exception.filter'; import { AnalyticsModule } from './analytics/analytics.module'; import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { AidEscrowModule } from './onchain/aid-escrow.module'; +import { UploadsModule } from './uploads/uploads.module'; @Module({ imports: [ @@ -76,6 +77,7 @@ import { AidEscrowModule } from './onchain/aid-escrow.module'; JobsModule, AnalyticsModule, AidEscrowModule, + UploadsModule, ThrottlerModule.forRoot([ { ttl: 60000, // 60 seconds window diff --git a/app/backend/src/uploads/dto/create-upload-session.dto.ts b/app/backend/src/uploads/dto/create-upload-session.dto.ts new file mode 100644 index 00000000..25ed0596 --- /dev/null +++ b/app/backend/src/uploads/dto/create-upload-session.dto.ts @@ -0,0 +1,20 @@ +import { IsString, IsInt, IsNotEmpty, Min, Max } from 'class-validator'; + +export class CreateUploadSessionDto { + @IsString() + @IsNotEmpty() + ownerId: string; + + @IsString() + @IsNotEmpty() + filename: string; + + @IsString() + @IsNotEmpty() + contentType: string; + + @IsInt() + @Min(1) + @Max(100 * 1024 * 1024) // 100MB limit + totalSize: number; +} diff --git a/app/backend/src/uploads/dto/finalize-upload.dto.ts b/app/backend/src/uploads/dto/finalize-upload.dto.ts new file mode 100644 index 00000000..f74350eb --- /dev/null +++ b/app/backend/src/uploads/dto/finalize-upload.dto.ts @@ -0,0 +1,7 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class FinalizeUploadDto { + @IsString() + @IsNotEmpty() + ownerId: string; +} diff --git a/app/backend/src/uploads/uploads.controller.ts b/app/backend/src/uploads/uploads.controller.ts new file mode 100644 index 00000000..bdfbbe89 --- /dev/null +++ b/app/backend/src/uploads/uploads.controller.ts @@ -0,0 +1,47 @@ +import { Controller, Post, Body, Param, Put, UploadedFile, UseInterceptors, ParseIntPipe, BadRequestException, HttpCode, HttpStatus } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiTags, ApiOperation, ApiConsumes, ApiOkResponse, ApiCreatedResponse, ApiParam } from '@nestjs/swagger'; +import { UploadsService } from './uploads.service'; +import { CreateUploadSessionDto } from './dto/create-upload-session.dto'; +import { FinalizeUploadDto } from './dto/finalize-upload.dto'; + +@ApiTags('Uploads') +@Controller('uploads') +export class UploadsController { + constructor(private readonly uploadsService: UploadsService) {} + + @Post('session') + @ApiOperation({ summary: 'Create an upload session for evidence' }) + @ApiCreatedResponse({ description: 'Upload session created successfully.' }) + async createSession(@Body() dto: CreateUploadSessionDto) { + return this.uploadsService.createSession(dto); + } + + @Put('session/:id/chunks/:chunkIndex') + @ApiOperation({ summary: 'Upload a chunk for a specific session' }) + @ApiParam({ name: 'id', description: 'Upload session ID' }) + @ApiParam({ name: 'chunkIndex', description: '0-based index of the chunk' }) + @ApiConsumes('multipart/form-data') + @UseInterceptors(FileInterceptor('file')) + async uploadChunk( + @Param('id') id: string, + @Param('chunkIndex', ParseIntPipe) chunkIndex: number, + @UploadedFile() file: Express.Multer.File, + ) { + if (!file) { + throw new BadRequestException('File chunk is required'); + } + return this.uploadsService.uploadChunk(id, chunkIndex, file.size, file.buffer); + } + + @Post('session/:id/finalize') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Finalize an upload session' }) + @ApiOkResponse({ description: 'Upload session finalized successfully.' }) + async finalizeSession( + @Param('id') id: string, + @Body() dto: FinalizeUploadDto, + ) { + return this.uploadsService.finalizeSession(id, dto.ownerId); + } +} diff --git a/app/backend/src/uploads/uploads.module.ts b/app/backend/src/uploads/uploads.module.ts new file mode 100644 index 00000000..e053ff31 --- /dev/null +++ b/app/backend/src/uploads/uploads.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UploadsController } from './uploads.controller'; +import { UploadsService } from './uploads.service'; +import { PrismaModule } from '../prisma/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [UploadsController], + providers: [UploadsService], + exports: [UploadsService], +}) +export class UploadsModule {} diff --git a/app/backend/src/uploads/uploads.service.ts b/app/backend/src/uploads/uploads.service.ts new file mode 100644 index 00000000..7b7d8b5d --- /dev/null +++ b/app/backend/src/uploads/uploads.service.ts @@ -0,0 +1,123 @@ +import { Injectable, NotFoundException, BadRequestException, PayloadTooLargeException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateUploadSessionDto } from './dto/create-upload-session.dto'; +import { UploadSessionStatus } from '@prisma/client'; + +@Injectable() +export class UploadsService { + constructor(private prisma: PrismaService) {} + + async createSession(dto: CreateUploadSessionDto) { + // Validate content type + const validContentTypes = ['image/jpeg', 'image/png', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; + if (!validContentTypes.includes(dto.contentType)) { + throw new BadRequestException('Invalid content type. Only images and documents are allowed.'); + } + + // Validate size limit + if (dto.totalSize > 50 * 1024 * 1024) { // 50MB + throw new PayloadTooLargeException('File size exceeds 50MB limit'); + } + + // Set expiry + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); // 24 hour expiry + + return this.prisma.uploadSession.create({ + data: { + ownerId: dto.ownerId, + filename: dto.filename, + contentType: dto.contentType, + totalSize: dto.totalSize, + status: UploadSessionStatus.pending, + expiresAt, + }, + }); + } + + async uploadChunk(sessionId: string, chunkIndex: number, size: number, buffer: Buffer) { + const session = await this.prisma.uploadSession.findUnique({ + where: { id: sessionId }, + }); + + if (!session) { + throw new NotFoundException('Upload session not found'); + } + + if (session.status !== UploadSessionStatus.pending) { + throw new BadRequestException(`Cannot upload chunk to session with status ${session.status}`); + } + + if (session.expiresAt < new Date()) { + await this.prisma.uploadSession.update({ + where: { id: sessionId }, + data: { status: UploadSessionStatus.expired }, + }); + throw new BadRequestException('Upload session expired'); + } + + // Here we would typically stream the buffer to S3, GCS, or a local file system. + // For now, we simulate the storage and just track the chunk completion in the DB. + + // Check if chunk already exists for idempotency (resume semantics) + const existingChunk = await this.prisma.uploadChunk.findUnique({ + where: { + uploadSessionId_chunkIndex: { + uploadSessionId: sessionId, + chunkIndex, + }, + }, + }); + + if (existingChunk) { + return existingChunk; + } + + return this.prisma.uploadChunk.create({ + data: { + uploadSessionId: sessionId, + chunkIndex, + size, + }, + }); + } + + async finalizeSession(sessionId: string, ownerId: string) { + const session = await this.prisma.uploadSession.findUnique({ + where: { id: sessionId }, + include: { chunks: true }, + }); + + if (!session) { + throw new NotFoundException('Upload session not found'); + } + + // Validate ownership + if (session.ownerId !== ownerId) { + throw new BadRequestException('Ownership validation failed'); + } + + if (session.status !== UploadSessionStatus.pending) { + throw new BadRequestException(`Cannot finalize session with status ${session.status}`); + } + + const uploadedSize = session.chunks.reduce((acc, chunk) => acc + chunk.size, 0); + if (uploadedSize !== session.totalSize) { + throw new BadRequestException(`Size mismatch: expected ${session.totalSize}, got ${uploadedSize}`); + } + + // Check chunk ordering (make sure all chunks are present and contiguous) + const sortedChunks = session.chunks.sort((a, b) => a.chunkIndex - b.chunkIndex); + for (let i = 0; i < sortedChunks.length; i++) { + if (sortedChunks[i].chunkIndex !== i) { + throw new BadRequestException(`Missing chunk at index ${i}`); + } + } + + // Transition state + return this.prisma.uploadSession.update({ + where: { id: sessionId }, + data: { status: UploadSessionStatus.completed }, + }); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94bd8aa8..1a760889 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@nestjs/swagger': specifier: ^11.2.6 - version: 11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + version: 11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) express: specifier: ^5.2.1 version: 5.2.1 @@ -32,16 +32,16 @@ importers: dependencies: '@liaoliaots/nestjs-redis': specifier: ^10.0.0 - version: 10.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(ioredis@5.10.1) + version: 10.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(ioredis@5.10.1) '@nestjs/axios': specifier: ^4.0.1 version: 4.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.6)(rxjs@7.8.2) '@nestjs/bull': specifier: ^11.0.4 - version: 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(bull@4.16.5) + version: 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bull@4.16.5) '@nestjs/bullmq': specifier: ^11.0.4 - version: 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(bullmq@5.71.0) + version: 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.71.0) '@nestjs/common': specifier: ^11.0.1 version: 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -56,13 +56,13 @@ importers: version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) '@nestjs/swagger': specifier: ^11.2.5 - version: 11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + version: 11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: ^11.0.0 - version: 11.1.1(@nestjs/axios@4.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.6)(rxjs@7.8.2))(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3))(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.1(@nestjs/axios@4.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.6)(rxjs@7.8.2))(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3))(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/throttler': specifier: ^6.5.0 - version: 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2) + version: 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) '@prisma/client': specifier: ^6.19.2 version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3) @@ -93,6 +93,9 @@ importers: ioredis: specifier: ^5.9.2 version: 5.10.1 + openai: + specifier: ^6.33.0 + version: 6.34.0(ws@8.19.0)(zod@4.3.6) pino: specifier: ^10.3.0 version: 10.3.1 @@ -126,7 +129,7 @@ importers: version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.0.1 - version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-express@11.1.17) + version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)) '@types/express': specifier: ^5.0.0 version: 5.0.6 @@ -180,7 +183,7 @@ importers: version: 7.2.2 ts-jest: specifier: ^29.2.5 - version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@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) @@ -283,7 +286,7 @@ importers: version: 4.2.2 ts-jest: specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.19.37)(typescript@5.9.3) @@ -6331,6 +6334,18 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openai@6.34.0: + resolution: {integrity: sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -9849,7 +9864,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@liaoliaots/nestjs-redis@10.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(ioredis@5.10.1)': + '@liaoliaots/nestjs-redis@10.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(ioredis@5.10.1)': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -9898,23 +9913,23 @@ snapshots: axios: 1.13.6 rxjs: 7.8.2 - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bull@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(bull@4.16.5)': + '@nestjs/bull@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bull@4.16.5)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) bull: 4.16.5 tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(bullmq@5.71.0)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.71.0)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) bullmq: 5.71.0 @@ -10014,7 +10029,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -10029,7 +10044,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.4 - '@nestjs/terminus@11.1.1(@nestjs/axios@4.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.6)(rxjs@7.8.2))(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/terminus@11.1.1(@nestjs/axios@4.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.6)(rxjs@7.8.2))(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3))(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -10041,7 +10056,7 @@ snapshots: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.6)(rxjs@7.8.2) '@prisma/client': 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3) - '@nestjs/testing@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-express@11.1.17)': + '@nestjs/testing@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17))': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -10049,7 +10064,7 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) - '@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)': + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -12863,7 +12878,7 @@ snapshots: transitivePeerDependencies: - supports-color - 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@9.39.4(jiti@2.6.1)): + 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)): dependencies: debug: 3.2.7 optionalDependencies: @@ -12894,7 +12909,7 @@ snapshots: 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@9.39.4(jiti@2.6.1)) + 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 @@ -15668,6 +15683,11 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@6.34.0(ws@8.19.0)(zod@4.3.6): + optionalDependencies: + ws: 8.19.0 + zod: 4.3.6 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -17005,7 +17025,7 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 30.3.0 - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@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.3.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@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 @@ -17022,10 +17042,10 @@ snapshots: '@babel/core': 7.29.0 '@jest/transform': 30.3.0 '@jest/types': 30.3.0 - babel-jest: 30.3.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 30.3.0 - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@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.3.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@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 @@ -17042,7 +17062,7 @@ snapshots: '@babel/core': 7.29.0 '@jest/transform': 30.3.0 '@jest/types': 30.3.0 - babel-jest: 30.3.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 30.3.0 ts-loader@9.5.4(typescript@5.9.3)(webpack@5.104.1): From 21d1ba6fe113ad9079bbf294b36c61b32fd1d806 Mon Sep 17 00:00:00 2001 From: Delightech28 Date: Wed, 29 Apr 2026 08:44:31 +0100 Subject: [PATCH 2/2] chore: resolve lint errors and harden type safety --- app/backend/src/onchain/soroban.adapter.ts | 44 +++++----- .../src/onchain/utils/soroban-error.mapper.ts | 87 ++++++++++--------- app/backend/src/uploads/uploads.controller.ts | 32 ++++++- app/backend/src/uploads/uploads.service.ts | 56 +++++++++--- 4 files changed, 137 insertions(+), 82 deletions(-) diff --git a/app/backend/src/onchain/soroban.adapter.ts b/app/backend/src/onchain/soroban.adapter.ts index 7589d727..51d03d1b 100644 --- a/app/backend/src/onchain/soroban.adapter.ts +++ b/app/backend/src/onchain/soroban.adapter.ts @@ -27,6 +27,13 @@ import { } from './onchain.adapter'; import { SorobanErrorMapper } from './utils/soroban-error.mapper'; +interface SorobanSDK { + SorobanRpc: { + Server: new (url: string, options: { allowHttp: boolean }) => any; + }; + [key: string]: any; +} + /** * Soroban adapter implementation for AidEscrow contract * Handles all interactions with the Soroban AidEscrow contract via RPC @@ -41,7 +48,7 @@ export class SorobanAdapter implements OnchainAdapter { // Note: The actual Soroban SDK will be lazily imported when needed // to avoid bundle size issues in development builds - private sorobanLib: Record | null = null; + private sorobanLib: SorobanSDK | null = null; constructor(private configService: ConfigService) { this.contractId = this.configService.get('SOROBAN_CONTRACT_ID', ''); @@ -62,7 +69,7 @@ export class SorobanAdapter implements OnchainAdapter { } } - private async loadSorobanSDK() { + private async loadSorobanSDK(): Promise { if (this.sorobanLib) { return this.sorobanLib; } @@ -70,14 +77,12 @@ export class SorobanAdapter implements OnchainAdapter { try { // Dynamically import stellar/cli SDK // @ts-expect-error - stellar is optional, only required in production - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const mod = await import('stellar'); + const mod = (await import('stellar')) as unknown as SorobanSDK; this.sorobanLib = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment rpc: mod, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment api: mod, - ...(mod as Record), + SorobanRpc: mod.SorobanRpc, + ...mod, }; return this.sorobanLib; } catch (error) { @@ -93,7 +98,6 @@ export class SorobanAdapter implements OnchainAdapter { */ private async getRpcClient() { const sdk = await this.loadSorobanSDK(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return return new sdk.SorobanRpc.Server(this.rpcUrl, { allowHttp: this.rpcUrl.startsWith('http://'), }); @@ -116,8 +120,7 @@ export class SorobanAdapter implements OnchainAdapter { try { const _sdk = await this.loadSorobanSDK(); - - const _client = await this.getRpcClient(); // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const _client = await this.getRpcClient(); // Note: Actual implementation would require signing the transaction // with the contract owner's keypair and submitting to the network. @@ -156,8 +159,7 @@ export class SorobanAdapter implements OnchainAdapter { try { const _sdk = await this.loadSorobanSDK(); - - const _client = await this.getRpcClient(); // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const _client = await this.getRpcClient(); // Implementation would call contract's create_package method // This is a placeholder showing the expected response @@ -194,8 +196,7 @@ export class SorobanAdapter implements OnchainAdapter { try { const _sdk = await this.loadSorobanSDK(); - - const _client = await this.getRpcClient(); // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const _client = await this.getRpcClient(); // Implementation would call contract's batch_create_packages method const packageIds = params.recipientAddresses.map((_, index) => @@ -231,8 +232,7 @@ export class SorobanAdapter implements OnchainAdapter { try { const _sdk = await this.loadSorobanSDK(); - - const _client = await this.getRpcClient(); // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const _client = await this.getRpcClient(); // Implementation would call contract's claim method const transactionHash = this.generateMockHash( @@ -268,8 +268,7 @@ export class SorobanAdapter implements OnchainAdapter { try { const _sdk = await this.loadSorobanSDK(); - - const _client = await this.getRpcClient(); // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const _client = await this.getRpcClient(); // Implementation would call contract's disburse method const transactionHash = this.generateMockHash( @@ -302,8 +301,7 @@ export class SorobanAdapter implements OnchainAdapter { try { const _sdk = await this.loadSorobanSDK(); - - const _client = await this.getRpcClient(); // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const _client = await this.getRpcClient(); // Implementation would call contract's get_package method // For now, returning a mock response structure @@ -339,8 +337,7 @@ export class SorobanAdapter implements OnchainAdapter { try { const _sdk = await this.loadSorobanSDK(); - - const _client = await this.getRpcClient(); // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const _client = await this.getRpcClient(); // Implementation would call contract's get_aggregates method // Returns aggregates for the specified token @@ -370,8 +367,7 @@ export class SorobanAdapter implements OnchainAdapter { try { const _sdk = await this.loadSorobanSDK(); - - const _client = await this.getRpcClient(); // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const _client = await this.getRpcClient(); // Implementation would call token contract's balance method // This is a placeholder showing the expected response diff --git a/app/backend/src/onchain/utils/soroban-error.mapper.ts b/app/backend/src/onchain/utils/soroban-error.mapper.ts index 81886790..c8c61cc5 100644 --- a/app/backend/src/onchain/utils/soroban-error.mapper.ts +++ b/app/backend/src/onchain/utils/soroban-error.mapper.ts @@ -3,6 +3,17 @@ import { InternalServerErrorException, } from '@nestjs/common'; +interface BlockchainError { + code?: string | number; + message?: string; + response?: { + data?: { + error?: any; + }; + }; + errorCode?: number; +} + /** * Maps Soroban contract errors to standardized backend error responses * Aligns with the global error handling strategy @@ -37,45 +48,41 @@ export class SorobanErrorMapper { /** * Maps a Soroban error to a backend-compatible error with HTTP status code */ - mapError(error: any): { + mapError(error: unknown): { statusCode: number; message: string; - details?: Record; + details?: unknown; } { + // Cast to BlockchainError for type safety + const err = error as BlockchainError; + // Handle RPC/Network errors - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND') { + if (err && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND')) { return { statusCode: 503, message: 'Blockchain network unreachable', details: { error_type: 'network_error', - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - original_error: error?.message, + original_error: err.message, }, }; } // Handle JSON-RPC errors (Soroban RPC Server responses) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (error?.response?.data?.error) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment - const jsonRpcError = error.response.data.error; + if (err && err.response?.data?.error) { + const jsonRpcError = err.response.data.error; return this.mapJsonRpcError(jsonRpcError); } // Handle Soroban SDK errors with specific error codes - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (error?.errorCode !== undefined) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const mapping = this.contractErrors[error.errorCode as number]; + if (err && err.errorCode !== undefined) { + const mapping = this.contractErrors[err.errorCode]; if (mapping) { return { statusCode: mapping.code, message: mapping.message, details: { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - error_code: error.errorCode, + error_code: err.errorCode, error_type: 'contract_error', }, }; @@ -83,40 +90,38 @@ export class SorobanErrorMapper { } // Handle contract invocation errors - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const message = error?.message as string | undefined; if ( - message && - (message.includes('NotInitialized') || - message.includes('AlreadyInitialized') || - message.includes('NotAuthorized') || - message.includes('PackageNotFound') || - message.includes('PackageExpired')) + err && + err.message && + (err.message.includes('NotInitialized') || + err.message.includes('AlreadyInitialized') || + err.message.includes('NotAuthorized') || + err.message.includes('PackageNotFound') || + err.message.includes('PackageExpired')) ) { - return this.mapContractErrorMessage(message); + return this.mapContractErrorMessage(err.message); } // Handle timeout errors - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (error?.code === 'ETIMEDOUT' || message?.includes('timeout')) { + if (err && (err.code === 'ETIMEDOUT' || err.message?.includes('timeout'))) { return { statusCode: 504, message: 'Blockchain operation timed out', details: { error_type: 'timeout', - original_error: message, + original_error: err.message, }, }; } // Handle transaction submission errors - if (message?.includes('transaction')) { + if (err && err.message?.includes('transaction')) { return { statusCode: 400, message: 'Transaction submission failed', details: { error_type: 'transaction_error', - original_error: message, + original_error: err.message, }, }; } @@ -127,7 +132,8 @@ export class SorobanErrorMapper { message: 'An error occurred while communicating with the blockchain', details: { error_type: 'unknown_error', - original_message: message, + original_message: + err instanceof Error ? err.message : 'Unknown blockchain error', }, }; } @@ -135,15 +141,14 @@ export class SorobanErrorMapper { /** * Maps JSON-RPC error responses (from Soroban RPC) */ - private mapJsonRpcError(jsonRpcError: any): { + private mapJsonRpcError(jsonRpcError: unknown): { statusCode: number; message: string; - details?: Record; + details?: unknown; } { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const code = jsonRpcError.code; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const message = (jsonRpcError.message as string) || ''; + const err = jsonRpcError as { code?: number; message?: string }; + const code = err?.code; + const message = err?.message || ''; // JSON-RPC error codes mapping switch (code) { @@ -166,7 +171,7 @@ export class SorobanErrorMapper { return { statusCode: 500, message: 'Blockchain RPC internal error', - details: { error_code: code as number, rpc_message: message }, + details: { error_code: code, rpc_message: message }, }; default: @@ -175,13 +180,13 @@ export class SorobanErrorMapper { return { statusCode: 500, message: 'Blockchain RPC server error', - details: { error_code: code as number, rpc_message: message }, + details: { error_code: code, rpc_message: message }, }; } return { statusCode: 500, message: 'Blockchain RPC error', - details: { error_code: code as number, rpc_message: message }, + details: { error_code: code, rpc_message: message }, }; } } @@ -192,7 +197,7 @@ export class SorobanErrorMapper { private mapContractErrorMessage(message: string): { statusCode: number; message: string; - details?: Record; + details?: unknown; } { const errorMap: Record = { NotInitialized: { code: 400, message: 'Escrow not initialized' }, diff --git a/app/backend/src/uploads/uploads.controller.ts b/app/backend/src/uploads/uploads.controller.ts index bdfbbe89..48c5025d 100644 --- a/app/backend/src/uploads/uploads.controller.ts +++ b/app/backend/src/uploads/uploads.controller.ts @@ -1,6 +1,25 @@ -import { Controller, Post, Body, Param, Put, UploadedFile, UseInterceptors, ParseIntPipe, BadRequestException, HttpCode, HttpStatus } from '@nestjs/common'; +import { + Controller, + Post, + Body, + Param, + Put, + UploadedFile, + UseInterceptors, + ParseIntPipe, + BadRequestException, + HttpCode, + HttpStatus, +} from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; -import { ApiTags, ApiOperation, ApiConsumes, ApiOkResponse, ApiCreatedResponse, ApiParam } from '@nestjs/swagger'; +import { + ApiTags, + ApiOperation, + ApiConsumes, + ApiOkResponse, + ApiCreatedResponse, + ApiParam, +} from '@nestjs/swagger'; import { UploadsService } from './uploads.service'; import { CreateUploadSessionDto } from './dto/create-upload-session.dto'; import { FinalizeUploadDto } from './dto/finalize-upload.dto'; @@ -26,12 +45,17 @@ export class UploadsController { async uploadChunk( @Param('id') id: string, @Param('chunkIndex', ParseIntPipe) chunkIndex: number, - @UploadedFile() file: Express.Multer.File, + @UploadedFile() file: { size: number; buffer: Buffer }, ) { if (!file) { throw new BadRequestException('File chunk is required'); } - return this.uploadsService.uploadChunk(id, chunkIndex, file.size, file.buffer); + return this.uploadsService.uploadChunk( + id, + chunkIndex, + file.size, + file.buffer, + ); } @Post('session/:id/finalize') diff --git a/app/backend/src/uploads/uploads.service.ts b/app/backend/src/uploads/uploads.service.ts index 7b7d8b5d..70ba640c 100644 --- a/app/backend/src/uploads/uploads.service.ts +++ b/app/backend/src/uploads/uploads.service.ts @@ -1,4 +1,9 @@ -import { Injectable, NotFoundException, BadRequestException, PayloadTooLargeException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + BadRequestException, + PayloadTooLargeException, +} from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateUploadSessionDto } from './dto/create-upload-session.dto'; import { UploadSessionStatus } from '@prisma/client'; @@ -9,13 +14,22 @@ export class UploadsService { async createSession(dto: CreateUploadSessionDto) { // Validate content type - const validContentTypes = ['image/jpeg', 'image/png', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; + const validContentTypes = [ + 'image/jpeg', + 'image/png', + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ]; if (!validContentTypes.includes(dto.contentType)) { - throw new BadRequestException('Invalid content type. Only images and documents are allowed.'); + throw new BadRequestException( + 'Invalid content type. Only images and documents are allowed.', + ); } - + // Validate size limit - if (dto.totalSize > 50 * 1024 * 1024) { // 50MB + if (dto.totalSize > 50 * 1024 * 1024) { + // 50MB throw new PayloadTooLargeException('File size exceeds 50MB limit'); } @@ -35,7 +49,12 @@ export class UploadsService { }); } - async uploadChunk(sessionId: string, chunkIndex: number, size: number, buffer: Buffer) { + async uploadChunk( + sessionId: string, + chunkIndex: number, + size: number, + _buffer: Buffer, + ) { const session = await this.prisma.uploadSession.findUnique({ where: { id: sessionId }, }); @@ -45,7 +64,9 @@ export class UploadsService { } if (session.status !== UploadSessionStatus.pending) { - throw new BadRequestException(`Cannot upload chunk to session with status ${session.status}`); + throw new BadRequestException( + `Cannot upload chunk to session with status ${session.status}`, + ); } if (session.expiresAt < new Date()) { @@ -58,7 +79,7 @@ export class UploadsService { // Here we would typically stream the buffer to S3, GCS, or a local file system. // For now, we simulate the storage and just track the chunk completion in the DB. - + // Check if chunk already exists for idempotency (resume semantics) const existingChunk = await this.prisma.uploadChunk.findUnique({ where: { @@ -91,23 +112,32 @@ export class UploadsService { if (!session) { throw new NotFoundException('Upload session not found'); } - + // Validate ownership if (session.ownerId !== ownerId) { throw new BadRequestException('Ownership validation failed'); } if (session.status !== UploadSessionStatus.pending) { - throw new BadRequestException(`Cannot finalize session with status ${session.status}`); + throw new BadRequestException( + `Cannot finalize session with status ${session.status}`, + ); } - const uploadedSize = session.chunks.reduce((acc, chunk) => acc + chunk.size, 0); + const uploadedSize = session.chunks.reduce( + (acc, chunk) => acc + chunk.size, + 0, + ); if (uploadedSize !== session.totalSize) { - throw new BadRequestException(`Size mismatch: expected ${session.totalSize}, got ${uploadedSize}`); + throw new BadRequestException( + `Size mismatch: expected ${session.totalSize}, got ${uploadedSize}`, + ); } // Check chunk ordering (make sure all chunks are present and contiguous) - const sortedChunks = session.chunks.sort((a, b) => a.chunkIndex - b.chunkIndex); + const sortedChunks = session.chunks.sort( + (a, b) => a.chunkIndex - b.chunkIndex, + ); for (let i = 0; i < sortedChunks.length; i++) { if (sortedChunks[i].chunkIndex !== i) { throw new BadRequestException(`Missing chunk at index ${i}`);