Some checks failed
Quality gates / quality (pull_request) Failing after 1m38s
- src/image-ingress.js: sniff magic bytes independent of declared MIME, reject polyglots (embedded ZIP/script payloads), enforce 4 MB body cap and 4096 px dimension cap before decode, re-encode via fixed-argv Pillow subprocess with 15s timeout, strip EXIF/GPS/XMP/tEXt metadata, fail closed with sanitized short errors and manual fallback. - scripts/reencode_image.py: defensive decoder; decompression-bomb guarded; prints one JSON verdict line, never image bytes. - src/rate-limiter.js + server.mjs /api/analyze: bounded fixed-window per-client limiter, 429 with retry-after, no payload retention. - tests/image-ingress.test.js: hostile synthetic fixtures only (spoofed MIME, GIF/ZIP polyglots, truncated/garbage/empty images, 12000x12000 bomb, 6000x6000 oversized, EXIF+GPS, SVG-with-script); asserts provider is never reached on rejection and error messages contain no bytes/base64/stacks. Closes #16
33 lines
1.7 KiB
JavaScript
33 lines
1.7 KiB
JavaScript
import { validateImageIngress } from './image-ingress.js';
|
|
import { buildVisionRequest, parseVisionResponse } from './analysis.js';
|
|
|
|
function providerEndpoint(baseUrl) {
|
|
let url;
|
|
try { url = new URL(baseUrl); } catch { throw new Error('Invalid AI provider URL.'); }
|
|
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Invalid AI provider URL.');
|
|
return `${url.toString().replace(/\/$/, '')}/chat/completions`;
|
|
}
|
|
|
|
export async function analyzePhoto({ payload, fetchImpl = fetch, config }) {
|
|
// Hardened ingress first: consent, magic bytes, limits, safe re-encode,
|
|
// metadata stripping. All failures happen before any provider work.
|
|
const photo = await validateImageIngress(payload);
|
|
if (!config?.model) throw new Error('AI analysis is not configured.');
|
|
const endpoint = providerEndpoint(config.baseUrl);
|
|
const response = await fetchImpl(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
authorization: `Bearer ${config.apiKey || 'local-proxy'}`,
|
|
},
|
|
body: JSON.stringify(buildVisionRequest({ imageDataUrl: photo.imageDataUrl, model: config.model })),
|
|
signal: AbortSignal.timeout(config.requestTimeoutMs || 60_000),
|
|
}).catch(() => { throw new Error('AI analysis is temporarily unavailable. Continue manually.'); });
|
|
if (!response.ok) throw new Error('AI analysis is temporarily unavailable. Continue manually.');
|
|
let data;
|
|
try { data = await response.json(); } catch { throw new Error('Invalid response from the AI provider.'); }
|
|
const content = data?.choices?.[0]?.message?.content;
|
|
if (typeof content !== 'string' && (typeof content !== 'object' || content === null)) throw new Error('Invalid response from the AI provider.');
|
|
return parseVisionResponse(content);
|
|
}
|