@@ -20,7 +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" ;
23+ import { runWithRequestContext , updateRequestContext } from "$lib/server/requestContext" ;
2424
2525export const init : ServerInit = async ( ) => {
2626 // Wait for config to be fully loaded
@@ -105,196 +105,207 @@ export const handle: Handle = async ({ event, resolve }) => {
105105 const requestId = crypto . randomUUID ( ) ;
106106
107107 // Run the entire request handling within the request context
108- return runWithRequestContext ( async ( ) => {
109- await ready . then ( ( ) => {
110- config . checkForUpdates ( ) ;
111- } ) ;
112-
113- logger . debug ( {
114- locals : event . locals ,
115- url : event . url . pathname ,
116- params : event . params ,
117- request : event . request ,
118- } ) ;
119-
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- } ,
108+ return runWithRequestContext (
109+ async ( ) => {
110+ await ready . then ( ( ) => {
111+ config . checkForUpdates ( ) ;
129112 } ) ;
130- }
131113
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 ;
134-
135- if ( ! ADMIN_SECRET ) {
136- return errorResponse ( 500 , "Admin API is not configured" ) ;
137- }
114+ logger . debug ( {
115+ locals : event . locals ,
116+ url : event . url . pathname ,
117+ params : event . params ,
118+ request : event . request ,
119+ } ) ;
138120
139- if ( event . request . headers . get ( "Authorization" ) !== `Bearer ${ ADMIN_SECRET } ` ) {
140- return errorResponse ( 401 , "Unauthorized" ) ;
121+ function errorResponse ( status : number , message : string ) {
122+ const sendJson =
123+ event . request . headers . get ( "accept" ) ?. includes ( "application/json" ) ||
124+ event . request . headers . get ( "content-type" ) ?. includes ( "application/json" ) ;
125+ return new Response ( sendJson ? JSON . stringify ( { error : message } ) : message , {
126+ status,
127+ headers : {
128+ "content-type" : sendJson ? "application/json" : "text/plain" ,
129+ } ,
130+ } ) ;
141131 }
142- }
143-
144- const auth = await authenticateRequest (
145- { type : "svelte" , value : event . request . headers } ,
146- { type : "svelte" , value : event . cookies } ,
147- event . url
148- ) ;
149132
150- event . locals . sessionId = auth . sessionId ;
133+ if (
134+ event . url . pathname . startsWith ( `${ base } /admin/` ) ||
135+ event . url . pathname === `${ base } /admin`
136+ ) {
137+ const ADMIN_SECRET = config . ADMIN_API_SECRET || config . PARQUET_EXPORT_SECRET ;
151138
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 ) ;
139+ if ( ! ADMIN_SECRET ) {
140+ return errorResponse ( 500 , "Admin API is not configured" ) ;
162141 }
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 ) ;
142+
143+ if ( event . request . headers . get ( "Authorization" ) !== `Bearer ${ ADMIN_SECRET } ` ) {
144+ return errorResponse ( 401 , "Unauthorized" ) ;
178145 }
179146 }
180- }
181-
182- event . locals . user = auth . user || undefined ;
183- event . locals . token = auth . token ;
184147
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" ) ;
148+ const auth = await authenticateRequest (
149+ { type : "svelte" , value : event . request . headers } ,
150+ { type : "svelte" , value : event . cookies } ,
151+ event . url
152+ ) ;
200153
201- if ( ! origin ) {
202- return errorResponse ( 403 , "Non-JSON form requests need to have an origin" ) ;
154+ event . locals . sessionId = auth . sessionId ;
155+
156+ if ( loginEnabled && ! auth . user && ! event . url . pathname . startsWith ( `${ base } /.well-known/` ) ) {
157+ if ( config . AUTOMATIC_LOGIN === "true" ) {
158+ // AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
159+ if (
160+ ! event . url . pathname . startsWith ( `${ base } /login` ) &&
161+ ! event . url . pathname . startsWith ( `${ base } /healthcheck` )
162+ ) {
163+ // To get the same CSRF token after callback
164+ refreshSessionCookie ( event . cookies , auth . secretSessionId ) ;
165+ return await triggerOauthFlow ( event ) ;
166+ }
167+ } else {
168+ // Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
169+ if (
170+ event . url . pathname !== `${ base } /` &&
171+ event . url . pathname !== `${ base } ` &&
172+ ! event . url . pathname . startsWith ( `${ base } /login` ) &&
173+ ! event . url . pathname . startsWith ( `${ base } /login/callback` ) &&
174+ ! event . url . pathname . startsWith ( `${ base } /healthcheck` ) &&
175+ ! event . url . pathname . startsWith ( `${ base } /r/` ) &&
176+ ! event . url . pathname . startsWith ( `${ base } /conversation/` ) &&
177+ ! event . url . pathname . startsWith ( `${ base } /models/` ) &&
178+ ! event . url . pathname . startsWith ( `${ base } /api` )
179+ ) {
180+ refreshSessionCookie ( event . cookies , auth . secretSessionId ) ;
181+ return triggerOauthFlow ( event ) ;
182+ }
203183 }
184+ }
185+
186+ event . locals . user = auth . user || undefined ;
187+ event . locals . token = auth . token ;
204188
205- const validOrigins = [
206- new URL ( event . request . url ) . host ,
207- ... ( config . PUBLIC_ORIGIN ? [ new URL ( config . PUBLIC_ORIGIN ) . host ] : [ ] ) ,
208- ] ;
189+ // Update request context with user after authentication
190+ if ( auth . user ?. username ) {
191+ updateRequestContext ( { user : auth . user . username } ) ;
192+ }
209193
210- if ( ! validOrigins . includes ( new URL ( origin ) . host ) ) {
211- return errorResponse ( 403 , "Invalid referer for POST request" ) ;
194+ event . locals . isAdmin =
195+ event . locals . user ?. isAdmin || adminTokenManager . isAdmin ( event . locals . sessionId ) ;
196+
197+ // CSRF protection
198+ const requestContentType = event . request . headers . get ( "content-type" ) ?. split ( ";" ) [ 0 ] ?? "" ;
199+ /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
200+ const nativeFormContentTypes = [
201+ "multipart/form-data" ,
202+ "application/x-www-form-urlencoded" ,
203+ "text/plain" ,
204+ ] ;
205+
206+ if ( event . request . method === "POST" ) {
207+ if ( nativeFormContentTypes . includes ( requestContentType ) ) {
208+ const origin = event . request . headers . get ( "origin" ) ;
209+
210+ if ( ! origin ) {
211+ return errorResponse ( 403 , "Non-JSON form requests need to have an origin" ) ;
212+ }
213+
214+ const validOrigins = [
215+ new URL ( event . request . url ) . host ,
216+ ...( config . PUBLIC_ORIGIN ? [ new URL ( config . PUBLIC_ORIGIN ) . host ] : [ ] ) ,
217+ ] ;
218+
219+ if ( ! validOrigins . includes ( new URL ( origin ) . host ) ) {
220+ return errorResponse ( 403 , "Invalid referer for POST request" ) ;
221+ }
212222 }
213223 }
214- }
215224
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 ) ;
223-
224- await collections . sessions . updateOne (
225- { sessionId : auth . sessionId } ,
226- { $set : { updatedAt : new Date ( ) , expiresAt : addWeeks ( new Date ( ) , 2 ) } }
227- ) ;
228- }
225+ if (
226+ event . request . method === "POST" ||
227+ event . url . pathname . startsWith ( `${ base } /login` ) ||
228+ event . url . pathname . startsWith ( `${ base } /login/callback` )
229+ ) {
230+ // if the request is a POST request or login-related we refresh the cookie
231+ refreshSessionCookie ( event . cookies , auth . secretSessionId ) ;
229232
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- }
233+ await collections . sessions . updateOne (
234+ { sessionId : auth . sessionId } ,
235+ { $set : { updatedAt : new Date ( ) , expiresAt : addWeeks ( new Date ( ) , 2 ) } }
236+ ) ;
237+ }
240238
241- let replaced = false ;
239+ if (
240+ loginEnabled &&
241+ ! event . locals . user &&
242+ ! event . url . pathname . startsWith ( `${ base } /login` ) &&
243+ ! event . url . pathname . startsWith ( `${ base } /admin` ) &&
244+ ! event . url . pathname . startsWith ( `${ base } /settings` ) &&
245+ ! [ "GET" , "OPTIONS" , "HEAD" ] . includes ( event . request . method )
246+ ) {
247+ return errorResponse ( 401 , ERROR_MESSAGES . authOnly ) ;
248+ }
242249
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 ;
250-
251- return chunk . html . replace ( "%gaId%" , config . PUBLIC_GOOGLE_ANALYTICS_ID ) ;
252- } ,
253- filterSerializedResponseHeaders : ( header ) => {
254- return header . includes ( "content-type" ) ;
255- } ,
256- } ) ;
257-
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- }
250+ let replaced = false ;
262251
263- if (
264- event . url . pathname . startsWith ( `${ base } /login/callback` ) ||
265- event . url . pathname . startsWith ( `${ base } /login` )
266- ) {
267- response . headers . append ( "Cache-Control" , "no-store" ) ;
268- }
252+ const response = await resolve ( event , {
253+ transformPageChunk : ( chunk ) => {
254+ // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
255+ if ( replaced || ! chunk . html . includes ( "%gaId%" ) ) {
256+ return chunk . html ;
257+ }
258+ replaced = true ;
269259
270- if ( event . url . pathname . startsWith ( `${ base } /api/` ) ) {
271- // get origin from the request
272- const requestOrigin = event . request . headers . get ( "origin" ) ;
260+ return chunk . html . replace ( "%gaId%" , config . PUBLIC_GOOGLE_ANALYTICS_ID ) ;
261+ } ,
262+ filterSerializedResponseHeaders : ( header ) => {
263+ return header . includes ( "content-type" ) ;
264+ } ,
265+ } ) ;
273266
274- // get origin from the config if its defined
275- let allowedOrigin = config . PUBLIC_ORIGIN ? new URL ( config . PUBLIC_ORIGIN ) . origin : undefined ;
267+ // Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
268+ if ( config . ALLOW_IFRAME !== "true" ) {
269+ response . headers . append ( "Content-Security-Policy" , "frame-ancestors 'none';" ) ;
270+ }
276271
277272 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
273+ event . url . pathname . startsWith ( `${ base } /login/callback` ) ||
274+ event . url . pathname . startsWith ( `${ base } /login` )
281275 ) {
282- allowedOrigin = "*" ; // allow all origins
283- } else if ( allowedOrigin === requestOrigin ) {
284- allowedOrigin = requestOrigin ; // echo back the caller
276+ response . headers . append ( "Cache-Control" , "no-store" ) ;
285277 }
286278
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" ) ;
279+ if ( event . url . pathname . startsWith ( `${ base } /api/` ) ) {
280+ // get origin from the request
281+ const requestOrigin = event . request . headers . get ( "origin" ) ;
282+
283+ // get origin from the config if its defined
284+ let allowedOrigin = config . PUBLIC_ORIGIN ? new URL ( config . PUBLIC_ORIGIN ) . origin : undefined ;
285+
286+ if (
287+ dev || // if we're in dev mode
288+ ! requestOrigin || // or the origin is null (SSR)
289+ isHostLocalhost ( new URL ( requestOrigin ) . hostname ) // or the origin is localhost
290+ ) {
291+ allowedOrigin = "*" ; // allow all origins
292+ } else if ( allowedOrigin === requestOrigin ) {
293+ allowedOrigin = requestOrigin ; // echo back the caller
294+ }
295+
296+ if ( allowedOrigin ) {
297+ response . headers . set ( "Access-Control-Allow-Origin" , allowedOrigin ) ;
298+ response . headers . set (
299+ "Access-Control-Allow-Methods" ,
300+ "GET, POST, PUT, PATCH, DELETE, OPTIONS"
301+ ) ;
302+ response . headers . set ( "Access-Control-Allow-Headers" , "Content-Type, Authorization" ) ;
303+ }
294304 }
295- }
296- return response ;
297- } , requestId ) ;
305+ return response ;
306+ } ,
307+ { requestId, url : event . url . pathname , ip : event . getClientAddress ( ) }
308+ ) ;
298309} ;
299310
300311export const handleFetch : HandleFetch = async ( { event, request, fetch } ) => {
0 commit comments