mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
committed by
GitHub
parent
08e18867fd
commit
3d75fb3fae
@@ -14,6 +14,13 @@ import { checkBudget } from "@/domain/costRules";
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||
import * as log from "@/sse/utils/logger";
|
||||
import { checkRateLimit, RateLimitRule } from "./rateLimiter";
|
||||
|
||||
const DEFAULT_RATE_LIMITS: RateLimitRule[] = [
|
||||
{ limit: 1000, window: 86400 }, // 1000 per day
|
||||
{ limit: 5000, window: 604800 }, // 5000 per week
|
||||
{ limit: 20000, window: 2592000 } // 20000 per month
|
||||
];
|
||||
|
||||
interface AccessSchedule {
|
||||
enabled: boolean;
|
||||
@@ -34,10 +41,13 @@ export interface ApiKeyMetadata {
|
||||
budget?: number;
|
||||
usedBudget?: number;
|
||||
isActive?: boolean;
|
||||
isBanned?: boolean;
|
||||
expiresAt?: string | null;
|
||||
accessSchedule?: AccessSchedule | null;
|
||||
maxRequestsPerDay?: number | null;
|
||||
maxRequestsPerMinute?: number | null;
|
||||
maxSessions?: number | null;
|
||||
rateLimits?: RateLimitRule[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,64 +116,7 @@ function isWithinSchedule(schedule: AccessSchedule): boolean {
|
||||
return localMinutes >= fromMinutes && localMinutes < untilMinutes;
|
||||
}
|
||||
|
||||
// ── In-memory request counter for per-key rate limits (#452) ──
|
||||
|
||||
/** Sliding-window request timestamps per API key */
|
||||
const _requestTimestamps = new Map<string, number[]>();
|
||||
const REQUEST_COUNTER_MAX_KEYS = 5000;
|
||||
const REQUEST_DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const REQUEST_MINUTE_MS = 60 * 1000;
|
||||
|
||||
/** Record a request and check per-key limits. Returns null if OK, or an error message. */
|
||||
function checkRequestCountLimits(
|
||||
apiKeyId: string,
|
||||
maxPerDay: number | null | undefined,
|
||||
maxPerMinute: number | null | undefined
|
||||
): string | null {
|
||||
if (!maxPerDay && !maxPerMinute) return null;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Get or create timestamp array for this key
|
||||
let timestamps = _requestTimestamps.get(apiKeyId);
|
||||
if (!timestamps) {
|
||||
timestamps = [];
|
||||
_requestTimestamps.set(apiKeyId, timestamps);
|
||||
// Prevent unbounded growth
|
||||
if (_requestTimestamps.size > REQUEST_COUNTER_MAX_KEYS) {
|
||||
const firstKey = _requestTimestamps.keys().next().value;
|
||||
if (firstKey) _requestTimestamps.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Prune timestamps older than 24h
|
||||
const dayAgo = now - REQUEST_DAY_MS;
|
||||
while (timestamps.length > 0 && timestamps[0] < dayAgo) {
|
||||
timestamps.shift();
|
||||
}
|
||||
|
||||
// Check per-minute limit (before recording this request)
|
||||
if (maxPerMinute && maxPerMinute > 0) {
|
||||
const minuteAgo = now - REQUEST_MINUTE_MS;
|
||||
const recentCount = timestamps.filter((t) => t >= minuteAgo).length;
|
||||
if (recentCount >= maxPerMinute) {
|
||||
return `Per-minute request limit exceeded (${maxPerMinute} RPM). Try again in a few seconds.`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check per-day limit
|
||||
if (maxPerDay && maxPerDay > 0) {
|
||||
if (timestamps.length >= maxPerDay) {
|
||||
return `Daily request limit exceeded (${maxPerDay} RPD). Resets in ${Math.ceil(
|
||||
(timestamps[0] + REQUEST_DAY_MS - now) / 60000
|
||||
)} minutes.`;
|
||||
}
|
||||
}
|
||||
|
||||
// All checks passed — record this request
|
||||
timestamps.push(now);
|
||||
return null;
|
||||
}
|
||||
// Legacy in-memory request counter has been replaced by Redis-backed multi-window rate limiter
|
||||
|
||||
export interface ApiKeyPolicyResult {
|
||||
/** API key string (null if no key provided) */
|
||||
@@ -222,7 +175,7 @@ export async function enforceApiKeyPolicy(
|
||||
return { apiKey, apiKeyInfo: null, rejection: null };
|
||||
}
|
||||
|
||||
// ── Check 1: is_active — hard block regardless of schedule ──
|
||||
// ── Check 1: is_active / is_banned ──
|
||||
if (apiKeyInfo.isActive === false) {
|
||||
return {
|
||||
apiKey,
|
||||
@@ -230,6 +183,25 @@ export async function enforceApiKeyPolicy(
|
||||
rejection: errorResponse(HTTP_STATUS.FORBIDDEN, "This API key is disabled"),
|
||||
};
|
||||
}
|
||||
if (apiKeyInfo.isBanned === true) {
|
||||
return {
|
||||
apiKey,
|
||||
apiKeyInfo,
|
||||
rejection: errorResponse(HTTP_STATUS.FORBIDDEN, "This API key is banned due to policy violations"),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Check 1.5: expires_at ──
|
||||
if (apiKeyInfo.expiresAt) {
|
||||
const expiry = new Date(apiKeyInfo.expiresAt).getTime();
|
||||
if (Date.now() > expiry) {
|
||||
return {
|
||||
apiKey,
|
||||
apiKeyInfo,
|
||||
rejection: errorResponse(HTTP_STATUS.FORBIDDEN, "This API key has expired"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Check 2: access_schedule — time-based access window ──
|
||||
if (apiKeyInfo.accessSchedule && apiKeyInfo.accessSchedule.enabled) {
|
||||
@@ -286,18 +258,31 @@ export async function enforceApiKeyPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Check 5: Request-count limits (#452) ──
|
||||
if (apiKeyInfo.id && (apiKeyInfo.maxRequestsPerDay || apiKeyInfo.maxRequestsPerMinute)) {
|
||||
const limitError = checkRequestCountLimits(
|
||||
apiKeyInfo.id,
|
||||
apiKeyInfo.maxRequestsPerDay,
|
||||
apiKeyInfo.maxRequestsPerMinute
|
||||
);
|
||||
if (limitError) {
|
||||
// ── Check 5: Generic Multi-Window Rate Limits ──
|
||||
if (apiKeyInfo.id) {
|
||||
const rulesToApply = (apiKeyInfo.rateLimits && apiKeyInfo.rateLimits.length > 0)
|
||||
? [...apiKeyInfo.rateLimits]
|
||||
: [...DEFAULT_RATE_LIMITS];
|
||||
|
||||
// Combine with legacy limits if they exist and custom rate limits aren't set
|
||||
if (!apiKeyInfo.rateLimits || apiKeyInfo.rateLimits.length === 0) {
|
||||
if (apiKeyInfo.maxRequestsPerDay) {
|
||||
rulesToApply.push({ limit: apiKeyInfo.maxRequestsPerDay, window: 86400 });
|
||||
}
|
||||
if (apiKeyInfo.maxRequestsPerMinute) {
|
||||
rulesToApply.push({ limit: apiKeyInfo.maxRequestsPerMinute, window: 60 });
|
||||
}
|
||||
}
|
||||
|
||||
const rateLimitResult = await checkRateLimit(apiKeyInfo.id, rulesToApply);
|
||||
if (!rateLimitResult.allowed) {
|
||||
const failedWindowStr = rateLimitResult.failedWindow
|
||||
? ` (${rateLimitResult.failedWindow}s window)`
|
||||
: "";
|
||||
return {
|
||||
apiKey,
|
||||
apiKeyInfo,
|
||||
rejection: errorResponse(HTTP_STATUS.RATE_LIMITED, limitError),
|
||||
rejection: errorResponse(HTTP_STATUS.RATE_LIMITED, `Request limit exceeded${failedWindowStr}. Please try again later.`),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user