main f6d8405

rate-limiter.ts RawHistoryFolder

1import type { RequestEvent } from '@sveltejs/kit';
2import { createLogger } from '$lib/utils/logger.server';
3
4const log = createLogger('rate-limiter');
5
6const isDev = import.meta.env.DEV;
7
8// Rate limit configuration by endpoint type
9const RATE_LIMITS = {
10	// Authentication endpoints (login, register)
11	auth: {
12		points: isDev ? 1000 : 20,
13		durationMs: 5 * 60 * 1000
14	},
15
16	// Public endpoints (public chat)
17	public: {
18		points: isDev ? 1000 : 30,
19		durationMs: 60 * 1000
20	},
21
22	// Protected endpoints (authenticated API calls)
23	protected: {
24		points: isDev ? 1000 : 100,
25		durationMs: 60 * 1000
26	}
27} as const;
28
29// In-memory store for rate limiting
30const ipRequests = new Map<string, Array<{ timestamp: number }>>();
31
32// Compute max duration across all rate limits for cleanup
33const MAX_RATE_LIMIT_DURATION = Math.max(
34	RATE_LIMITS.auth.durationMs,
35	RATE_LIMITS.public.durationMs,
36	RATE_LIMITS.protected.durationMs
37);
38
39// Lazy cleanup: remove stale entries on each check
40let lastCleanup = 0;
41
42function cleanupStaleEntries() {
43	const now = Date.now();
44	// Run cleanup at most once per max duration window
45	if (now - lastCleanup < MAX_RATE_LIMIT_DURATION) return;
46	lastCleanup = now;
47
48	for (const [ip, timestamps] of ipRequests.entries()) {
49		const filtered = timestamps.filter((t) => now - t.timestamp < MAX_RATE_LIMIT_DURATION);
50		if (filtered.length === 0) {
51			ipRequests.delete(ip);
52		} else {
53			ipRequests.set(ip, filtered);
54		}
55	}
56}
57
58function getEndpointType(pathname: string, isPublic: boolean): keyof typeof RATE_LIMITS {
59	if (pathname.startsWith('/api/session/login') || pathname.startsWith('/api/register')) {
60		return 'auth';
61	}
62	if (isPublic) {
63		return 'public';
64	}
65	return 'protected';
66}
67
68function isRateLimited(
69	ip: string,
70	endpointType: keyof typeof RATE_LIMITS
71): { limited: boolean; retryAfter?: number } {
72	cleanupStaleEntries();
73
74	// Skip rate limiting completely in development for localhost
75	if (isDev && (ip === '127.0.0.1' || ip === '::1' || ip === 'localhost')) {
76		return { limited: false };
77	}
78
79	const now = Date.now();
80	const { points, durationMs } = RATE_LIMITS[endpointType];
81	const requests = ipRequests.get(ip) || [];
82	const validRequests = requests.filter((req) => now - req.timestamp < durationMs);
83
84	log.debug(
85		`Rate limit check for ${ip} on ${endpointType}: ${validRequests.length}/${points} requests in the last ${durationMs / 1000}s`
86	);
87
88	if (validRequests.length >= points) {
89		const oldestRequest = validRequests[0];
90		const retryAfter = durationMs - (now - oldestRequest.timestamp);
91		return { limited: true, retryAfter };
92	}
93
94	validRequests.push({ timestamp: now });
95	ipRequests.set(ip, validRequests);
96
97	return { limited: false };
98}
99
100export async function handleRateLimit(event: RequestEvent): Promise<Response | null> {
101	const url = new URL(event.request.url);
102	const isPublic = url.searchParams.get('public') === 'true';
103
104	// Skip rate limiting for non-API routes
105	if (!url.pathname.startsWith('/api')) {
106		return null;
107	}
108
109	const ip = event.getClientAddress();
110
111	const endpointType = getEndpointType(url.pathname, isPublic);
112	const { limited, retryAfter } = isRateLimited(ip, endpointType);
113
114	if (limited) {
115		// Construct a user-friendly error message
116		let errorMessage = 'Too many requests. ';
117		if (endpointType === 'auth') {
118			errorMessage += 'Please wait before attempting to log in again.';
119		} else {
120			errorMessage += 'Please try again later.';
121		}
122
123		// Build response body - only include debug info in development
124		const responseBody: Record<string, unknown> = {
125			error: errorMessage,
126			retryAfter: Math.ceil((retryAfter || 0) / 1000)
127		};
128
129		// Only include potentially sensitive info in dev mode
130		if (isDev) {
131			responseBody.endpointType = endpointType;
132			responseBody.debug = {
133				hasUser: Boolean(event.locals.user),
134				hasSession: Boolean(event.locals.session),
135				endpointType,
136				rateLimitPoints: RATE_LIMITS[endpointType].points
137			};
138		}
139
140		// Build headers - only include debug headers in development
141		const headers: Record<string, string> = {
142			'Content-Type': 'application/json',
143			'Retry-After': Math.ceil((retryAfter || 0) / 1000).toString()
144		};
145
146		if (isDev) {
147			headers['X-Debug-Has-User'] = Boolean(event.locals.user).toString();
148			headers['X-Debug-Has-Session'] = Boolean(event.locals.session).toString();
149			headers['X-Debug-Endpoint-Type'] = endpointType;
150			headers['X-Debug-Rate-Limit-Points'] = RATE_LIMITS[endpointType].points.toString();
151		}
152
153		return new Response(JSON.stringify(responseBody), {
154			status: 429,
155			headers
156		});
157	}
158
159	return null;
160}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close