feat: Add configurable console log stripping and SSE debug headers
- commit
- ce139e1fbc8b880163646092a8ea8730d1aec0ea
- author
- Kit · · 2025-02-14 16:36 UTC
- parent
- e82fe7b
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe 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 }) => { | |||
| 59 | 59 | "/api/session/validate", | |
| 60 | 60 | "/api/register", | |
| 61 | 61 | "/login", | |
| 62 | - | "/register" | |
| 62 | + | "/register", | |
| 63 | + | "/api/sse" | |
| 63 | 64 | ]; | |
| 64 | 65 | ||
| 65 | 66 | // Get session token from cookies | |
| 66 | 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 | 79 | // Handle public requests separately: always clear session | |
| 69 | 80 | if (isPublicChatRequest || isPublicRoomRequest) { | |
| 70 | 81 | console.debug("Public request accessed:", event.url.pathname); | |
| 71 | 82 | event.locals.user = null; | |
| 72 | 83 | 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 | 88 | // Handle public routes: if token exists, validate and propagate | |
| @@ -85,7 +97,8 @@ export const handle: Handle = async ({ event, resolve }) => { | |||
| 85 | 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 | 104 | // Protected routes: ensure authentication | |
| @@ -93,7 +106,8 @@ export const handle: Handle = async ({ event, resolve }) => { | |||
| 93 | 106 | console.debug("No session token found"); | |
| 94 | 107 | event.locals.user = null; | |
| 95 | 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 | 113 | const { session, user } = await validateSessionToken(token); | |
| @@ -102,12 +116,14 @@ export const handle: Handle = async ({ event, resolve }) => { | |||
| 102 | 116 | setSessionTokenCookie(event, token, session.expiresAt); | |
| 103 | 117 | event.locals.session = session; | |
| 104 | 118 | event.locals.user = user; | |
| 105 | - | return resolve(event); | |
| 119 | + | const response = await resolve(event); | |
| 120 | + | return addDebugHeaders(response); | |
| 106 | 121 | } else { | |
| 107 | 122 | console.debug("Invalid session token"); | |
| 108 | 123 | deleteSessionTokenCookie(event); | |
| 109 | 124 | event.locals.session = null; | |
| 110 | 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 | }; ⏎̸ | |
| @@ -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 cookies | 66 | // 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 session | 79 | // 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 session | 83 | 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 propagate | 88 | // 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 authentication | 104 | // 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 | |||
| 133 | 133 | const { limited, retryAfter } = isRateLimited(ip, endpointType, isAuthenticated); | |
| 134 | 134 | ||
| 135 | 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 | 147 | // Construct a user-friendly error message | |
| 139 | 148 | let errorMessage = 'Too many requests. '; | |
| @@ -151,12 +160,22 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n | |||
| 151 | 160 | error: errorMessage, | |
| 152 | 161 | retryAfter: Math.ceil((retryAfter || 0) / 1000), | |
| 153 | 162 | 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 | + | } | |
| 155 | 173 | }), { | |
| 156 | 174 | status: 429, | |
| 157 | 175 | headers: { | |
| 158 | 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 | } | |
| @@ -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 message | 147 | // 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 | - | 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 | + | } | |||
| 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 @@ | |||
| 1 | 1 | import { defineConfig } from 'vitest/config'; | |
| 2 | + | import { loadEnv } from 'vite'; | |
| 2 | 3 | import { sveltekit } from '@sveltejs/kit/vite'; | |
| 3 | 4 | ||
| 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`); | |
| 6 | 24 | ||
| 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}'] | |
| 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 | }); | |
| @@ -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 | }); | ||