@@ -83,6 +83,20 @@ if (!OPENAI_API_KEY) {
8383 logger . warn ( "OPENAI_API_KEY is missing. Chat responses will use safe fallback guidance only." ) ;
8484}
8585
86+ // In production, require ADMIN_KEY and ALLOWED_ORIGINS to be explicitly configured
87+ if ( IS_PROD ) {
88+ if ( ! ADMIN_KEY ) {
89+ logger . error ( "ADMIN_KEY is required in production. Set ADMIN_KEY in environment." ) ;
90+ // Fail fast in production to avoid accidentally exposing admin endpoints.
91+ process . exit ( 1 ) ;
92+ }
93+
94+ if ( ! ALLOWED_ORIGINS . length ) {
95+ logger . error ( "ALLOWED_ORIGINS must be set in production to restrict CORS." ) ;
96+ process . exit ( 1 ) ;
97+ }
98+ }
99+
86100// -----------------------------
87101// OpenAI client
88102// -----------------------------
@@ -209,7 +223,19 @@ app.set("trust proxy", 1);
209223// Security headers
210224app . use (
211225 helmet ( {
212- contentSecurityPolicy : false ,
226+ // In production enable a reasonable CSP; disable only when developing locally.
227+ contentSecurityPolicy : IS_PROD
228+ ? {
229+ directives : {
230+ defaultSrc : [ "'self'" ] ,
231+ scriptSrc : [ "'self'" ] ,
232+ styleSrc : [ "'self'" , "https:" ] ,
233+ imgSrc : [ "'self'" , 'data:' ] ,
234+ connectSrc : [ "'self'" ] ,
235+ frameAncestors : [ "'self'" ] ,
236+ } ,
237+ }
238+ : false ,
213239 crossOriginEmbedderPolicy : false ,
214240 } )
215241) ;
@@ -221,9 +247,7 @@ app.use(
221247 // allow same-origin / curl / server-to-server requests
222248 if ( ! origin ) return cb ( null , true ) ;
223249
224- // if not configured, default open for MVP
225- if ( ! ALLOWED_ORIGINS . length ) return cb ( null , true ) ;
226-
250+ // In production ALLOWED_ORIGINS is required (validated at startup).
227251 if ( ALLOWED_ORIGINS . includes ( origin ) ) return cb ( null , true ) ;
228252 return cb ( new Error ( `CORS blocked: ${ origin } ` ) , false ) ;
229253 } ,
@@ -254,7 +278,8 @@ app.use(
254278 stream : {
255279 write : ( msg ) => logger . info ( msg . trim ( ) ) ,
256280 } ,
257- skip : ( ) => IS_PROD === false ,
281+ // In development we want request logs visible. Only skip morgan in production.
282+ skip : ( ) => IS_PROD === true ,
258283 } )
259284) ;
260285
@@ -351,6 +376,29 @@ app.get("/metrics", (req, res) => {
351376 } ) ;
352377} ) ;
353378
379+ // Serve knowledge base entry (simple JSON) for frontend citations
380+ app . get ( '/kb/:id' , ( req , res ) => {
381+ try {
382+ const kb = knowledge . getJson ( ) ;
383+ if ( ! kb ) return res . status ( 503 ) . json ( { error : 'Knowledge base not available' } ) ;
384+
385+ const id = req . params . id ;
386+ let entry = null ;
387+
388+ if ( Array . isArray ( kb . documents ) ) {
389+ entry = kb . documents . find ( ( d ) => d . id === id ) ;
390+ } else if ( kb [ id ] ) {
391+ entry = kb [ id ] ;
392+ }
393+
394+ if ( ! entry ) return res . status ( 404 ) . json ( { error : 'KB entry not found' } ) ;
395+ return res . json ( { id, entry } ) ;
396+ } catch ( e ) {
397+ logger . error ( 'KB fetch error' , { error : e ?. message || String ( e ) } ) ;
398+ return res . status ( 500 ) . json ( { error : 'KB lookup failed' } ) ;
399+ }
400+ } ) ;
401+
354402// -----------------------------
355403// Admin endpoints
356404// - In production: requires ADMIN_KEY via x-admin-key header OR {adminKey} body
@@ -378,6 +426,30 @@ app.post("/admin/knowledge/reload", requireAdmin, async (req, res) => {
378426 }
379427} ) ;
380428
429+ // Admin: upload new knowledge JSON (protected in production)
430+ app . post ( '/admin/knowledge/upload' , requireAdmin , express . json ( { limit : '1mb' } ) , async ( req , res ) => {
431+ try {
432+ const payload = req . body ;
433+ if ( ! payload || ( typeof payload !== 'object' ) ) {
434+ return res . status ( 400 ) . json ( { ok : false , error : 'Invalid JSON payload' , requestId : req . requestId } ) ;
435+ }
436+
437+ // Basic validation: must contain metadata and documents OR be object-based
438+ const hasDocs = Array . isArray ( payload . documents ) && payload . documents . length > 0 ;
439+ const hasObj = Object . keys ( payload ) . length > 0 && ( payload . metadata || hasDocs ) ;
440+ if ( ! hasDocs && ! hasObj ) {
441+ return res . status ( 400 ) . json ( { ok : false , error : 'Knowledge JSON missing required fields' , requestId : req . requestId } ) ;
442+ }
443+
444+ await fs . writeFile ( KNOWLEDGE_PATH , JSON . stringify ( payload , null , 2 ) , 'utf8' ) ;
445+ await knowledge . load ( true ) ;
446+ return res . json ( { ok : true , updated : true , requestId : req . requestId } ) ;
447+ } catch ( e ) {
448+ logger . error ( 'KB upload failed' , { error : e ?. message || String ( e ) } ) ;
449+ return res . status ( 500 ) . json ( { ok : false , error : 'KB upload failed' , requestId : req . requestId } ) ;
450+ }
451+ } ) ;
452+
381453// -----------------------------
382454// Chat endpoint - UNIFIED MODE
383455// Body: { message: string, stream?: boolean }
@@ -639,8 +711,13 @@ app.use((err, req, res, _next) => {
639711 error : err ?. message || String ( err ) ,
640712 stack : err ?. stack ,
641713 } ) ;
714+ const userMessage = IS_PROD
715+ ? "Server error. Please try again later or contact support."
716+ : ( err ?. message || "Error" ) + ( err ?. stack ? `\n${ err . stack } ` : "" ) ;
717+
642718 res . status ( 500 ) . json ( {
643719 error : IS_PROD ? "Internal server error" : ( err ?. message || "Error" ) ,
720+ text : userMessage ,
644721 requestId : req . requestId ,
645722 } ) ;
646723} ) ;
0 commit comments