@@ -2,6 +2,9 @@ import compression from "compression";
22import cors from "cors" ;
33import "dotenv/config" ;
44import express , { Request , Response } from "express" ;
5+ import helmet from "helmet" ;
6+ import http , { Server } from "http" ;
7+ import { createServer } from "http" ;
58
69import { validateEnv } from "./validateEnv" ;
710import { z } from "zod" ;
@@ -75,6 +78,14 @@ const RATE_LIMIT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_READ_LIMIT ?? proc
7578const WRITE_RATE_LIMIT_MAX_REQUESTS = Number ( process . env . RATE_LIMIT_WRITE_LIMIT ?? process . env . WRITE_RATE_LIMIT_MAX_REQUESTS ?? 20 ) ;
7679const CAMPAIGN_DETAIL_PLEDGE_PREVIEW_LIMIT = 5 ;
7780
81+ app . use ( helmet ( {
82+ contentSecurityPolicy : {
83+ directives : {
84+ defaultSrc : [ "'none'" ] ,
85+ } ,
86+ } ,
87+ } ) ) ;
88+
7889app . use (
7990 cors ( {
8091 origin : ( origin , callback ) => {
@@ -268,6 +279,61 @@ app.get('/api/health', (_req: Request, res: Response) => {
268279 } ) ;
269280} ) ;
270281
282+ app . get ( '/api/health/deep' , applyRateLimit ( 1000 ) , async ( _req : Request , res : Response ) => {
283+ try {
284+ const database = checkDbHealth ( ) ;
285+ const hasContractId = ! ! config . contractId ;
286+ let sorobanHealthy = false ;
287+
288+ try {
289+ if ( config . sorobanRpcUrl ) {
290+ const response = await fetch ( config . sorobanRpcUrl , {
291+ method : 'POST' ,
292+ headers : { 'Content-Type' : 'application/json' } ,
293+ body : JSON . stringify ( {
294+ jsonrpc : '2.0' ,
295+ method : 'getHealth' ,
296+ id : 1 ,
297+ } ) ,
298+ signal : AbortSignal . timeout ( 5000 ) ,
299+ } ) ;
300+ sorobanHealthy = response . ok || response . status < 500 ;
301+ }
302+ } catch {
303+ sorobanHealthy = false ;
304+ }
305+
306+ const allHealthy = database . reachable && hasContractId && sorobanHealthy ;
307+
308+ res . status ( allHealthy ? 200 : 503 ) . json ( {
309+ overall : allHealthy ? 'up' : 'down' ,
310+ timestamp : new Date ( ) . toISOString ( ) ,
311+ uptimeSeconds : Number ( process . uptime ( ) . toFixed ( 3 ) ) ,
312+ components : {
313+ db : {
314+ status : database . reachable ? 'up' : 'down' ,
315+ details : database . reachable ? 'SQLite database reachable' : database . error ,
316+ } ,
317+ soroban : {
318+ status : sorobanHealthy ? 'up' : 'down' ,
319+ details : config . sorobanRpcUrl ? 'Soroban RPC reachable' : 'Soroban RPC URL not configured' ,
320+ } ,
321+ contract : {
322+ status : hasContractId ? 'up' : 'down' ,
323+ details : hasContractId ? 'CONTRACT_ID configured' : 'CONTRACT_ID not set' ,
324+ } ,
325+ } ,
326+ } ) ;
327+ } catch ( error ) {
328+ res . status ( 503 ) . json ( {
329+ overall : 'down' ,
330+ timestamp : new Date ( ) . toISOString ( ) ,
331+ error : 'Deep health check failed' ,
332+ message : error instanceof Error ? error . message : String ( error ) ,
333+ } ) ;
334+ }
335+ } ) ;
336+
271337app . get ( '/api/campaigns' , ( req : Request , res : Response ) => {
272338 const queryResult = parseCampaignListQuery ( req . query as Record < string , unknown > ) ;
273339 if ( ! queryResult . ok ) {
@@ -717,6 +783,8 @@ export function configureHttpServer(server: Server): Server {
717783 return server ;
718784}
719785
786+ let isShuttingDown = false ;
787+
720788function startServer ( ) {
721789 validateEnv ( ) ;
722790 printStartupBanner ( ) ;
@@ -730,8 +798,54 @@ function startServer() {
730798 } ) ;
731799 }
732800
801+ // Reject new requests during shutdown
802+ app . use ( ( req , res , next ) => {
803+ if ( isShuttingDown ) {
804+ res . status ( 503 ) . json ( {
805+ success : false ,
806+ error : {
807+ code : 'SERVICE_UNAVAILABLE' ,
808+ message : 'Server is shutting down' ,
809+ } ,
810+ } ) ;
811+ return ;
812+ }
813+ next ( ) ;
814+ } ) ;
815+
733816 const server = configureHttpServer ( createServer ( app ) ) ;
734817
818+ const gracefulShutdown = ( signal : string ) => {
819+ if ( isShuttingDown ) return ;
820+ isShuttingDown = true ;
821+
822+ logInfo ( 'server_shutting_down' , { signal } , config . logLevel ) ;
823+
824+ // Stop accepting new connections
825+ server . close ( ( ) => {
826+ logInfo ( 'server_closed' , { message : 'Server closed' } , config . logLevel ) ;
827+ process . exit ( 0 ) ;
828+ } ) ;
829+
830+ // Force shutdown after grace period
831+ const gracePeriodSeconds = 10 ;
832+ const gracePeriodTimer = setTimeout ( ( ) => {
833+ logError (
834+ new Error ( 'Graceful shutdown timeout exceeded' ) ,
835+ { event : 'graceful_shutdown_timeout' , gracePeriodSeconds } ,
836+ config . logLevel ,
837+ ) ;
838+ process . exit ( 1 ) ;
839+ } , gracePeriodSeconds * 1000 ) ;
840+
841+ // Close the database connection when shutting down
842+ gracePeriodTimer . unref ( ) ;
843+ } ;
844+
845+ // Handle graceful shutdown
846+ process . on ( 'SIGTERM' , ( ) => gracefulShutdown ( 'SIGTERM' ) ) ;
847+ process . on ( 'SIGINT' , ( ) => gracefulShutdown ( 'SIGINT' ) ) ;
848+
735849 server . listen ( config . port , ( ) => {
736850 logInfo (
737851 'server_started' ,
@@ -744,6 +858,8 @@ function startServer() {
744858 config . logLevel ,
745859 ) ;
746860 } ) ;
861+
862+ return server ;
747863}
748864
749865if ( require . main === module ) {
0 commit comments