import { Injectable } from '@nestjs/common'; import { PrismaThrottlerStorage } from '../common/throttler/prisma-throttler.storage'; /** * Unknown, revoked or expired keys per client network before that network * gets 429 for further invalid keys - the same budget the ERP export * contract names (docs/erp-inventar-export.md). It slows down guessing and a * misconfigured client; a valid key is never refused by it. Otherwise * anyone able to send requests from (or forge) a shared address - the * cluster egress, a proxy - could lock ERPNext or the shop out, and a key * holds 256 random bits, so letting it through costs nothing. */ export const API_KEY_FAILURE_LIMIT = { maxFailures: 20, windowMs: 15 * 60 * 1000, } as const; /** * Throttler name and key prefix of these counters in "ThrottlerHit", so * they never mix with the request rate limits stored in the same table. */ export const API_KEY_FAILURE_THROTTLER = 'api-key-failures'; export interface FailedAttemptVerdict { /** The source is over the limit: refuse the request with 429. */ blocked: boolean; /** This failure is the one that reached the limit (once per window). */ limitReached: boolean; } /** * Counts invalid API keys per source (client network) in the shared, * database-backed rate-limit storage, so the limit holds across all api * pods. One upsert per failure, every time comparison on the database * clock (PrismaThrottlerStorage). The window starts with the first failure; * the failure after the limit blocks the source for one window length. * A database error propagates: the request fails instead of passing. */ @Injectable() export class FailedAttemptLimiter { constructor(private readonly storage: PrismaThrottlerStorage) {} async recordFailure(source: string): Promise { const { maxFailures, windowMs } = API_KEY_FAILURE_LIMIT; const record = await this.storage.increment( `${API_KEY_FAILURE_THROTTLER}:${source}`, windowMs, maxFailures, windowMs, API_KEY_FAILURE_THROTTLER, ); return { blocked: record.isBlocked, limitReached: !record.isBlocked && record.totalHits === maxFailures, }; } }