@@ -20,6 +20,7 @@ import { adminTokenManager } from "$lib/server/adminToken";
2020import { isHostLocalhost } from "$lib/server/isURLLocal" ;
2121import { MetricsServer } from "$lib/server/metrics" ;
2222import { loadMcpServersOnStartup } from "$lib/server/mcp/registry" ;
23+ import { runWithRequestContext } from "$lib/server/requestContext" ;
2324
2425export const init : ServerInit = async ( ) => {
2526 // Wait for config to be fully loaded
@@ -100,194 +101,200 @@ export const handleError: HandleServerError = async ({ error, event, status, mes
100101} ;
101102
102103export const handle : Handle = async ( { event, resolve } ) => {
103- await ready . then ( ( ) => {
104- config . checkForUpdates ( ) ;
105- } ) ;
104+ // Generate a unique request ID for this request
105+ const requestId = crypto . randomUUID ( ) ;
106106
107- logger . debug ( {
108- locals : event . locals ,
109- url : event . url . pathname ,
110- params : event . params ,
111- request : event . request ,
112- } ) ;
113-
114- function errorResponse ( status : number , message : string ) {
115- const sendJson =
116- event . request . headers . get ( "accept" ) ?. includes ( "application/json" ) ||
117- event . request . headers . get ( "content-type" ) ?. includes ( "application/json" ) ;
118- return new Response ( sendJson ? JSON . stringify ( { error : message } ) : message , {
119- status,
120- headers : {
121- "content-type" : sendJson ? "application/json" : "text/plain" ,
122- } ,
107+ // Run the entire request handling within the request context
108+ return runWithRequestContext ( async ( ) => {
109+ await ready . then ( ( ) => {
110+ config . checkForUpdates ( ) ;
123111 } ) ;
124- }
125112
126- if ( event . url . pathname . startsWith ( `${ base } /admin/` ) || event . url . pathname === `${ base } /admin` ) {
127- const ADMIN_SECRET = config . ADMIN_API_SECRET || config . PARQUET_EXPORT_SECRET ;
113+ logger . debug ( {
114+ locals : event . locals ,
115+ url : event . url . pathname ,
116+ params : event . params ,
117+ request : event . request ,
118+ } ) ;
128119
129- if ( ! ADMIN_SECRET ) {
130- return errorResponse ( 500 , "Admin API is not configured" ) ;
120+ function errorResponse ( status : number , message : string ) {
121+ const sendJson =
122+ event . request . headers . get ( "accept" ) ?. includes ( "application/json" ) ||
123+ event . request . headers . get ( "content-type" ) ?. includes ( "application/json" ) ;
124+ return new Response ( sendJson ? JSON . stringify ( { error : message } ) : message , {
125+ status,
126+ headers : {
127+ "content-type" : sendJson ? "application/json" : "text/plain" ,
128+ } ,
129+ } ) ;
131130 }
132131
133- if ( event . request . headers . get ( "Authorization" ) !== `Bearer ${ ADMIN_SECRET } ` ) {
134- return errorResponse ( 401 , "Unauthorized" ) ;
135- }
136- }
132+ if ( event . url . pathname . startsWith ( `${ base } /admin/` ) || event . url . pathname === `${ base } /admin` ) {
133+ const ADMIN_SECRET = config . ADMIN_API_SECRET || config . PARQUET_EXPORT_SECRET ;
137134
138- const auth = await authenticateRequest (
139- { type : "svelte" , value : event . request . headers } ,
140- { type : "svelte" , value : event . cookies } ,
141- event . url
142- ) ;
143-
144- event . locals . sessionId = auth . sessionId ;
145-
146- if ( loginEnabled && ! auth . user && ! event . url . pathname . startsWith ( `${ base } /.well-known/` ) ) {
147- if ( config . AUTOMATIC_LOGIN === "true" ) {
148- // AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
149- if (
150- ! event . url . pathname . startsWith ( `${ base } /login` ) &&
151- ! event . url . pathname . startsWith ( `${ base } /healthcheck` )
152- ) {
153- // To get the same CSRF token after callback
154- refreshSessionCookie ( event . cookies , auth . secretSessionId ) ;
155- return await triggerOauthFlow ( event ) ;
135+ if ( ! ADMIN_SECRET ) {
136+ return errorResponse ( 500 , "Admin API is not configured" ) ;
156137 }
157- } else {
158- // Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
159- if (
160- event . url . pathname !== `${ base } /` &&
161- event . url . pathname !== `${ base } ` &&
162- ! event . url . pathname . startsWith ( `${ base } /login` ) &&
163- ! event . url . pathname . startsWith ( `${ base } /login/callback` ) &&
164- ! event . url . pathname . startsWith ( `${ base } /healthcheck` ) &&
165- ! event . url . pathname . startsWith ( `${ base } /r/` ) &&
166- ! event . url . pathname . startsWith ( `${ base } /conversation/` ) &&
167- ! event . url . pathname . startsWith ( `${ base } /models/` ) &&
168- ! event . url . pathname . startsWith ( `${ base } /api` )
169- ) {
170- refreshSessionCookie ( event . cookies , auth . secretSessionId ) ;
171- return triggerOauthFlow ( event ) ;
138+
139+ if ( event . request . headers . get ( "Authorization" ) !== `Bearer ${ ADMIN_SECRET } ` ) {
140+ return errorResponse ( 401 , "Unauthorized" ) ;
172141 }
173142 }
174- }
175-
176- event . locals . user = auth . user || undefined ;
177- event . locals . token = auth . token ;
178-
179- event . locals . isAdmin =
180- event . locals . user ?. isAdmin || adminTokenManager . isAdmin ( event . locals . sessionId ) ;
181143
182- // CSRF protection
183- const requestContentType = event . request . headers . get ( "content-type" ) ?. split ( ";" ) [ 0 ] ?? "" ;
184- /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
185- const nativeFormContentTypes = [
186- "multipart/form-data" ,
187- "application/x-www-form-urlencoded" ,
188- "text/plain" ,
189- ] ;
190-
191- if ( event . request . method === "POST" ) {
192- if ( nativeFormContentTypes . includes ( requestContentType ) ) {
193- const origin = event . request . headers . get ( "origin" ) ;
144+ const auth = await authenticateRequest (
145+ { type : "svelte" , value : event . request . headers } ,
146+ { type : "svelte" , value : event . cookies } ,
147+ event . url
148+ ) ;
194149
195- if ( ! origin ) {
196- return errorResponse ( 403 , "Non-JSON form requests need to have an origin" ) ;
150+ event . locals . sessionId = auth . sessionId ;
151+
152+ if ( loginEnabled && ! auth . user && ! event . url . pathname . startsWith ( `${ base } /.well-known/` ) ) {
153+ if ( config . AUTOMATIC_LOGIN === "true" ) {
154+ // AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
155+ if (
156+ ! event . url . pathname . startsWith ( `${ base } /login` ) &&
157+ ! event . url . pathname . startsWith ( `${ base } /healthcheck` )
158+ ) {
159+ // To get the same CSRF token after callback
160+ refreshSessionCookie ( event . cookies , auth . secretSessionId ) ;
161+ return await triggerOauthFlow ( event ) ;
162+ }
163+ } else {
164+ // Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
165+ if (
166+ event . url . pathname !== `${ base } /` &&
167+ event . url . pathname !== `${ base } ` &&
168+ ! event . url . pathname . startsWith ( `${ base } /login` ) &&
169+ ! event . url . pathname . startsWith ( `${ base } /login/callback` ) &&
170+ ! event . url . pathname . startsWith ( `${ base } /healthcheck` ) &&
171+ ! event . url . pathname . startsWith ( `${ base } /r/` ) &&
172+ ! event . url . pathname . startsWith ( `${ base } /conversation/` ) &&
173+ ! event . url . pathname . startsWith ( `${ base } /models/` ) &&
174+ ! event . url . pathname . startsWith ( `${ base } /api` )
175+ ) {
176+ refreshSessionCookie ( event . cookies , auth . secretSessionId ) ;
177+ return triggerOauthFlow ( event ) ;
178+ }
197179 }
180+ }
198181
199- const validOrigins = [
200- new URL ( event . request . url ) . host ,
201- ...( config . PUBLIC_ORIGIN ? [ new URL ( config . PUBLIC_ORIGIN ) . host ] : [ ] ) ,
202- ] ;
203-
204- if ( ! validOrigins . includes ( new URL ( origin ) . host ) ) {
205- return errorResponse ( 403 , "Invalid referer for POST request" ) ;
182+ event . locals . user = auth . user || undefined ;
183+ event . locals . token = auth . token ;
184+
185+ event . locals . isAdmin =
186+ event . locals . user ?. isAdmin || adminTokenManager . isAdmin ( event . locals . sessionId ) ;
187+
188+ // CSRF protection
189+ const requestContentType = event . request . headers . get ( "content-type" ) ?. split ( ";" ) [ 0 ] ?? "" ;
190+ /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
191+ const nativeFormContentTypes = [
192+ "multipart/form-data" ,
193+ "application/x-www-form-urlencoded" ,
194+ "text/plain" ,
195+ ] ;
196+
197+ if ( event . request . method === "POST" ) {
198+ if ( nativeFormContentTypes . includes ( requestContentType ) ) {
199+ const origin = event . request . headers . get ( "origin" ) ;
200+
201+ if ( ! origin ) {
202+ return errorResponse ( 403 , "Non-JSON form requests need to have an origin" ) ;
203+ }
204+
205+ const validOrigins = [
206+ new URL ( event . request . url ) . host ,
207+ ...( config . PUBLIC_ORIGIN ? [ new URL ( config . PUBLIC_ORIGIN ) . host ] : [ ] ) ,
208+ ] ;
209+
210+ if ( ! validOrigins . includes ( new URL ( origin ) . host ) ) {
211+ return errorResponse ( 403 , "Invalid referer for POST request" ) ;
212+ }
206213 }
207214 }
208- }
209215
210- if (
211- event . request . method === "POST" ||
212- event . url . pathname . startsWith ( `${ base } /login` ) ||
213- event . url . pathname . startsWith ( `${ base } /login/callback` )
214- ) {
215- // if the request is a POST request or login-related we refresh the cookie
216- refreshSessionCookie ( event . cookies , auth . secretSessionId ) ;
217-
218- await collections . sessions . updateOne (
219- { sessionId : auth . sessionId } ,
220- { $set : { updatedAt : new Date ( ) , expiresAt : addWeeks ( new Date ( ) , 2 ) } }
221- ) ;
222- }
223-
224- if (
225- loginEnabled &&
226- ! event . locals . user &&
227- ! event . url . pathname . startsWith ( `${ base } /login` ) &&
228- ! event . url . pathname . startsWith ( `${ base } /admin` ) &&
229- ! event . url . pathname . startsWith ( `${ base } /settings` ) &&
230- ! [ "GET" , "OPTIONS" , "HEAD" ] . includes ( event . request . method )
231- ) {
232- return errorResponse ( 401 , ERROR_MESSAGES . authOnly ) ;
233- }
234-
235- let replaced = false ;
216+ if (
217+ event . request . method === "POST" ||
218+ event . url . pathname . startsWith ( `${ base } /login` ) ||
219+ event . url . pathname . startsWith ( `${ base } /login/callback` )
220+ ) {
221+ // if the request is a POST request or login-related we refresh the cookie
222+ refreshSessionCookie ( event . cookies , auth . secretSessionId ) ;
236223
237- const response = await resolve ( event , {
238- transformPageChunk : ( chunk ) => {
239- // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
240- if ( replaced || ! chunk . html . includes ( "%gaId%" ) ) {
241- return chunk . html ;
242- }
243- replaced = true ;
224+ await collections . sessions . updateOne (
225+ { sessionId : auth . sessionId } ,
226+ { $set : { updatedAt : new Date ( ) , expiresAt : addWeeks ( new Date ( ) , 2 ) } }
227+ ) ;
228+ }
244229
245- return chunk . html . replace ( "%gaId%" , config . PUBLIC_GOOGLE_ANALYTICS_ID ) ;
246- } ,
247- filterSerializedResponseHeaders : ( header ) => {
248- return header . includes ( "content-type" ) ;
249- } ,
250- } ) ;
230+ if (
231+ loginEnabled &&
232+ ! event . locals . user &&
233+ ! event . url . pathname . startsWith ( `${ base } /login` ) &&
234+ ! event . url . pathname . startsWith ( `${ base } /admin` ) &&
235+ ! event . url . pathname . startsWith ( `${ base } /settings` ) &&
236+ ! [ "GET" , "OPTIONS" , "HEAD" ] . includes ( event . request . method )
237+ ) {
238+ return errorResponse ( 401 , ERROR_MESSAGES . authOnly ) ;
239+ }
251240
252- // Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
253- if ( config . ALLOW_IFRAME !== "true" ) {
254- response . headers . append ( "Content-Security-Policy" , "frame-ancestors 'none';" ) ;
255- }
241+ let replaced = false ;
256242
257- if (
258- event . url . pathname . startsWith ( `${ base } /login/callback` ) ||
259- event . url . pathname . startsWith ( `${ base } /login` )
260- ) {
261- response . headers . append ( "Cache-Control" , "no-store" ) ;
262- }
243+ const response = await resolve ( event , {
244+ transformPageChunk : ( chunk ) => {
245+ // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
246+ if ( replaced || ! chunk . html . includes ( "%gaId%" ) ) {
247+ return chunk . html ;
248+ }
249+ replaced = true ;
263250
264- if ( event . url . pathname . startsWith ( `${ base } /api/` ) ) {
265- // get origin from the request
266- const requestOrigin = event . request . headers . get ( "origin" ) ;
251+ return chunk . html . replace ( "%gaId%" , config . PUBLIC_GOOGLE_ANALYTICS_ID ) ;
252+ } ,
253+ filterSerializedResponseHeaders : ( header ) => {
254+ return header . includes ( "content-type" ) ;
255+ } ,
256+ } ) ;
267257
268- // get origin from the config if its defined
269- let allowedOrigin = config . PUBLIC_ORIGIN ? new URL ( config . PUBLIC_ORIGIN ) . origin : undefined ;
258+ // Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
259+ if ( config . ALLOW_IFRAME !== "true" ) {
260+ response . headers . append ( "Content-Security-Policy" , "frame-ancestors 'none';" ) ;
261+ }
270262
271263 if (
272- dev || // if we're in dev mode
273- ! requestOrigin || // or the origin is null (SSR)
274- isHostLocalhost ( new URL ( requestOrigin ) . hostname ) // or the origin is localhost
264+ event . url . pathname . startsWith ( `${ base } /login/callback` ) ||
265+ event . url . pathname . startsWith ( `${ base } /login` )
275266 ) {
276- allowedOrigin = "*" ; // allow all origins
277- } else if ( allowedOrigin === requestOrigin ) {
278- allowedOrigin = requestOrigin ; // echo back the caller
267+ response . headers . append ( "Cache-Control" , "no-store" ) ;
279268 }
280269
281- if ( allowedOrigin ) {
282- response . headers . set ( "Access-Control-Allow-Origin" , allowedOrigin ) ;
283- response . headers . set (
284- "Access-Control-Allow-Methods" ,
285- "GET, POST, PUT, PATCH, DELETE, OPTIONS"
286- ) ;
287- response . headers . set ( "Access-Control-Allow-Headers" , "Content-Type, Authorization" ) ;
270+ if ( event . url . pathname . startsWith ( `${ base } /api/` ) ) {
271+ // get origin from the request
272+ const requestOrigin = event . request . headers . get ( "origin" ) ;
273+
274+ // get origin from the config if its defined
275+ let allowedOrigin = config . PUBLIC_ORIGIN ? new URL ( config . PUBLIC_ORIGIN ) . origin : undefined ;
276+
277+ if (
278+ dev || // if we're in dev mode
279+ ! requestOrigin || // or the origin is null (SSR)
280+ isHostLocalhost ( new URL ( requestOrigin ) . hostname ) // or the origin is localhost
281+ ) {
282+ allowedOrigin = "*" ; // allow all origins
283+ } else if ( allowedOrigin === requestOrigin ) {
284+ allowedOrigin = requestOrigin ; // echo back the caller
285+ }
286+
287+ if ( allowedOrigin ) {
288+ response . headers . set ( "Access-Control-Allow-Origin" , allowedOrigin ) ;
289+ response . headers . set (
290+ "Access-Control-Allow-Methods" ,
291+ "GET, POST, PUT, PATCH, DELETE, OPTIONS"
292+ ) ;
293+ response . headers . set ( "Access-Control-Allow-Headers" , "Content-Type, Authorization" ) ;
294+ }
288295 }
289- }
290- return response ;
296+ return response ;
297+ } , requestId ) ;
291298} ;
292299
293300export const handleFetch : HandleFetch = async ( { event, request, fetch } ) => {
0 commit comments