@@ -4,6 +4,7 @@ const SENSITIVE_KEY_RE = /(secret|token|password|api[_-]?key|authorization|signa
44const DEFAULT_AUDIT_RATE_LIMIT_MAX = 60 ;
55const DEFAULT_AUDIT_RATE_LIMIT_WINDOW_MS = 60_000 ;
66const DEFAULT_AUDIT_FIELD_MAX_LENGTH = 2048 ;
7+ const MAX_AUDIT_RATE_LIMIT_KEYS = 10_000 ;
78
89/**
910 * Allowlist of permitted audit action identifiers.
@@ -29,6 +30,19 @@ const ALLOWED_AUDIT_ACTIONS = new Set([
2930
3031const auditRateLimitState = new Map ( ) ;
3132
33+ function pruneExpiredAuditRateLimitEntries ( now , windowMs ) {
34+ let cleaned = 0 ;
35+
36+ for ( const [ key , state ] of auditRateLimitState . entries ( ) ) {
37+ if ( now >= state . windowStart + windowMs ) {
38+ auditRateLimitState . delete ( key ) ;
39+ cleaned += 1 ;
40+ }
41+ }
42+
43+ return cleaned ;
44+ }
45+
3246function stableStringify ( value , depth = 0 , seen = new WeakSet ( ) ) {
3347 if ( depth > 10 ) {
3448 return '"[Too Deep]"' ;
@@ -164,19 +178,17 @@ export function consumeAuditLogRateLimit(
164178) {
165179 if ( ! key ) return { allowed : true , remaining : max , resetTime : now + windowMs } ;
166180
181+ pruneExpiredAuditRateLimitEntries ( now , windowMs ) ;
182+
167183 // Evict expired entries if Map size exceeds safety threshold (DoS / OOM protection)
168- if ( auditRateLimitState . size >= 10000 ) {
169- for ( const [ k , v ] of auditRateLimitState . entries ( ) ) {
170- if ( now >= v . windowStart + windowMs ) {
171- auditRateLimitState . delete ( k ) ;
172- }
173- }
174- // Hard cap eviction if still over threshold
175- if ( auditRateLimitState . size >= 10000 ) {
176- const oldestKeys = Array . from ( auditRateLimitState . keys ( ) ) . slice ( 0 , 100 ) ;
177- for ( const k of oldestKeys ) {
178- auditRateLimitState . delete ( k ) ;
179- }
184+ if ( auditRateLimitState . size >= MAX_AUDIT_RATE_LIMIT_KEYS ) {
185+ const oldestKeys = Array . from ( auditRateLimitState . entries ( ) )
186+ . sort ( ( [ , a ] , [ , b ] ) => a . windowStart - b . windowStart )
187+ . slice ( 0 , Math . max ( 100 , Math . ceil ( auditRateLimitState . size * 0.1 ) ) )
188+ . map ( ( [ k ] ) => k ) ;
189+
190+ for ( const k of oldestKeys ) {
191+ auditRateLimitState . delete ( k ) ;
180192 }
181193 }
182194
@@ -208,17 +220,21 @@ export function consumeAuditLogRateLimit(
208220 * Get comprehensive rate limit statistics for audit logging (issue #902).
209221 * Useful for monitoring and debugging rate limit behavior.
210222 */
211- export function getAuditRateLimitStats ( ) {
212- const now = Date . now ( ) ;
223+ export function getAuditRateLimitStats ( { now = Date . now ( ) } = { } ) {
224+ const windowMs = Number (
225+ process . env . AUDIT_LOG_RATE_LIMIT_WINDOW_MS || DEFAULT_AUDIT_RATE_LIMIT_WINDOW_MS ,
226+ ) ;
227+ pruneExpiredAuditRateLimitEntries ( now , windowMs ) ;
228+
213229 const stats = {
214230 totalKeys : auditRateLimitState . size ,
215231 activeWindows : 0 ,
216232 expiredWindows : 0 ,
217233 maxRequestsPerWindow : Number ( process . env . AUDIT_LOG_RATE_LIMIT_MAX || DEFAULT_AUDIT_RATE_LIMIT_MAX ) ,
218- windowMs : Number ( process . env . AUDIT_LOG_RATE_LIMIT_WINDOW_MS || DEFAULT_AUDIT_RATE_LIMIT_WINDOW_MS ) ,
234+ windowMs,
219235 } ;
220236
221- for ( const [ key , state ] of auditRateLimitState . entries ( ) ) {
237+ for ( const [ , state ] of auditRateLimitState . entries ( ) ) {
222238 if ( now >= state . windowStart + stats . windowMs ) {
223239 stats . expiredWindows ++ ;
224240 } else {
@@ -233,8 +249,7 @@ export function getAuditRateLimitStats() {
233249 * Cleanup expired audit rate limit entries to prevent memory exhaustion (issue #902).
234250 * Should be called periodically (e.g., via cron or on a schedule).
235251 */
236- export function cleanupExpiredAuditRateLimits ( ) {
237- const now = Date . now ( ) ;
252+ export function cleanupExpiredAuditRateLimits ( { now = Date . now ( ) } = { } ) {
238253 const windowMs = Number (
239254 process . env . AUDIT_LOG_RATE_LIMIT_WINDOW_MS || DEFAULT_AUDIT_RATE_LIMIT_WINDOW_MS ,
240255 ) ;
0 commit comments