diff --git a/src/cli/parse-duration.ts b/src/cli/parse-duration.ts index 69efb1abe..29adced73 100644 --- a/src/cli/parse-duration.ts +++ b/src/cli/parse-duration.ts @@ -31,3 +31,9 @@ export function parseDurationMs(raw: string, opts?: DurationMsParseOptions): num if (!Number.isFinite(ms)) throw new Error(`invalid duration: ${raw}`); return ms; } + +/** + * Alias for parseDurationMs + * @deprecated Use parseDurationMs instead + */ +export const parseDuration = parseDurationMs; diff --git a/src/cli/security-cli.ts b/src/cli/security-cli.ts index 0031ff2f0..c738c8b62 100644 --- a/src/cli/security-cli.ts +++ b/src/cli/security-cli.ts @@ -167,18 +167,32 @@ export function registerSecurityCli(program: Command) { const lines: string[] = []; lines.push(theme.heading("Security Shield Status")); lines.push(""); - lines.push(`Shield: ${enabled ? theme.success("ENABLED") : theme.error("DISABLED")}`); - lines.push(`Rate Limiting: ${rateLimitingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`); - lines.push(`Intrusion Detection: ${intrusionDetectionEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`); - lines.push(`Firewall Integration: ${firewallEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`); - lines.push(`Alerting: ${alertingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`); + lines.push( + `Shield: ${enabled ? theme.success("ENABLED") : theme.error("DISABLED")}`, + ); + lines.push( + `Rate Limiting: ${rateLimitingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`, + ); + lines.push( + `Intrusion Detection: ${intrusionDetectionEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`, + ); + lines.push( + `Firewall Integration: ${firewallEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`, + ); + lines.push( + `Alerting: ${alertingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`, + ); if (alertingEnabled && cfg.security?.alerting?.channels?.telegram?.enabled) { lines.push(` Telegram: ${theme.success("ENABLED")}`); } lines.push(""); - lines.push(theme.muted(`Docs: ${formatDocsLink("/security/shield", "docs.openclaw.ai/security/shield")}`)); + lines.push( + theme.muted( + `Docs: ${formatDocsLink("/security/shield", "docs.openclaw.ai/security/shield")}`, + ), + ); defaultRuntime.log(lines.join("\n")); }); @@ -194,7 +208,11 @@ export function registerSecurityCli(program: Command) { await writeConfigFile(cfg); defaultRuntime.log(theme.success("✓ Security shield enabled")); - defaultRuntime.log(theme.muted(` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`)); + defaultRuntime.log( + theme.muted( + ` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`, + ), + ); }); // openclaw security disable @@ -211,7 +229,11 @@ export function registerSecurityCli(program: Command) { cfg.security.shield.enabled = false; await writeConfigFile(cfg); defaultRuntime.log(theme.warn("⚠ Security shield disabled")); - defaultRuntime.log(theme.muted(` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`)); + defaultRuntime.log( + theme.muted( + ` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`, + ), + ); }); // openclaw security logs @@ -279,16 +301,14 @@ export function registerSecurityCli(program: Command) { }); // openclaw blocklist - const blocklist = program - .command("blocklist") - .description("Manage IP blocklist"); + const blocklist = program.command("blocklist").description("Manage IP blocklist"); blocklist .command("list") .description("List all blocked IPs") .option("--json", "Print JSON", false) .action(async (opts: { json?: boolean }) => { - const entries = ipManager.getBlocklist(); + const entries = ipManager.getBlockedIps(); if (opts.json) { defaultRuntime.log(JSON.stringify(entries, null, 2)); @@ -310,11 +330,13 @@ export function registerSecurityCli(program: Command) { const hours = Math.floor(remaining / (1000 * 60 * 60)); const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60)); - defaultRuntime.log(`${theme.bold(entry.ip)}`); + defaultRuntime.log(`${theme.heading(entry.ip)}`); defaultRuntime.log(` Reason: ${entry.reason}`); defaultRuntime.log(` Source: ${entry.source}`); defaultRuntime.log(` Blocked: ${new Date(entry.blockedAt).toLocaleString()}`); - defaultRuntime.log(` Expires: ${expiresAt.toLocaleString()} (${hours}h ${minutes}m remaining)`); + defaultRuntime.log( + ` Expires: ${expiresAt.toLocaleString()} (${hours}h ${minutes}m remaining)`, + ); defaultRuntime.log(""); } }); @@ -354,16 +376,14 @@ export function registerSecurityCli(program: Command) { }); // openclaw allowlist - const allowlist = program - .command("allowlist") - .description("Manage IP allowlist"); + const allowlist = program.command("allowlist").description("Manage IP allowlist"); allowlist .command("list") .description("List all allowed IPs") .option("--json", "Print JSON", false) .action(async (opts: { json?: boolean }) => { - const entries = ipManager.getAllowlist(); + const entries = ipManager.getAllowedIps(); if (opts.json) { defaultRuntime.log(JSON.stringify(entries, null, 2)); @@ -379,7 +399,7 @@ export function registerSecurityCli(program: Command) { defaultRuntime.log(""); for (const entry of entries) { - defaultRuntime.log(`${theme.bold(entry.ip)}`); + defaultRuntime.log(`${theme.heading(entry.ip)}`); defaultRuntime.log(` Reason: ${entry.reason}`); defaultRuntime.log(` Source: ${entry.source}`); defaultRuntime.log(` Added: ${new Date(entry.addedAt).toLocaleString()}`); diff --git a/src/config/types.security.ts b/src/config/types.security.ts index b221b802d..0bbaf8980 100644 --- a/src/config/types.security.ts +++ b/src/config/types.security.ts @@ -97,7 +97,12 @@ export interface AlertingConfig { /** Alert triggers */ triggers?: { criticalEvents?: AlertTriggerConfig; - failedAuthSpike?: { enabled?: boolean; threshold?: number; windowMs?: number; throttleMs?: number }; + failedAuthSpike?: { + enabled?: boolean; + threshold?: number; + windowMs?: number; + throttleMs?: number; + }; ipBlocked?: AlertTriggerConfig; }; diff --git a/src/security/alerting/manager.ts b/src/security/alerting/manager.ts index 91cb999d0..aef1aa3df 100644 --- a/src/security/alerting/manager.ts +++ b/src/security/alerting/manager.ts @@ -24,11 +24,11 @@ export class AlertManager { private initializeChannels(): void { // Telegram channel - if (this.config.channels.telegram?.enabled) { + if (this.config.channels?.telegram?.enabled) { const telegram = new TelegramAlertChannel({ enabled: true, - botToken: this.config.channels.telegram.botToken, - chatId: this.config.channels.telegram.chatId, + botToken: this.config.channels.telegram.botToken ?? "", + chatId: this.config.channels.telegram.chatId ?? "", }); if (telegram.isEnabled()) { this.channels.push(telegram); @@ -47,7 +47,7 @@ export class AlertManager { * Check if alerting is enabled */ isEnabled(): boolean { - return this.config.enabled && this.channels.length > 0; + return (this.config.enabled ?? false) && this.channels.length > 0; } /** @@ -71,20 +71,17 @@ export class AlertManager { } // Send to all channels - const results = await Promise.allSettled( - this.channels.map((channel) => channel.send(alert)), - ); + const results = await Promise.allSettled(this.channels.map((channel) => channel.send(alert))); // Log results let successCount = 0; - let failureCount = 0; + let _failureCount = 0; for (const result of results) { if (result.status === "fulfilled" && result.value.ok) { successCount++; } else { - failureCount++; - const error = - result.status === "fulfilled" ? result.value.error : String(result.reason); + _failureCount++; + const error = result.status === "fulfilled" ? result.value.error : String(result.reason); log.error(`alert send failed: ${error}`); } } @@ -105,10 +102,7 @@ export class AlertManager { } // Critical events - if ( - event.severity === "critical" && - this.config.triggers.criticalEvents?.enabled - ) { + if (event.severity === "critical" && this.config.triggers?.criticalEvents?.enabled) { await this.sendAlert({ id: randomUUID(), severity: "critical", @@ -126,10 +120,7 @@ export class AlertManager { } // IP blocked - if ( - event.action === SecurityActions.IP_BLOCKED && - this.config.triggers.ipBlocked?.enabled - ) { + if (event.action === SecurityActions.IP_BLOCKED && this.config.triggers?.ipBlocked?.enabled) { await this.sendAlert({ id: randomUUID(), severity: "warn", @@ -146,14 +137,14 @@ export class AlertManager { } // Intrusion detected - if ( - [ - SecurityActions.BRUTE_FORCE_DETECTED, - SecurityActions.SSRF_BYPASS_ATTEMPT, - SecurityActions.PATH_TRAVERSAL_ATTEMPT, - SecurityActions.PORT_SCANNING_DETECTED, - ].includes(event.action) - ) { + const criticalActions = [ + SecurityActions.BRUTE_FORCE_DETECTED, + SecurityActions.SSRF_BYPASS_ATTEMPT, + SecurityActions.PATH_TRAVERSAL_ATTEMPT, + SecurityActions.PORT_SCANNING_DETECTED, + ] as const; + + if (criticalActions.includes(event.action as (typeof criticalActions)[number])) { const pattern = event.attackPattern || "unknown"; await this.sendAlert({ id: randomUUID(), @@ -164,7 +155,8 @@ export class AlertManager { details: { pattern, ip: event.ip, - attempts: event.details.failedAttempts || event.details.attempts || event.details.connections, + attempts: + event.details.failedAttempts || event.details.attempts || event.details.connections, threshold: event.details.threshold, }, trigger: "intrusion_detected", @@ -175,9 +167,9 @@ export class AlertManager { private getThrottleMs(trigger: string): number { switch (trigger) { case "critical_event": - return this.config.triggers.criticalEvents?.throttleMs || 0; + return this.config.triggers?.criticalEvents?.throttleMs || 0; case "ip_blocked": - return this.config.triggers.ipBlocked?.throttleMs || 0; + return this.config.triggers?.ipBlocked?.throttleMs || 0; case "intrusion_detected": return 300_000; // 5 minutes default default: diff --git a/src/security/alerting/types.ts b/src/security/alerting/types.ts index ee43ce3ef..151e8123f 100644 --- a/src/security/alerting/types.ts +++ b/src/security/alerting/types.ts @@ -31,26 +31,44 @@ export interface AlertChannelInterface { } export interface AlertTriggerConfig { - enabled: boolean; + enabled?: boolean; throttleMs?: number; } export interface AlertingConfig { - enabled: boolean; - triggers: { + enabled?: boolean; + triggers?: { criticalEvents?: AlertTriggerConfig; - failedAuthSpike?: AlertTriggerConfig & { threshold: number; windowMs: number }; + failedAuthSpike?: AlertTriggerConfig & { threshold?: number; windowMs?: number }; ipBlocked?: AlertTriggerConfig; }; - channels: { + channels?: { telegram?: { - enabled: boolean; - botToken: string; - chatId: string; + enabled?: boolean; + botToken?: string; + chatId?: string; }; webhook?: { - enabled: boolean; - url: string; + enabled?: boolean; + url?: string; + }; + slack?: { + enabled?: boolean; + webhookUrl?: string; + }; + email?: { + enabled?: boolean; + smtp?: { + host?: string; + port?: number; + secure?: boolean; + auth?: { + user?: string; + pass?: string; + }; + }; + from?: string; + to?: string[]; }; }; } diff --git a/src/security/events/aggregator.ts b/src/security/events/aggregator.ts index 5123732d0..5478171a9 100644 --- a/src/security/events/aggregator.ts +++ b/src/security/events/aggregator.ts @@ -3,7 +3,7 @@ * Aggregates events over time windows for alerting and intrusion detection */ -import type { SecurityEvent, SecurityEventCategory, SecurityEventSeverity } from "./schema.js"; +import type { SecurityEvent } from "./schema.js"; /** * Event count within a time window @@ -59,9 +59,7 @@ export class SecurityEventAggregator { } // Filter out events outside the time window - count.events = count.events.filter( - (e) => new Date(e.timestamp).getTime() > windowStart - ); + count.events = count.events.filter((e) => new Date(e.timestamp).getTime() > windowStart); // Add new event count.events.push(event); @@ -80,10 +78,7 @@ export class SecurityEventAggregator { /** * Get event count for a key within a window */ - getCount(params: { - key: string; - windowMs: number; - }): number { + getCount(params: { key: string; windowMs: number }): number { const { key, windowMs } = params; const count = this.eventCounts.get(key); @@ -94,7 +89,7 @@ export class SecurityEventAggregator { // Filter events in window const eventsInWindow = count.events.filter( - (e) => new Date(e.timestamp).getTime() > windowStart + (e) => new Date(e.timestamp).getTime() > windowStart, ); return eventsInWindow.length; @@ -103,10 +98,7 @@ export class SecurityEventAggregator { /** * Get aggregated events for a key */ - getEvents(params: { - key: string; - windowMs?: number; - }): SecurityEvent[] { + getEvents(params: { key: string; windowMs?: number }): SecurityEvent[] { const { key, windowMs } = params; const count = this.eventCounts.get(key); @@ -119,9 +111,7 @@ export class SecurityEventAggregator { const now = Date.now(); const windowStart = now - windowMs; - return count.events.filter( - (e) => new Date(e.timestamp).getTime() > windowStart - ); + return count.events.filter((e) => new Date(e.timestamp).getTime() > windowStart); } /** diff --git a/src/security/events/logger.ts b/src/security/events/logger.ts index 539bf634d..20a6de130 100644 --- a/src/security/events/logger.ts +++ b/src/security/events/logger.ts @@ -7,9 +7,13 @@ import fs from "node:fs"; import path from "node:path"; import { randomUUID } from "node:crypto"; -import type { SecurityEvent, SecurityEventSeverity, SecurityEventCategory, SecurityEventOutcome } from "./schema.js"; -import { DEFAULT_LOG_DIR } from "../../logging/logger.js"; -import { getChildLogger } from "../../logging/index.js"; +import type { + SecurityEvent, + SecurityEventSeverity, + SecurityEventCategory, + SecurityEventOutcome, +} from "./schema.js"; +import { DEFAULT_LOG_DIR, getChildLogger } from "../../logging/logger.js"; import { getAlertManager } from "../alerting/manager.js"; const SECURITY_LOG_PREFIX = "security"; @@ -229,7 +233,8 @@ class SecurityEventLogger { * Log event to main logger for OTEL export and console output */ private logToMainLogger(event: SecurityEvent): void { - const logMethod = event.severity === "critical" ? "error" : event.severity === "warn" ? "warn" : "info"; + const logMethod = + event.severity === "critical" ? "error" : event.severity === "warn" ? "warn" : "info"; this.logger[logMethod](`[${event.category}] ${event.action}`, { eventId: event.eventId, diff --git a/src/security/firewall/manager.ts b/src/security/firewall/manager.ts index 13351990e..debdb0e7f 100644 --- a/src/security/firewall/manager.ts +++ b/src/security/firewall/manager.ts @@ -41,7 +41,7 @@ export class FirewallManager { } else if (this.config.backend === "ufw") { this.backend = new UfwBackend(); } else { - return { ok: false, error: `unknown backend: ${this.config.backend}` }; + return { ok: false, error: `unknown backend: ${String(this.config.backend)}` }; } // Check availability @@ -196,9 +196,7 @@ let firewallManager: FirewallManager | null = null; /** * Initialize firewall manager with config */ -export async function initFirewallManager( - config: FirewallManagerConfig, -): Promise { +export async function initFirewallManager(config: FirewallManagerConfig): Promise { firewallManager = new FirewallManager(config); await firewallManager.initialize(); return firewallManager; diff --git a/src/security/intrusion-detector.test.ts b/src/security/intrusion-detector.test.ts index 30fd1ce93..ae0f83b8c 100644 --- a/src/security/intrusion-detector.test.ts +++ b/src/security/intrusion-detector.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable typescript-eslint/unbound-method */ import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; import { IntrusionDetector } from "./intrusion-detector.js"; import { SecurityActions, AttackPatterns, type SecurityEvent } from "./events/schema.js"; diff --git a/src/security/intrusion-detector.ts b/src/security/intrusion-detector.ts index a19d73010..a598372a2 100644 --- a/src/security/intrusion-detector.ts +++ b/src/security/intrusion-detector.ts @@ -47,16 +47,16 @@ export class IntrusionDetector { /** * Check for brute force attack pattern */ - checkBruteForce(params: { - ip: string; - event: SecurityEvent; - }): IntrusionDetectionResult { + checkBruteForce(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult { if (!this.config.enabled) { return { detected: false }; } const { ip, event } = params; const pattern = this.config.patterns.bruteForce; + if (!pattern || !pattern.threshold || !pattern.windowMs) { + return { detected: false }; + } const key = `brute_force:${ip}`; const crossed = securityEventAggregator.trackEvent({ @@ -99,16 +99,16 @@ export class IntrusionDetector { /** * Check for SSRF bypass attempts */ - checkSsrfBypass(params: { - ip: string; - event: SecurityEvent; - }): IntrusionDetectionResult { + checkSsrfBypass(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult { if (!this.config.enabled) { return { detected: false }; } const { ip, event } = params; const pattern = this.config.patterns.ssrfBypass; + if (!pattern || !pattern.threshold || !pattern.windowMs) { + return { detected: false }; + } const key = `ssrf_bypass:${ip}`; const crossed = securityEventAggregator.trackEvent({ @@ -148,16 +148,16 @@ export class IntrusionDetector { /** * Check for path traversal attempts */ - checkPathTraversal(params: { - ip: string; - event: SecurityEvent; - }): IntrusionDetectionResult { + checkPathTraversal(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult { if (!this.config.enabled) { return { detected: false }; } const { ip, event } = params; const pattern = this.config.patterns.pathTraversal; + if (!pattern || !pattern.threshold || !pattern.windowMs) { + return { detected: false }; + } const key = `path_traversal:${ip}`; const crossed = securityEventAggregator.trackEvent({ @@ -197,16 +197,16 @@ export class IntrusionDetector { /** * Check for port scanning */ - checkPortScanning(params: { - ip: string; - event: SecurityEvent; - }): IntrusionDetectionResult { + checkPortScanning(params: { ip: string; event: SecurityEvent }): IntrusionDetectionResult { if (!this.config.enabled) { return { detected: false }; } const { ip, event } = params; const pattern = this.config.patterns.portScanning; + if (!pattern || !pattern.threshold || !pattern.windowMs) { + return { detected: false }; + } const key = `port_scan:${ip}`; const crossed = securityEventAggregator.trackEvent({ diff --git a/src/security/ip-manager.test.ts b/src/security/ip-manager.test.ts index 618722f32..7cae4b00f 100644 --- a/src/security/ip-manager.test.ts +++ b/src/security/ip-manager.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; import { IpManager } from "./ip-manager.js"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; vi.mock("node:fs", () => ({ default: { diff --git a/src/security/ip-manager.ts b/src/security/ip-manager.ts index 157fcfb7c..8204348e0 100644 --- a/src/security/ip-manager.ts +++ b/src/security/ip-manager.ts @@ -250,7 +250,7 @@ export class IpManager { securityLogger.logIpManagement({ action: "firewall_block_failed", ip, - severity: "error", + severity: "critical", details: { error: String(err) }, }); }); @@ -282,7 +282,7 @@ export class IpManager { securityLogger.logIpManagement({ action: "firewall_unblock_failed", ip, - severity: "error", + severity: "critical", details: { error: String(err) }, }); }); @@ -295,11 +295,7 @@ export class IpManager { /** * Add IP to allowlist */ - allowIp(params: { - ip: string; - reason: string; - source?: "auto" | "manual"; - }): void { + allowIp(params: { ip: string; reason: string; source?: "auto" | "manual" }): void { const { ip, reason, source = "manual" } = params; // Check if already in allowlist diff --git a/src/security/middleware.ts b/src/security/middleware.ts index bed9f40a0..fb5274bae 100644 --- a/src/security/middleware.ts +++ b/src/security/middleware.ts @@ -24,7 +24,7 @@ export function createSecurityContext(req: IncomingMessage): SecurityContext { export function securityMiddleware( req: IncomingMessage, res: ServerResponse, - next: () => void + next: () => void, ): void { const shield = getSecurityShield(); @@ -48,7 +48,10 @@ export function securityMiddleware( if (!requestCheck.allowed) { res.statusCode = 429; res.setHeader("Content-Type", "text/plain"); - res.setHeader("Retry-After", String(Math.ceil((requestCheck.rateLimitInfo?.retryAfterMs ?? 60000) / 1000))); + res.setHeader( + "Retry-After", + String(Math.ceil((requestCheck.rateLimitInfo?.retryAfterMs ?? 60000) / 1000)), + ); res.end("Too Many Requests"); return; } @@ -83,7 +86,10 @@ export function checkConnectionRateLimit(req: IncomingMessage): { * Authentication rate limit check * Call this before processing authentication */ -export function checkAuthRateLimit(req: IncomingMessage, deviceId?: string): { +export function checkAuthRateLimit( + req: IncomingMessage, + deviceId?: string, +): { allowed: boolean; reason?: string; retryAfterMs?: number; @@ -130,11 +136,7 @@ export function logAuthFailure(req: IncomingMessage, reason: string, deviceId?: /** * Pairing rate limit check */ -export function checkPairingRateLimit(params: { - channel: string; - sender: string; - ip: string; -}): { +export function checkPairingRateLimit(params: { channel: string; sender: string; ip: string }): { allowed: boolean; reason?: string; } { @@ -155,11 +157,7 @@ export function checkPairingRateLimit(params: { /** * Webhook rate limit check */ -export function checkWebhookRateLimit(params: { - token: string; - path: string; - ip: string; -}): { +export function checkWebhookRateLimit(params: { token: string; path: string; ip: string }): { allowed: boolean; reason?: string; retryAfterMs?: number; diff --git a/src/security/shield.test.ts b/src/security/shield.test.ts index fa984320a..d48a11a46 100644 --- a/src/security/shield.test.ts +++ b/src/security/shield.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable typescript-eslint/unbound-method */ import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; import { SecurityShield, type SecurityContext } from "./shield.js"; import { rateLimiter } from "./rate-limiter.js"; diff --git a/src/security/shield.ts b/src/security/shield.ts index 61b367bd9..78c27e1b0 100644 --- a/src/security/shield.ts +++ b/src/security/shield.ts @@ -4,7 +4,7 @@ */ import type { IncomingMessage } from "node:http"; -import { rateLimiter, RateLimitKeys, type RateLimit, type RateLimitResult } from "./rate-limiter.js"; +import { rateLimiter, RateLimitKeys, type RateLimitResult } from "./rate-limiter.js"; import { ipManager } from "./ip-manager.js"; import { intrusionDetector } from "./intrusion-detector.js"; import { securityLogger } from "./events/logger.js"; @@ -36,9 +36,10 @@ export class SecurityShield { this.config = { enabled: config?.enabled ?? DEFAULT_SECURITY_CONFIG.shield.enabled, rateLimiting: config?.rateLimiting ?? DEFAULT_SECURITY_CONFIG.shield.rateLimiting, - intrusionDetection: config?.intrusionDetection ?? DEFAULT_SECURITY_CONFIG.shield.intrusionDetection, + intrusionDetection: + config?.intrusionDetection ?? DEFAULT_SECURITY_CONFIG.shield.intrusionDetection, ipManagement: config?.ipManagement ?? DEFAULT_SECURITY_CONFIG.shield.ipManagement, - }; + } as Required; } /** @@ -105,7 +106,11 @@ export class SecurityShield { } // Rate limit per-device (if deviceId provided) - if (ctx.deviceId && this.config.rateLimiting?.enabled && this.config.rateLimiting.perDevice?.authAttempts) { + if ( + ctx.deviceId && + this.config.rateLimiting?.enabled && + this.config.rateLimiting.perDevice?.authAttempts + ) { const limit = this.config.rateLimiting.perDevice.authAttempts; const result = rateLimiter.check(RateLimitKeys.authAttemptDevice(ctx.deviceId), limit); @@ -348,11 +353,7 @@ export class SecurityShield { /** * Check webhook rate limit */ - checkWebhook(params: { - token: string; - path: string; - ip: string; - }): SecurityCheckResult { + checkWebhook(params: { token: string; path: string; ip: string }): SecurityCheckResult { if (!this.config.enabled) { return { allowed: true }; } diff --git a/src/security/token-bucket.ts b/src/security/token-bucket.ts index 6e3676a45..16364f1b1 100644 --- a/src/security/token-bucket.ts +++ b/src/security/token-bucket.ts @@ -16,9 +16,7 @@ export class TokenBucket { private tokens: number; private lastRefillTime: number; - constructor( - private readonly config: TokenBucketConfig - ) { + constructor(private readonly config: TokenBucketConfig) { this.tokens = config.capacity; this.lastRefillTime = Date.now(); } @@ -86,10 +84,7 @@ export class TokenBucket { /** * Create a token bucket from max/window configuration */ -export function createTokenBucket(params: { - max: number; - windowMs: number; -}): TokenBucket { +export function createTokenBucket(params: { max: number; windowMs: number }): TokenBucket { const { max, windowMs } = params; // Refill rate: max tokens over windowMs