@@ -113,6 +113,11 @@ export const VIEWER_PURPOSE = "derby_viewer_session";
113113export const ADMIN_LOGIN_PURPOSE = "derby_admin_login" ;
114114const COOKIE_MAX_AGE = 60 * 60 * 24 * 30 ; // 30 days
115115
116+ // Precompute expected HMACs at startup — keys never change at runtime
117+ const _expectedAdminHmac = _adminKey ? await computeHmac ( _adminKey , ADMIN_PURPOSE ) : null ;
118+ const _expectedViewerHmac = _viewerKey ? await computeHmac ( _viewerKey , VIEWER_PURPOSE ) : null ;
119+ const _expectedAdminLoginHmac = _adminKey ? await computeHmac ( _adminKey , ADMIN_LOGIN_PURPOSE ) : null ;
120+
116121export const isSecureRequest = ( req : Request ) : boolean => {
117122 if ( new URL ( req . url ) . protocol === "https:" ) return true ;
118123 const proto = req . headers . get ( "x-forwarded-proto" ) ;
@@ -131,16 +136,14 @@ const setCookie = (
131136 headers . append ( "Set-Cookie" , cookie ) ;
132137} ;
133138
134- export const setAdminCookie = async ( headers : Headers , secure : boolean = false ) : Promise < void > => {
135- if ( ! _adminKey ) return ;
136- const hmac = await computeHmac ( _adminKey , ADMIN_PURPOSE ) ;
137- setCookie ( headers , ADMIN_COOKIE , hmac , COOKIE_MAX_AGE , secure ) ;
139+ export const setAdminCookie = ( headers : Headers , secure : boolean = false ) : void => {
140+ if ( ! _expectedAdminHmac ) return ;
141+ setCookie ( headers , ADMIN_COOKIE , _expectedAdminHmac , COOKIE_MAX_AGE , secure ) ;
138142} ;
139143
140- export const setViewerCookie = async ( headers : Headers , secure : boolean = false ) : Promise < void > => {
141- if ( ! _viewerKey ) return ;
142- const hmac = await computeHmac ( _viewerKey , VIEWER_PURPOSE ) ;
143- setCookie ( headers , VIEWER_COOKIE , hmac , COOKIE_MAX_AGE , secure ) ;
144+ export const setViewerCookie = ( headers : Headers , secure : boolean = false ) : void => {
145+ if ( ! _expectedViewerHmac ) return ;
146+ setCookie ( headers , VIEWER_COOKIE , _expectedViewerHmac , COOKIE_MAX_AGE , secure ) ;
144147} ;
145148
146149export const clearAdminCookie = ( headers : Headers ) : void => {
@@ -153,32 +156,30 @@ export const clearViewerCookie = (headers: Headers): void => {
153156
154157// ===== COOKIE VALIDATION =====
155158
156- const validateAdminCookie = async (
159+ const validateAdminCookie = (
157160 cookies : Record < string , string >
158- ) : Promise < boolean > => {
159- if ( ! _adminKey ) return false ;
161+ ) : boolean => {
162+ if ( ! _expectedAdminHmac ) return false ;
160163 const cookie = cookies [ ADMIN_COOKIE ] ;
161164 if ( ! cookie ) return false ;
162- const expected = await computeHmac ( _adminKey , ADMIN_PURPOSE ) ;
163- return timingSafeEqual ( cookie , expected ) ;
165+ return timingSafeEqual ( cookie , _expectedAdminHmac ) ;
164166} ;
165167
166- const validateViewerCookie = async (
168+ const validateViewerCookie = (
167169 cookies : Record < string , string >
168- ) : Promise < boolean > => {
169- if ( ! _viewerKey ) return false ;
170+ ) : boolean => {
171+ if ( ! _expectedViewerHmac ) return false ;
170172 const cookie = cookies [ VIEWER_COOKIE ] ;
171173 if ( ! cookie ) return false ;
172- const expected = await computeHmac ( _viewerKey , VIEWER_PURPOSE ) ;
173- return timingSafeEqual ( cookie , expected ) ;
174+ return timingSafeEqual ( cookie , _expectedViewerHmac ) ;
174175} ;
175176
176- export const hasViewerAccess = async ( req : Request ) : Promise < boolean > => {
177+ export const hasViewerAccess = ( req : Request ) : boolean => {
177178 if ( _publicMode ) return true ;
178179 if ( ! _privateMode ) return true ;
179180 const cookies = parseCookies ( req ) ;
180- if ( await validateAdminCookie ( cookies ) ) return true ;
181- if ( await validateViewerCookie ( cookies ) ) return true ;
181+ if ( validateAdminCookie ( cookies ) ) return true ;
182+ if ( validateViewerCookie ( cookies ) ) return true ;
182183 return false ;
183184} ;
184185
@@ -193,8 +194,7 @@ export const adminOnly = (handler: Handler): Handler => {
193194 }
194195
195196 const cookies = parseCookies ( req ) ;
196- const isAdmin = await validateAdminCookie ( cookies ) ;
197- if ( ! isAdmin ) {
197+ if ( ! validateAdminCookie ( cookies ) ) {
198198 return respondJson ( { error : "Unauthorized" } , 401 ) ;
199199 }
200200
@@ -215,13 +215,11 @@ export const viewerRequired = (handler: Handler): Handler => {
215215 const cookies = parseCookies ( req ) ;
216216
217217 // Admin cookie implicitly satisfies viewer check
218- const isAdmin = await validateAdminCookie ( cookies ) ;
219- if ( isAdmin ) {
218+ if ( validateAdminCookie ( cookies ) ) {
220219 return handler ( req , server ) ;
221220 }
222221
223- const isViewer = await validateViewerCookie ( cookies ) ;
224- if ( isViewer ) {
222+ if ( validateViewerCookie ( cookies ) ) {
225223 return handler ( req , server ) ;
226224 }
227225
@@ -231,29 +229,77 @@ export const viewerRequired = (handler: Handler): Handler => {
231229
232230// ===== AUTH STATUS =====
233231
234- export const getAuthStatus = async (
232+ export const getAuthStatus = (
235233 req : Request
236- ) : Promise < {
234+ ) : {
237235 admin : boolean ;
238236 viewer : boolean ;
239237 publicMode : boolean ;
240238 privateMode : boolean ;
241- } > => {
239+ } => {
242240 if ( _publicMode ) {
243241 return { admin : true , viewer : true , publicMode : true , privateMode : false } ;
244242 }
245243
246244 const cookies = parseCookies ( req ) ;
247- const admin = await validateAdminCookie ( cookies ) ;
248- const viewer = admin || ( _privateMode && ( await validateViewerCookie ( cookies ) ) ) ;
245+ const admin = validateAdminCookie ( cookies ) ;
246+ const viewer = admin || ( _privateMode && validateViewerCookie ( cookies ) ) ;
249247
250248 return { admin, viewer, publicMode : false , privateMode : _privateMode } ;
251249} ;
252250
251+ // ===== LOGIN TOKEN VALIDATION =====
252+
253+ export const validateLoginToken = ( token : string ) : boolean => {
254+ if ( ! _expectedAdminLoginHmac ) return false ;
255+ return timingSafeEqual ( token , _expectedAdminLoginHmac ) ;
256+ } ;
257+
258+ // ===== LOGIN RATE LIMITING =====
259+
260+ const LOGIN_RATE_LIMIT = 10 ;
261+ const LOGIN_RATE_WINDOW_MS = 60_000 ;
262+ const _loginAttempts = new Map < string , number [ ] > ( ) ;
263+
264+ const getClientIp = ( req : Request ) : string => {
265+ const forwarded = req . headers . get ( "x-forwarded-for" ) ;
266+ if ( forwarded ) return forwarded . split ( "," ) [ 0 ] ! . trim ( ) ;
267+ return req . headers . get ( "x-real-ip" ) ?? "direct" ;
268+ } ;
269+
270+ export const checkLoginRateLimit = ( req : Request ) : boolean => {
271+ const ip = getClientIp ( req ) ;
272+ const now = Date . now ( ) ;
273+ const attempts = _loginAttempts . get ( ip ) ;
274+ const recent = attempts ? attempts . filter ( ( t ) => now - t < LOGIN_RATE_WINDOW_MS ) : [ ] ;
275+
276+ if ( recent . length >= LOGIN_RATE_LIMIT ) {
277+ _loginAttempts . set ( ip , recent ) ;
278+ return false ;
279+ }
280+
281+ recent . push ( now ) ;
282+ _loginAttempts . set ( ip , recent ) ;
283+ return true ;
284+ } ;
285+
286+ // Periodic cleanup to prevent unbounded memory growth
287+ setInterval ( ( ) => {
288+ const now = Date . now ( ) ;
289+ for ( const [ ip , attempts ] of _loginAttempts ) {
290+ const recent = attempts . filter ( ( t ) => now - t < LOGIN_RATE_WINDOW_MS ) ;
291+ if ( recent . length === 0 ) _loginAttempts . delete ( ip ) ;
292+ else _loginAttempts . set ( ip , recent ) ;
293+ }
294+ } , LOGIN_RATE_WINDOW_MS ) . unref ( ) ;
295+
253296// ===== STARTUP LOGGING =====
254297
255298export const logAuthConfig = ( ) : void => {
256299 if ( ! _adminKey ) {
300+ if ( _viewerKey ) {
301+ console . warn ( "Auth: DERBY_VIEWER_KEY is set but DERBY_ADMIN_KEY is not — viewer key ignored" ) ;
302+ }
257303 console . log ( "Auth: PUBLIC MODE (no DERBY_ADMIN_KEY set)" ) ;
258304 return ;
259305 }
0 commit comments