From 1294ba010650a5c7c86222970236de3b62ab4720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Dr=C3=A9an?= Date: Fri, 3 Apr 2026 23:12:04 +0200 Subject: [PATCH] feat: add error logging across services, fix ci script, and silence test output --- bun.lock | 3 ++ jest.config.ts | 1 + package.json | 6 ++- src/app.module.ts | 15 +++++++ src/common/auth/bearer.guard.ts | 7 +++ src/common/throttler/throttler.guard.ts | 37 ++++++++++++++++ src/jobs/jobs.controller.ts | 16 ++++++- src/jobs/jobs.service.ts | 18 +++++--- src/main.ts | 12 +++++ src/metadata/metadata.service.ts | 40 ++++++++++------- src/queue/key-generation.processor.ts | 12 ++++- src/queue/signing.processor.spec.ts | 12 ++--- src/queue/signing.processor.ts | 44 ++++++++++++++----- .../key-generation.controller.ts | 16 ++++++- .../key-generation/key-generation.dto.ts | 1 - src/tasks/signing/signing.controller.ts | 15 ++++++- test/jest.setup.ts | 4 ++ 17 files changed, 211 insertions(+), 48 deletions(-) create mode 100644 src/common/throttler/throttler.guard.ts create mode 100644 test/jest.setup.ts diff --git a/bun.lock b/bun.lock index a2c68ff..b856b63 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "@nestjs/microservices": "^11.1.18", "@nestjs/platform-express": "^11.1.18", "@nestjs/swagger": "^11.2.6", + "@nestjs/throttler": "^6.5.0", "bullmq": "^5.73.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.4", @@ -327,6 +328,8 @@ "@nestjs/testing": ["@nestjs/testing@11.1.18", "", { "dependencies": { "tslib": "2.8.1" }, "peerDependencies": { "@nestjs/common": "^11.0.0", "@nestjs/core": "^11.0.0", "@nestjs/microservices": "^11.0.0", "@nestjs/platform-express": "^11.0.0" }, "optionalPeers": ["@nestjs/microservices", "@nestjs/platform-express"] }, "sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw=="], + "@nestjs/throttler": ["@nestjs/throttler@6.5.0", "", { "peerDependencies": { "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "reflect-metadata": "^0.1.13 || ^0.2.0" } }, "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ=="], + "@nicolo-ribaudo/eslint-scope-5-internals": ["@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1", "", { "dependencies": { "eslint-scope": "5.1.1" } }, "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], diff --git a/jest.config.ts b/jest.config.ts index fc47025..0977b48 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -11,6 +11,7 @@ const config: Config = { moduleNameMapper: { "^@/(.*)$": "/src/$1", }, + setupFilesAfterEnv: ["/test/jest.setup.ts"], }; export default config; diff --git a/package.json b/package.json index da394b7..9cf197c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", "docs": "typedoc", + "ci": "bun run format && bun run lint && bun run test:coverage && bun run docs && bun run build", "act:ci": "act -W .github/workflows/ci.yaml -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:full-latest --container-architecture linux/amd64" }, "dependencies": { @@ -31,6 +32,7 @@ "@nestjs/microservices": "^11.1.18", "@nestjs/platform-express": "^11.1.18", "@nestjs/swagger": "^11.2.6", + "@nestjs/throttler": "^6.5.0", "bullmq": "^5.73.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.4", @@ -79,8 +81,8 @@ "ts-loader": "^9.5.7", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", + "typedoc": "^0.28.18", "typescript": "^5.9.3", - "typescript-eslint": "^8.58.0", - "typedoc": "^0.28.18" + "typescript-eslint": "^8.58.0" } } diff --git a/src/app.module.ts b/src/app.module.ts index 3a49f47..2c5b4c9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,7 @@ import { AppConfigModule } from "@/common/config/config.module"; import { AppConfigService } from "@/common/config/config.service"; import { LogLevel } from "@/common/constants/log-level"; +import { IpBearerThrottlerGuard } from "@/common/throttler/throttler.guard"; import { Environment } from "@/common/utils/environment"; import { GrpcModule } from "@/grpc/grpc.module"; import { JobsModule } from "@/jobs/jobs.module"; @@ -9,6 +10,8 @@ import { KeyGenerationModule } from "@/tasks/key-generation/key-generation.modul import { SigningModule } from "@/tasks/signing/signing.module"; import { BullModule } from "@nestjs/bullmq"; import { Module } from "@nestjs/common"; +import { APP_GUARD } from "@nestjs/core"; +import { ThrottlerModule } from "@nestjs/throttler"; import { LoggerModule, type Params } from "nestjs-pino"; import { type TransportTargetOptions } from "pino"; @@ -26,6 +29,12 @@ import { type TransportTargetOptions } from "pino"; @Module({ imports: [ AppConfigModule, + ThrottlerModule.forRoot([ + { + ttl: 60_000, + limit: 100, + }, + ]), LoggerModule.forRootAsync({ inject: [AppConfigService], useFactory: (configService: AppConfigService): Params => { @@ -78,6 +87,12 @@ import { type TransportTargetOptions } from "pino"; SigningModule, JobsModule, ], + providers: [ + { + provide: APP_GUARD, + useClass: IpBearerThrottlerGuard, + }, + ], }) class AppModule {} diff --git a/src/common/auth/bearer.guard.ts b/src/common/auth/bearer.guard.ts index 2b249d8..1147001 100644 --- a/src/common/auth/bearer.guard.ts +++ b/src/common/auth/bearer.guard.ts @@ -5,6 +5,7 @@ import { type CanActivate, type ExecutionContext, Injectable, + Logger, UnauthorizedException, } from "@nestjs/common"; import { timingSafeEqual } from "crypto"; @@ -21,6 +22,8 @@ import { timingSafeEqual } from "crypto"; */ @Injectable() class BearerGuard implements CanActivate { + private readonly logger: Logger = new Logger(BearerGuard.name); + constructor(private readonly configService: AppConfigService) {} /** @@ -47,12 +50,14 @@ class BearerGuard implements CanActivate { ] ?? null); if (!authorizationHeader) { + this.logger.warn("Rejected request: missing Authorization header."); throw new UnauthorizedException(Message.MISSING_AUTH_HEADER); } const [scheme, token]: string[] = authorizationHeader.split(" "); if (scheme?.toLowerCase() !== AuthScheme.BEARER || !token) { + this.logger.warn("Rejected request: invalid Authorization scheme."); throw new UnauthorizedException(Message.INVALID_AUTH_SCHEME); } @@ -60,6 +65,7 @@ class BearerGuard implements CanActivate { // Prevent token length from leaking information. if (token.length !== expectedToken.length) { + this.logger.warn("Rejected request: invalid Bearer token."); throw new UnauthorizedException(Message.INVALID_BEARER_TOKEN); } @@ -67,6 +73,7 @@ class BearerGuard implements CanActivate { const expectedBuffer: Buffer = Buffer.from(expectedToken); if (!timingSafeEqual(tokenBuffer, expectedBuffer)) { + this.logger.warn("Rejected request: invalid Bearer token."); throw new UnauthorizedException(Message.INVALID_BEARER_TOKEN); } diff --git a/src/common/throttler/throttler.guard.ts b/src/common/throttler/throttler.guard.ts new file mode 100644 index 0000000..a884f93 --- /dev/null +++ b/src/common/throttler/throttler.guard.ts @@ -0,0 +1,37 @@ +import { AuthScheme, Header } from "@/common/constants/header"; +import { Injectable } from "@nestjs/common"; +import { ThrottlerGuard as _ThrottlerGuard } from "@nestjs/throttler"; +import type { Request } from "express"; + +/** + * Rate-limiting guard that keys each throttle bucket on the combination of the + * client IP address and Bearer token. + * + * Using both dimensions means: + * + * - Different IPs with the same token are counted separately (IP-level limit). + * - The same IP using different tokens is also counted separately (token-level + * limit). + * + * If no token is present the key degrades gracefully to `:` so that + * unauthenticated probing is also throttled. + */ +@Injectable() +class ThrottlerGuard extends _ThrottlerGuard { + /** + * Builds the throttle storage key for the incoming request. + * + * @param {Request} request - The raw Express request object. + * @returns {Promise} A string key of the form `:`. + */ + protected override async getTracker(request: Request): Promise { + const ip: string = request.ip ?? request.socket?.remoteAddress ?? ""; + const authHeader: string = request.headers?.[Header.AUTHORIZATION] ?? ""; + const token: string = authHeader.startsWith(AuthScheme.BEARER_PREFIX) + ? authHeader.slice(7) + : ""; + return `${ip}:${token}`; + } +} + +export { ThrottlerGuard as IpBearerThrottlerGuard }; diff --git a/src/jobs/jobs.controller.ts b/src/jobs/jobs.controller.ts index d6d2451..72aa270 100644 --- a/src/jobs/jobs.controller.ts +++ b/src/jobs/jobs.controller.ts @@ -6,6 +6,7 @@ import { Controller, Get, HttpStatus, + Logger, Param, ParseUUIDPipe, UseGuards, @@ -34,6 +35,8 @@ import { @UseGuards(BearerGuard) @Controller(Endpoint.JOBS) class JobsController { + private readonly logger: Logger = new Logger(JobsController.name); + constructor(private readonly jobsService: JobsService) {} /** @@ -51,10 +54,21 @@ class JobsController { description: "Unauthorized.", }) @ApiResponse({ status: HttpStatus.NOT_FOUND, description: "Job not found." }) + @ApiResponse({ + status: 429, + description: "Too Many Requests.", + }) async getJobStatus( @Param("jobId", new ParseUUIDPipe()) jobId: string, ): Promise { - return this.jobsService.getJobStatus(jobId); + this.logger.debug(`GET /jobs/${jobId}`); + + const result: JobStatusResponse = + await this.jobsService.getJobStatus(jobId); + + this.logger.debug(`Job status — ${JSON.stringify(result)}`); + + return result; } } diff --git a/src/jobs/jobs.service.ts b/src/jobs/jobs.service.ts index 7c42dbc..847509a 100644 --- a/src/jobs/jobs.service.ts +++ b/src/jobs/jobs.service.ts @@ -8,7 +8,7 @@ import { } from "@/jobs/jobs.types"; import { QueueName } from "@/queue/queue.constants"; import { InjectQueue } from "@nestjs/bullmq"; -import { Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { type Job, type JobState, type Queue } from "bullmq"; /** @@ -20,6 +20,8 @@ import { type Job, type JobState, type Queue } from "bullmq"; */ @Injectable() class JobsService { + private readonly logger: Logger = new Logger(JobsService.name); + constructor( @InjectQueue(QueueName.KEY_GENERATION) private readonly keyGenerationQueue: Queue, @@ -74,7 +76,10 @@ class JobsService { job.finishedOn ?? job.processedOn ?? job.timestamp, ).toISOString(); - if (!job.id) throw new Error("Job is missing its identifier."); + if (!job.id) { + this.logger.error("BullMQ returned a job without an identifier."); + throw new Error("Job is missing its identifier."); + } return { jobId: job.id, @@ -99,10 +104,11 @@ class JobsService { * @throws {Error} When the return value is not a valid object. */ private validateJobResult(value: unknown): JobResult { - if (typeof value !== "object" || value === null) { - throw new Error("Job completed with an invalid result."); - } - return value as JobResult; + if (typeof value === "object" && value !== null) return value as JobResult; + this.logger.error( + `Job completed with an invalid result: ${JSON.stringify(value)}`, + ); + throw new Error("Job completed with an invalid result."); } /** diff --git a/src/main.ts b/src/main.ts index 9deb254..98fe825 100644 --- a/src/main.ts +++ b/src/main.ts @@ -49,6 +49,18 @@ const bootstrap = async (): Promise => { .build(); SwaggerModule.setup("api", app, SwaggerModule.createDocument(app, config)); + + const logger: Logger = app.get(Logger); + await app.listen(configService.port); + logger.log( + `Swagger UI: http://localhost:${configService.port}/api`, + "Bootstrap", + ); + logger.log( + `Bearer token: ${configService.clientBearerToken}`, + "Bootstrap", + ); + return; } await app.listen(configService.port); diff --git a/src/metadata/metadata.service.ts b/src/metadata/metadata.service.ts index e4dab1e..93ad172 100644 --- a/src/metadata/metadata.service.ts +++ b/src/metadata/metadata.service.ts @@ -1,7 +1,8 @@ import { AppConfigService } from "@/common/config/config.service"; import { type Metadata } from "@/metadata/metadata.types"; -import { Injectable, OnModuleDestroy } from "@nestjs/common"; +import { Injectable, Logger, OnModuleDestroy } from "@nestjs/common"; import Redis from "ioredis"; +import { ResultAsync } from "neverthrow"; /** * Redis key prefix used to namespace metadata entries. Prevents collisions @@ -28,6 +29,7 @@ const METADATA_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days. */ @Injectable() class MetadataService implements OnModuleDestroy { + private readonly logger = new Logger(MetadataService.name); private readonly redis: Redis; constructor(private readonly configService: AppConfigService) { @@ -42,7 +44,7 @@ class MetadataService implements OnModuleDestroy { }); // Prevent unhandled error events from crashing the process. this.redis.on("error", (error: Error) => { - console.error("[MetadataService] Redis error:", error.message); + this.logger.error("Redis connection error", error.message); }); } @@ -73,22 +75,28 @@ class MetadataService implements OnModuleDestroy { * * @param {string} keyIdentifier - Application-assigned stable key * identifier. - * @returns {Promise} The stored `Metadata`, or `null` if - * not found or expired. + * @returns {ResultAsync} The stored `Metadata`, or + * `null` if not found or expired. Errors when the stored value cannot be + * parsed (corrupted entry). */ - async retrieve(keyIdentifier: string): Promise { + retrieve(keyIdentifier: string): ResultAsync { const redisKey: string = `${METADATA_REDIS_PREFIX}:${keyIdentifier}`; - const serialized: string | null = await this.redis.get(redisKey); - if (!serialized) return null; - - try { - return JSON.parse(serialized) as Metadata; - } catch { - throw new Error( - `Corrupted metadata for '${keyIdentifier}'. ` + - `Delete and re-run key generation.`, - ); - } + return ResultAsync.fromPromise( + this.redis.get(redisKey).then((serialized) => { + if (!serialized) return null; + return JSON.parse(serialized) as Metadata; + }), + (cause) => { + this.logger.error( + `Corrupted metadata for key identifier '${keyIdentifier}' — value cannot be parsed as JSON`, + ); + return new Error( + `Corrupted metadata for '${keyIdentifier}'. ` + + `Delete and re-run key generation.`, + { cause }, + ); + }, + ); } /** diff --git a/src/queue/key-generation.processor.ts b/src/queue/key-generation.processor.ts index ce85dfe..c69e16d 100644 --- a/src/queue/key-generation.processor.ts +++ b/src/queue/key-generation.processor.ts @@ -7,7 +7,7 @@ import { type KeyGenerationJobResult, } from "@/queue/queue.types"; import { Processor, WorkerHost } from "@nestjs/bullmq"; -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { type Job } from "bullmq"; import { Result } from "neverthrow"; @@ -33,6 +33,8 @@ import { Result } from "neverthrow"; lockDuration: JobTimeout.KEY_GENERATION, }) class KeyGenerationProcessor extends WorkerHost { + private readonly logger: Logger = new Logger(KeyGenerationProcessor.name); + constructor( private readonly grpcService: GrpcService, private readonly metadataService: MetadataService, @@ -61,7 +63,13 @@ class KeyGenerationProcessor extends WorkerHost { participants: job.data.participants, }); - if (result.isErr()) throw result.error; + if (result.isErr()) { + this.logger.error( + `gRPC GenerateKey failed for job ${job.id}: ${result.error.message}`, + ); + throw result.error; + } + const publicKey: string = Buffer.from(result.value.publicKey).toString( "hex", ); diff --git a/src/queue/signing.processor.spec.ts b/src/queue/signing.processor.spec.ts index ecb19ed..d5f2efe 100644 --- a/src/queue/signing.processor.spec.ts +++ b/src/queue/signing.processor.spec.ts @@ -58,7 +58,7 @@ describe("SigningProcessor", () => { it("Throws NotFoundException when key metadata is absent.", async () => { // The signing processor cannot proceed without the publicKeyPackage and // algorithm stored during key generation — fail fast with a clear error. - metadataService.retrieve.mockResolvedValue(null); + metadataService.retrieve.mockReturnValue(okAsync(null)); await expect(processor.process(makeJob(JOB_DATA))).rejects.toThrow( NotFoundException, @@ -68,7 +68,7 @@ describe("SigningProcessor", () => { it("Calls sign with the correct gRPC request payload.", async () => { // The processor must decode the stored base64 publicKeyPackage back to a // Buffer, and the hex message back to a Buffer, before forwarding to gRPC. - metadataService.retrieve.mockResolvedValue(KEY_METADATA); + metadataService.retrieve.mockReturnValue(okAsync(KEY_METADATA)); grpcService.sign.mockReturnValue( okAsync({ result: { raw: Buffer.alloc(64, 0xff) } }), ); @@ -90,7 +90,7 @@ describe("SigningProcessor", () => { // FROST algorithms (Ed25519, Schnorr) return a single 64-byte raw buffer // with no recovery id. const rawBytes: Buffer = Buffer.alloc(64, 0xff); - metadataService.retrieve.mockResolvedValue(KEY_METADATA); + metadataService.retrieve.mockReturnValue(okAsync(KEY_METADATA)); grpcService.sign.mockReturnValue(okAsync({ result: { raw: rawBytes } })); const result: SigningJobResult = await processor.process( @@ -106,7 +106,7 @@ describe("SigningProcessor", () => { // (0–3); the processor must concatenate r‖s into a single hex string. const r: Buffer = Buffer.alloc(32, 0x11); const s: Buffer = Buffer.alloc(32, 0x22); - metadataService.retrieve.mockResolvedValue(KEY_METADATA); + metadataService.retrieve.mockReturnValue(okAsync(KEY_METADATA)); grpcService.sign.mockReturnValue( okAsync({ result: { ecdsa: { r, s, v: 1 } } }), ); @@ -123,7 +123,7 @@ describe("SigningProcessor", () => { // An empty result object (neither `raw` nor `ecdsa`) indicates a bug in // the engine protocol; surface it as an explicit error rather than // silently returning an undefined signature. - metadataService.retrieve.mockResolvedValue(KEY_METADATA); + metadataService.retrieve.mockReturnValue(okAsync(KEY_METADATA)); grpcService.sign.mockReturnValue(okAsync({ result: {} })); await expect(processor.process(makeJob(JOB_DATA))).rejects.toThrow( @@ -136,7 +136,7 @@ describe("SigningProcessor", () => { const message: string = "Unavailable."; // GrpcService wraps the engine error via formatGrpcError before returning // errAsync; the processor re-throws result.error as-is. - metadataService.retrieve.mockResolvedValue(KEY_METADATA); + metadataService.retrieve.mockReturnValue(okAsync(KEY_METADATA)); grpcService.sign.mockReturnValue( errAsync(new Error(Message.ENGINE_ERROR(14, message))), ); diff --git a/src/queue/signing.processor.ts b/src/queue/signing.processor.ts index 6e09408..d75db2d 100644 --- a/src/queue/signing.processor.ts +++ b/src/queue/signing.processor.ts @@ -2,16 +2,16 @@ import { Message } from "@/common/constants/message"; import { GrpcService } from "@/grpc/grpc.service"; import { type SignResponse } from "@/grpc/grpc.types"; import { MetadataService } from "@/metadata/metadata.service"; -import { type Metadata } from "@/metadata/metadata.types"; +import { Metadata } from "@/metadata/metadata.types"; import { JobTimeout, QueueName } from "@/queue/queue.constants"; import { type SigningJobData, type SigningJobResult, } from "@/queue/queue.types"; import { Processor, WorkerHost } from "@nestjs/bullmq"; -import { Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { type Job } from "bullmq"; -import { Result } from "neverthrow"; +import { type Result } from "neverthrow"; /** * BullMQ processor for the `signing` queue. @@ -32,6 +32,8 @@ import { Result } from "neverthrow"; @Injectable() @Processor(QueueName.SIGNING, { lockDuration: JobTimeout.SIGNING }) class SigningProcessor extends WorkerHost { + private readonly logger: Logger = new Logger(SigningProcessor.name); + constructor( private readonly grpcService: GrpcService, private readonly metadataService: MetadataService, @@ -52,11 +54,21 @@ class SigningProcessor extends WorkerHost { * @throws {Error} Wrapping gRPC error details on engine failure. */ async process(job: Job): Promise { - const metadata: Metadata | null = await this.metadataService.retrieve( - job.data.keyIdentifier, - ); + const metadataResult: Result = + await this.metadataService.retrieve(job.data.keyIdentifier); + + if (metadataResult.isErr()) { + this.logger.error( + `Failed to retrieve key metadata for job ${job.id}:` + + `${metadataResult.error.message}`, + ); + throw metadataResult.error; + } - if (!metadata) { + if (!metadataResult.value) { + this.logger.error( + `Key metadata not found for job ${job.id} — run key generation first.`, + ); throw new NotFoundException( Message.KEY_METADATA_NOT_FOUND(job.data.keyIdentifier), ); @@ -64,14 +76,22 @@ class SigningProcessor extends WorkerHost { const result: Result = await this.grpcService.sign({ keyIdentifier: job.data.keyIdentifier, - publicKeyPackage: Buffer.from(metadata.publicKeyPackage, "base64"), - algorithm: metadata.algorithm, - threshold: metadata.threshold, - participants: metadata.participants, + publicKeyPackage: Buffer.from( + metadataResult.value.publicKeyPackage, + "base64", + ), + algorithm: metadataResult.value.algorithm, + threshold: metadataResult.value.threshold, + participants: metadataResult.value.participants, message: Buffer.from(job.data.message, "hex"), }); - if (result.isErr()) throw result.error; + if (result.isErr()) { + this.logger.error( + `gRPC Sign failed for job ${job.id}: ${result.error.message}`, + ); + throw result.error; + } return mapSignatureResult(result.value); } } diff --git a/src/tasks/key-generation/key-generation.controller.ts b/src/tasks/key-generation/key-generation.controller.ts index 2643cab..cf1978e 100644 --- a/src/tasks/key-generation/key-generation.controller.ts +++ b/src/tasks/key-generation/key-generation.controller.ts @@ -10,6 +10,7 @@ import { Controller, HttpCode, HttpStatus, + Logger, Post, UseGuards, } from "@nestjs/common"; @@ -34,6 +35,8 @@ import { @UseGuards(BearerGuard) @Controller(Endpoint.KEY_GENERATION) class KeyGenerationController { + private readonly logger: Logger = new Logger(KeyGenerationController.name); + constructor(private readonly keyGenerationService: KeyGenerationService) {} /** @@ -55,10 +58,21 @@ class KeyGenerationController { status: HttpStatus.UNAUTHORIZED, description: "Unauthorized.", }) + @ApiResponse({ + status: 429, + description: "Too Many Requests.", + }) async enqueueKeyGeneration( @Body() dto: KeyGenerationRequestDto, ): Promise { - return this.keyGenerationService.enqueue(dto); + this.logger.log(`POST /key-generation — ${JSON.stringify(dto)}`); + + const result: KeyGenerationResponseDto = + await this.keyGenerationService.enqueue(dto); + + this.logger.log(`Key generation job enqueued — ${JSON.stringify(result)}`); + + return result; } } diff --git a/src/tasks/key-generation/key-generation.dto.ts b/src/tasks/key-generation/key-generation.dto.ts index 43399de..8ddcbb0 100644 --- a/src/tasks/key-generation/key-generation.dto.ts +++ b/src/tasks/key-generation/key-generation.dto.ts @@ -36,7 +36,6 @@ class ThresholdWithinParticipants implements ValidatorConstraintInterface { return dto.participants === undefined || threshold <= dto.participants; } - /** * @returns {string} The default validation failure message. */ diff --git a/src/tasks/signing/signing.controller.ts b/src/tasks/signing/signing.controller.ts index 6ef0b79..6935780 100644 --- a/src/tasks/signing/signing.controller.ts +++ b/src/tasks/signing/signing.controller.ts @@ -10,6 +10,7 @@ import { Controller, HttpCode, HttpStatus, + Logger, Post, UseGuards, } from "@nestjs/common"; @@ -34,6 +35,8 @@ import { @UseGuards(BearerGuard) @Controller(Endpoint.SIGNING) class SigningController { + private readonly logger: Logger = new Logger(SigningController.name); + constructor(private readonly signingService: SigningService) {} /** @@ -55,10 +58,20 @@ class SigningController { status: HttpStatus.UNAUTHORIZED, description: "Unauthorized.", }) + @ApiResponse({ + status: 429, + description: "Too Many Requests.", + }) async enqueueSigning( @Body() dto: SigningRequestDto, ): Promise { - return this.signingService.enqueue(dto); + this.logger.log(`POST /signing — ${JSON.stringify(dto)}`); + + const result: SigningResponseDto = await this.signingService.enqueue(dto); + + this.logger.log(`Signing job enqueued — ${JSON.stringify(result)}`); + + return result; } } diff --git a/test/jest.setup.ts b/test/jest.setup.ts new file mode 100644 index 0000000..317870c --- /dev/null +++ b/test/jest.setup.ts @@ -0,0 +1,4 @@ +import { Logger } from "@nestjs/common"; + +// Silence all NestJS logger output during tests to keep the output clean. +Logger.overrideLogger(false);