diff --git a/server.mjs b/server.mjs index 99387e4..9c95cc4 100644 --- a/server.mjs +++ b/server.mjs @@ -76,8 +76,9 @@ http.createServer(async(req,res)=>{ return sendJson(res,200,{...visionConfig.publicStatus(),providerReady:provider.ready,modelSeen:provider.modelSeen,privacy}); } if(appPath==='/api/analyze'&&req.method==='POST'){ - if(!visionConfig.enabled)return sendJson(res,503,{error:'AI analysis is disabled. Continue manually.'}); - // Cheap refusals first, in cost order: the request budget, then the body, then the image itself. + // Refusals first, in cost order: the request budget, then the body, then the image bytes. None of + // these depend on the model, so hostile bytes are refused as hostile even when analysis is off — + // otherwise the disabled switch hides the ingress boundary behind a 503 and the refusal is untested. const budget=analyzeLimiter.take(clientBucket(req)); if(!budget.allowed){ res.setHeader('retry-after',String(Math.max(1,Math.ceil(budget.retryAfterMs/1000)))); @@ -94,6 +95,8 @@ http.createServer(async(req,res)=>{ } try{ingressGuard(payload)} catch(error){return sendIngressError(res,error)} + // Bytes are known to be a real photo at this point, so the disabled switch is the honest answer. + if(!visionConfig.enabled)return sendJson(res,503,{error:'AI analysis is disabled. Continue manually.'}); try{return sendJson(res,200,await analyzePhoto({payload,config:visionConfig}))} catch(error){const failure=visionFailureResponse(error);return sendJson(res,failure.status,failure.body)} }