detached ce139e1

feat: Add configurable console log stripping and SSE debug headers

commit
ce139e1fbc8b880163646092a8ea8730d1aec0ea
author
Kit · · 2025-02-14 16:36 UTC
parent
e82fe7b
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
3 files · +77 -23
M src/hooks.server.ts +22 -6
@@ -59,18 +59,30 @@ export const handle: Handle = async ({ event, resolve }) => {
5959 "/api/session/validate",
6060 "/api/register",
6161 "/login",
62- "/register"
62+ "/register",
63+ "/api/sse"
6364 ];
6465
6566 // Get session token from cookies
6667 const token = event.cookies.get("session") ?? null;
6768
69+ // Add debug headers for SSE requests
70+ const addDebugHeaders = (response: Response) => {
71+ if (event.url.pathname.startsWith('/api/sse')) {
72+ response.headers.set('X-Debug-Has-Token', Boolean(token).toString());
73+ response.headers.set('X-Debug-Token-Length', token ? token.length.toString() : '0');
74+ response.headers.set('X-Debug-Cookie-Count', event.cookies.getAll().length.toString());
75+ }
76+ return response;
77+ };
78+
6879 // Handle public requests separately: always clear session
6980 if (isPublicChatRequest || isPublicRoomRequest) {
7081 console.debug("Public request accessed:", event.url.pathname);
7182 event.locals.user = null;
7283 event.locals.session = null; // Public access doesn't require a session
73- return resolve(event);
84+ const response = await resolve(event);
85+ return addDebugHeaders(response);
7486 }
7587
7688 // Handle public routes: if token exists, validate and propagate
@@ -85,7 +97,8 @@ export const handle: Handle = async ({ event, resolve }) => {
8597 event.locals.user = user;
8698 }
8799 }
88- return resolve(event);
100+ const response = await resolve(event);
101+ return addDebugHeaders(response);
89102 }
90103
91104 // Protected routes: ensure authentication
@@ -93,7 +106,8 @@ export const handle: Handle = async ({ event, resolve }) => {
93106 console.debug("No session token found");
94107 event.locals.user = null;
95108 event.locals.session = null;
96- return new Response("Unauthorized", { status: 401 });
109+ const response = new Response("Unauthorized", { status: 401 });
110+ return addDebugHeaders(response);
97111 }
98112
99113 const { session, user } = await validateSessionToken(token);
@@ -102,12 +116,14 @@ export const handle: Handle = async ({ event, resolve }) => {
102116 setSessionTokenCookie(event, token, session.expiresAt);
103117 event.locals.session = session;
104118 event.locals.user = user;
105- return resolve(event);
119+ const response = await resolve(event);
120+ return addDebugHeaders(response);
106121 } else {
107122 console.debug("Invalid session token");
108123 deleteSessionTokenCookie(event);
109124 event.locals.session = null;
110125 event.locals.user = null;
111- return new Response("Unauthorized", { status: 401 });
126+ const response = new Response("Unauthorized", { status: 401 });
127+ return addDebugHeaders(response);
112128 }
113129 }; ⏎̸
@@ -59,18 +59,30 @@ export const handle: Handle = async ({ event, resolve }) => {
59 "/api/session/validate",59 "/api/session/validate",
60 "/api/register",60 "/api/register",
61 "/login",61 "/login",
62- "/register"62+ "/register",
63+ "/api/sse"
63 ];64 ];
64 65
65 // Get session token from cookies66 // Get session token from cookies
66 const token = event.cookies.get("session") ?? null;67 const token = event.cookies.get("session") ?? null;
67 68
69+ // Add debug headers for SSE requests
70+ const addDebugHeaders = (response: Response) => {
71+ if (event.url.pathname.startsWith('/api/sse')) {
72+ response.headers.set('X-Debug-Has-Token', Boolean(token).toString());
73+ response.headers.set('X-Debug-Token-Length', token ? token.length.toString() : '0');
74+ response.headers.set('X-Debug-Cookie-Count', event.cookies.getAll().length.toString());
75+ }
76+ return response;
77+ };
78+
68 // Handle public requests separately: always clear session79 // Handle public requests separately: always clear session
69 if (isPublicChatRequest || isPublicRoomRequest) {80 if (isPublicChatRequest || isPublicRoomRequest) {
70 console.debug("Public request accessed:", event.url.pathname);81 console.debug("Public request accessed:", event.url.pathname);
71 event.locals.user = null;82 event.locals.user = null;
72 event.locals.session = null; // Public access doesn't require a session83 event.locals.session = null; // Public access doesn't require a session
73- return resolve(event);84+ const response = await resolve(event);
85+ return addDebugHeaders(response);
74 }86 }
75 87
76 // Handle public routes: if token exists, validate and propagate88 // Handle public routes: if token exists, validate and propagate
@@ -85,7 +97,8 @@ export const handle: Handle = async ({ event, resolve }) => {
85 event.locals.user = user;97 event.locals.user = user;
86 }98 }
87 }99 }
88- return resolve(event);100+ const response = await resolve(event);
101+ return addDebugHeaders(response);
89 }102 }
90 103
91 // Protected routes: ensure authentication104 // Protected routes: ensure authentication
@@ -93,7 +106,8 @@ export const handle: Handle = async ({ event, resolve }) => {
93 console.debug("No session token found");106 console.debug("No session token found");
94 event.locals.user = null;107 event.locals.user = null;
95 event.locals.session = null;108 event.locals.session = null;
96- return new Response("Unauthorized", { status: 401 });109+ const response = new Response("Unauthorized", { status: 401 });
110+ return addDebugHeaders(response);
97 }111 }
98 112
99 const { session, user } = await validateSessionToken(token);113 const { session, user } = await validateSessionToken(token);
@@ -102,12 +116,14 @@ export const handle: Handle = async ({ event, resolve }) => {
102 setSessionTokenCookie(event, token, session.expiresAt);116 setSessionTokenCookie(event, token, session.expiresAt);
103 event.locals.session = session;117 event.locals.session = session;
104 event.locals.user = user;118 event.locals.user = user;
105- return resolve(event);119+ const response = await resolve(event);
120+ return addDebugHeaders(response);
106 } else {121 } else {
107 console.debug("Invalid session token");122 console.debug("Invalid session token");
108 deleteSessionTokenCookie(event);123 deleteSessionTokenCookie(event);
109 event.locals.session = null;124 event.locals.session = null;
110 event.locals.user = null;125 event.locals.user = null;
111- return new Response("Unauthorized", { status: 401 });126+ const response = new Response("Unauthorized", { status: 401 });
127+ return addDebugHeaders(response);
112 }128 }
113 }; ⏎̸129 }; ⏎̸
M src/lib/api/rate-limiter.ts +22 -3
@@ -133,7 +133,16 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
133133 const { limited, retryAfter } = isRateLimited(ip, endpointType, isAuthenticated);
134134
135135 if (limited) {
136- console.debug(`Rate limit hit for ${ip} on ${url.pathname} (${endpointType})`);
136+ // Create debug headers
137+ const debugHeaders = {
138+ 'X-Debug-Is-Authenticated': isAuthenticated.toString(),
139+ 'X-Debug-Has-User': Boolean(event.locals.user).toString(),
140+ 'X-Debug-Has-Session': Boolean(event.locals.session).toString(),
141+ 'X-Debug-Endpoint-Type': endpointType,
142+ 'X-Debug-Rate-Limit-Points': isAuthenticated
143+ ? RATE_LIMITS.sse.authenticated.points.toString()
144+ : RATE_LIMITS.sse.unauthenticated.points.toString(),
145+ };
137146
138147 // Construct a user-friendly error message
139148 let errorMessage = 'Too many requests. ';
@@ -151,12 +160,22 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
151160 error: errorMessage,
152161 retryAfter: Math.ceil((retryAfter || 0) / 1000),
153162 endpointType,
154- isAuthenticated
163+ isAuthenticated,
164+ debug: {
165+ isAuthenticated,
166+ hasUser: Boolean(event.locals.user),
167+ hasSession: Boolean(event.locals.session),
168+ endpointType,
169+ rateLimitPoints: isAuthenticated
170+ ? RATE_LIMITS.sse.authenticated.points
171+ : RATE_LIMITS.sse.unauthenticated.points
172+ }
155173 }), {
156174 status: 429,
157175 headers: {
158176 'Content-Type': 'application/json',
159- 'Retry-After': Math.ceil((retryAfter || 0) / 1000).toString()
177+ 'Retry-After': Math.ceil((retryAfter || 0) / 1000).toString(),
178+ ...debugHeaders
160179 }
161180 });
162181 }
@@ -133,7 +133,16 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
133 const { limited, retryAfter } = isRateLimited(ip, endpointType, isAuthenticated);133 const { limited, retryAfter } = isRateLimited(ip, endpointType, isAuthenticated);
134 134
135 if (limited) {135 if (limited) {
136- console.debug(`Rate limit hit for ${ip} on ${url.pathname} (${endpointType})`);136+ // Create debug headers
137+ const debugHeaders = {
138+ 'X-Debug-Is-Authenticated': isAuthenticated.toString(),
139+ 'X-Debug-Has-User': Boolean(event.locals.user).toString(),
140+ 'X-Debug-Has-Session': Boolean(event.locals.session).toString(),
141+ 'X-Debug-Endpoint-Type': endpointType,
142+ 'X-Debug-Rate-Limit-Points': isAuthenticated
143+ ? RATE_LIMITS.sse.authenticated.points.toString()
144+ : RATE_LIMITS.sse.unauthenticated.points.toString(),
145+ };
137 146
138 // Construct a user-friendly error message147 // Construct a user-friendly error message
139 let errorMessage = 'Too many requests. ';148 let errorMessage = 'Too many requests. ';
@@ -151,12 +160,22 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
151 error: errorMessage,160 error: errorMessage,
152 retryAfter: Math.ceil((retryAfter || 0) / 1000),161 retryAfter: Math.ceil((retryAfter || 0) / 1000),
153 endpointType,162 endpointType,
154- isAuthenticated163+ isAuthenticated,
164+ debug: {
165+ isAuthenticated,
166+ hasUser: Boolean(event.locals.user),
167+ hasSession: Boolean(event.locals.session),
168+ endpointType,
169+ rateLimitPoints: isAuthenticated
170+ ? RATE_LIMITS.sse.authenticated.points
171+ : RATE_LIMITS.sse.unauthenticated.points
172+ }
155 }), {173 }), {
156 status: 429,174 status: 429,
157 headers: {175 headers: {
158 'Content-Type': 'application/json',176 'Content-Type': 'application/json',
159- 'Retry-After': Math.ceil((retryAfter || 0) / 1000).toString()177+ 'Retry-After': Math.ceil((retryAfter || 0) / 1000).toString(),
178+ ...debugHeaders
160 }179 }
161 });180 });
162 }181 }
M vite.config.ts +33 -14
@@ -1,20 +1,39 @@
11 import { defineConfig } from 'vitest/config';
2+import { loadEnv } from 'vite';
23 import { sveltekit } from '@sveltejs/kit/vite';
34
4-export default defineConfig({
5- plugins: [sveltekit()],
5+// Define which console methods to strip based on LOG_LEVEL
6+const getConsoleFuncsToStrip = (logLevel: string) => {
7+ switch (logLevel?.toLowerCase()) {
8+ case 'error':
9+ return ['console.log', 'console.info', 'console.debug', 'console.warn'];
10+ case 'warn':
11+ return ['console.log', 'console.info', 'console.debug'];
12+ case 'info':
13+ return ['console.debug'];
14+ case 'debug':
15+ default:
16+ return [];
17+ }
18+};
19+
20+export default defineConfig(({ mode }) => {
21+ // Load env file based on `mode` value. mode can be 'development', 'production', etc.
22+ const env = loadEnv(mode, process.cwd(), '');
23+ console.log(`Building with LOG_LEVEL=${env.LOG_LEVEL || 'info'} in ${mode} mode`);
624
7- test: {
8- include: ['src/**/*.{test,spec}.{js,ts}']
9- },
10- build: {
11- minify: 'terser',
12- terserOptions: {
13- compress: {
14- pure_funcs: [
15- 'console.debug',
16- ]
17- }
25+ return {
26+ plugins: [sveltekit()],
27+ test: {
28+ include: ['src/**/*.{test,spec}.{js,ts}']
1829 },
19- }
30+ build: {
31+ minify: 'terser',
32+ terserOptions: {
33+ compress: {
34+ pure_funcs: getConsoleFuncsToStrip(env.LOG_LEVEL || 'info')
35+ }
36+ },
37+ }
38+ };
2039 });
@@ -1,20 +1,39 @@
1 import { defineConfig } from 'vitest/config';1 import { defineConfig } from 'vitest/config';
2+import { loadEnv } from 'vite';
2 import { sveltekit } from '@sveltejs/kit/vite';3 import { sveltekit } from '@sveltejs/kit/vite';
3 4
4-export default defineConfig({5+// Define which console methods to strip based on LOG_LEVEL
5- plugins: [sveltekit()],6+const getConsoleFuncsToStrip = (logLevel: string) => {
7+ switch (logLevel?.toLowerCase()) {
8+ case 'error':
9+ return ['console.log', 'console.info', 'console.debug', 'console.warn'];
10+ case 'warn':
11+ return ['console.log', 'console.info', 'console.debug'];
12+ case 'info':
13+ return ['console.debug'];
14+ case 'debug':
15+ default:
16+ return [];
17+ }
18+};
19+
20+export default defineConfig(({ mode }) => {
21+ // Load env file based on `mode` value. mode can be 'development', 'production', etc.
22+ const env = loadEnv(mode, process.cwd(), '');
23+ console.log(`Building with LOG_LEVEL=${env.LOG_LEVEL || 'info'} in ${mode} mode`);
6 24
7- test: {25+ return {
8- include: ['src/**/*.{test,spec}.{js,ts}']26+ plugins: [sveltekit()],
9- },27+ test: {
10- build: {28+ include: ['src/**/*.{test,spec}.{js,ts}']
11- minify: 'terser',
12- terserOptions: {
13- compress: {
14- pure_funcs: [
15- 'console.debug',
16- ]
17- }
18 },29 },
19- }30+ build: {
31+ minify: 'terser',
32+ terserOptions: {
33+ compress: {
34+ pure_funcs: getConsoleFuncsToStrip(env.LOG_LEVEL || 'info')
35+ }
36+ },
37+ }
38+ };
20 });39 });

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