@@ -52,6 +52,9 @@ async def lifespan(app: FastAPI):
5252 close = getattr (service .runtime , "close" , None )
5353 if close is not None :
5454 await close ()
55+ close_sender = getattr (service .sender , "close" , None )
56+ if close_sender is not None :
57+ await close_sender ()
5558 await asyncio .to_thread (service .repository .close )
5659
5760 app = FastAPI (
@@ -73,7 +76,10 @@ async def readyz():
7376 return {"status" : "ready" }
7477
7578 @app .get ("/metrics" , response_class = PlainTextResponse , tags = ["operations" ])
76- async def metrics ():
79+ async def metrics (x_metrics_token : str = Header (default = "" )):
80+ expected = require_secret (admin_token_env )
81+ if not hmac .compare_digest (x_metrics_token , expected ):
82+ raise HTTPException (status_code = 403 , detail = "forbidden" )
7783 return service .metrics .render_prometheus ()
7884
7985 @app .get ("/admin/tenants" , tags = ["admin" ])
@@ -85,9 +91,18 @@ async def tenants(x_admin_token: str = Header(default="")):
8591
8692 @app .post ("/webhooks/{channel}/{account_id}" , tags = ["webhooks" ])
8793 async def webhook (channel : str , account_id : str , request : Request ):
88- raw_body = await request .body ()
89- if len (raw_body ) > 1_048_576 :
94+ maximum = 1_048_576
95+ content_length = request .headers .get ("content-length" , "" )
96+ if content_length .isdigit () and int (content_length ) > maximum :
9097 raise HTTPException (status_code = 413 , detail = "callback body too large" )
98+ chunks : list [bytes ] = []
99+ received = 0
100+ async for chunk in request .stream ():
101+ received += len (chunk )
102+ if received > maximum :
103+ raise HTTPException (status_code = 413 , detail = "callback body too large" )
104+ chunks .append (chunk )
105+ raw_body = b"" .join (chunks )
91106 response = await service .handle_webhook (
92107 channel = channel ,
93108 account_id = account_id ,
@@ -115,14 +130,18 @@ def build_app_from_env() -> FastAPI:
115130 )
116131 registry = load_tenant_registry (config_path )
117132 offline = os .environ .get ("OFFLINE_ECHO_MODE" , "false" ).lower () == "true"
133+ require_secret ("ADMIN_API_TOKEN" )
134+ for tenant in registry .all ():
135+ for binding in tenant .bindings :
136+ # Callback authentication is mandatory in both offline and online
137+ # modes; only outbound/provider credentials may be skipped offline.
138+ require_secret (binding .webhook_secret_env )
118139 if not offline :
119- require_secret ("ADMIN_API_TOKEN" )
120140 for tenant in registry .all ():
121141 require_secret (tenant .model_api_key_env )
122142 if tenant .session_backend is not StorageBackend .MEMORY :
123143 require_secret (tenant .session_dsn_env )
124144 for binding in tenant .bindings :
125- require_secret (binding .webhook_secret_env )
126145 if binding .channel .lower () == "telegram" :
127146 require_secret (binding .bot_token_env )
128147 elif binding .channel .lower () == "wecom" :
0 commit comments