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); }