88import { Reflector } from '@nestjs/core' ;
99import { ApiKeyTier } from './entities/api-key.entity' ;
1010import { ApiKeyService , ResolvedKey } from './api-key.service' ;
11- import { REQUIRES_TIER_KEY , TIER_RATE_LIMIT_KEY } from './api-key.decorator' ;
11+ import { REQUIRES_ADMIN_KEY , REQUIRES_TIER_KEY , TIER_RATE_LIMIT_KEY } from './api-key.decorator' ;
1212
1313const TIER_ORDER : ApiKeyTier [ ] = [ 'free' , 'pro' , 'industrial' ] ;
1414
@@ -28,6 +28,37 @@ export class ApiKeyGuard implements CanActivate {
2828 async canActivate ( context : ExecutionContext ) : Promise < boolean > {
2929 const req = context . switchToHttp ( ) . getRequest < any > ( ) ;
3030
31+ // ── @RequiresAdminKey() — env-based admin gate ───────────────────────────
32+ // Compared with constant-time equality to prevent timing attacks.
33+ // The key must be set in the ADMIN_KEY environment variable.
34+ const requiresAdmin = this . reflector . getAllAndOverride < boolean | undefined > (
35+ REQUIRES_ADMIN_KEY ,
36+ [ context . getHandler ( ) , context . getClass ( ) ] ,
37+ ) ;
38+ if ( requiresAdmin ) {
39+ const adminKey = process . env [ 'ADMIN_KEY' ] ;
40+ if ( ! adminKey ) {
41+ // Server misconfiguration — ADMIN_KEY not set
42+ throw new ForbiddenException ( 'Admin endpoint not configured (ADMIN_KEY missing)' ) ;
43+ }
44+ const provided : string = req . headers [ 'x-api-key' ] ?? '' ;
45+ // Constant-time comparison via Buffer to mitigate timing attacks
46+ const aLen = Buffer . byteLength ( adminKey ) ;
47+ const bLen = Buffer . byteLength ( provided ) ;
48+ const a = Buffer . alloc ( aLen , 0 ) ;
49+ const b = Buffer . alloc ( aLen , 0 ) ;
50+ Buffer . from ( adminKey ) . copy ( a ) ;
51+ Buffer . from ( provided . slice ( 0 , aLen ) ) . copy ( b ) ;
52+ const match = a . equals ( b ) && aLen === bLen ;
53+ if ( ! match ) {
54+ throw new UnauthorizedException ( {
55+ message : 'Admin key required' ,
56+ hint : 'Send your ADMIN_KEY as header: x-api-key: <admin-key>' ,
57+ } ) ;
58+ }
59+ return true ;
60+ }
61+
3162 // ── Determine mode from metadata ────────────────────────────────────────
3263 const minTier = this . reflector . getAllAndOverride < ApiKeyTier | undefined > (
3364 REQUIRES_TIER_KEY ,
0 commit comments