import { HttpStatus, Injectable } from '@nestjs/common'; import { ThrottlerGuard } from '@nestjs/throttler'; import { AppException, ErrorCode } from '../../common/errors'; /** * Rate limits voice extraction per user rather than per IP. * * The default tracker keys on `req.ip`, which behind nginx means the whole deployment * shares one bucket unless `trust proxy` is set — and an abuser rotating IPs would bypass * it entirely. Since v1 ships with no plan gate, this is the only control on metered * vendor spend, so it has to key on something the client cannot change. * * Guard order matters: the controller's JwtAuthGuard runs before this method-level guard, * so `req.user` is populated by the time `getTracker` is called. */ @Injectable() export class VoiceThrottlerGuard extends ThrottlerGuard { protected getTracker(req: Record): Promise { const user = req?.user as { id?: unknown } | undefined; if (typeof user?.id === 'string' && user.id) { return Promise.resolve(`voice:user:${user.id}`); } // Unauthenticated requests never reach here, but fall back rather than share a bucket. const ip = typeof req?.ip === 'string' ? req.ip : 'unknown'; return Promise.resolve(`voice:ip:${ip}`); } /** Without this, ThrottlerException surfaces as INTERNAL_ERROR — there is no 429 fallback. */ protected throwThrottlingException(): Promise { throw new AppException( ErrorCode.VOICE_RATE_LIMITED, HttpStatus.TOO_MANY_REQUESTS, ); } }