main f6d8405

hooks.server.ts RawHistoryFolder

1import type { Handle } from '@sveltejs/kit';
2import { users } from '$lib/db/convex.server';
3import { handleRateLimit } from '$lib/api/rate-limiter';
4import { createLogger } from '$lib/utils/logger.server';
5import { ensureDefaultChatRoom } from '$lib/utils/chat.server';
6import { validateSessionToken } from '$lib/api/session.server';
7import { setSessionTokenCookie, deleteSessionTokenCookie } from '$lib/api/session.cookie';
8
9const log = createLogger('hooks-server');
10
11// Add security headers to response
12function addSecurityHeaders(response: Response): Response {
13	const headers = new Headers(response.headers);
14	headers.set('X-Frame-Options', 'DENY');
15	headers.set('X-Content-Type-Options', 'nosniff');
16	headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
17	headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
18	headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
19
20	return new Response(response.body, {
21		status: response.status,
22		statusText: response.statusText,
23		headers
24	});
25}
26
27// Set all users to offline on server start (using Convex)
28async function setAllUsersOffline() {
29	try {
30		const result = await users.setAllOffline();
31		log.info('All users set to offline on server start', { updated: result.updated });
32	} catch (error: unknown) {
33		log.error('Failed to set users offline on server start:', { error });
34	}
35}
36
37// Initialize server lazily on the first API request
38let initPromise: Promise<void> | null = null;
39let lastFailedAttempt = 0;
40let failureCount = 0;
41const BASE_COOLDOWN = 5000; // 5 seconds
42const MAX_COOLDOWN = 60000; // 1 minute max
43
44function initializeServer(): Promise<void> {
45	if (initPromise) return initPromise;
46
47	// Exponential backoff after failures
48	if (lastFailedAttempt > 0) {
49		const cooldown = Math.min(BASE_COOLDOWN * Math.pow(2, failureCount - 1), MAX_COOLDOWN);
50		if (Date.now() - lastFailedAttempt < cooldown) {
51			return Promise.resolve();
52		}
53	}
54
55	initPromise = (async () => {
56		try {
57			log.info('Initializing server...');
58			await ensureDefaultChatRoom();
59			await setAllUsersOffline();
60			log.info('Server initialized successfully');
61			lastFailedAttempt = 0;
62			failureCount = 0;
63		} catch (error: unknown) {
64			log.error('Failed to initialize server:', { error, failureCount: failureCount + 1 });
65			lastFailedAttempt = Date.now();
66			failureCount++;
67			initPromise = null;
68		}
69	})();
70
71	return initPromise;
72}
73
74export const handle: Handle = async ({ event, resolve }) => {
75	const path = event.url.pathname;
76
77	// Skip initialization for static assets and non-API routes
78	const needsInit = path.startsWith('/api/') && !path.startsWith('/api/health');
79	if (needsInit) {
80		await initializeServer();
81	}
82
83	log.debug('Handling request', { path });
84
85	// CSRF protection: validate Origin header for state-changing requests
86	if (event.request.method !== 'GET' && event.request.method !== 'HEAD') {
87		const origin = event.request.headers.get('origin');
88
89		if (!origin) {
90			// Browsers always send Origin on state-changing requests.
91			// Missing Origin means non-browser client (curl, scripts).
92			log.warn('CSRF check failed - missing origin header', {
93				method: event.request.method,
94				path: event.url.pathname
95			});
96			return new Response('Forbidden', { status: 403 });
97		}
98
99		try {
100			// event.url comes from ORIGIN in production, so proxies rewriting Host do not matter
101			const originUrl = new URL(origin);
102			if (originUrl.origin !== event.url.origin) {
103				log.warn('CSRF check failed - origin mismatch', {
104					origin: originUrl.origin,
105					expected: event.url.origin
106				});
107				return new Response('Forbidden', { status: 403 });
108			}
109		} catch {
110			log.warn('CSRF check failed - invalid origin', { origin });
111			return new Response('Forbidden', { status: 403 });
112		}
113	}
114
115	// Get session token from cookies
116	const token = event.cookies.get('session') ?? null;
117
118	// Validate session token if token exists
119	if (token) {
120		const { session, user } = await validateSessionToken(token);
121		if (session) {
122			log.debug('Valid session found', { user: user?.nickname });
123			setSessionTokenCookie(event, token, session.expiresAt);
124			event.locals.session = session;
125			event.locals.user = user;
126		} else {
127			log.debug('Invalid session token');
128			deleteSessionTokenCookie(event);
129			event.locals.session = null;
130			event.locals.user = null;
131		}
132	}
133
134	// Identify public chat requests if there is no valid session
135	const isPublicChatRequest =
136		event.url.pathname.startsWith('/api/chat/messages') && event.request.method === 'GET';
137
138	if (isPublicChatRequest && !event.locals.session) {
139		log.debug('Public request accessed', { path: event.url.pathname });
140		const response = await resolve(event);
141		return addSecurityHeaders(response);
142	}
143
144	// Public routes that don't require authentication
145	const publicRoutes = [
146		'/',
147		'/api/health',
148		'/api/session/login',
149		'/api/session/validate',
150		'/api/register',
151		'/login',
152		'/register'
153	];
154
155	// Handle public routes
156	if (publicRoutes.includes(event.url.pathname)) {
157		log.debug('Public route accessed', { path: event.url.pathname });
158		const response = await resolve(event);
159		return addSecurityHeaders(response);
160	}
161
162	// Apply rate limiting for non-public endpoints
163	if (!isPublicChatRequest) {
164		const rateLimitResult = await handleRateLimit(event);
165		if (rateLimitResult?.status === 429) {
166			return new Response('Too Many Requests', {
167				status: 429,
168				headers: {
169					'Retry-After': '60'
170				}
171			});
172		}
173	}
174
175	const response = await resolve(event);
176	return addSecurityHeaders(response);
177};

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