// Client fuer die Admin-API des Analytics-Collectors (interner Listener, Port 8091). // Token und Secret erscheinen nie in Fehlermeldungen oder Logs. import crypto from 'node:crypto' import { config } from '../config.js' const HEAT_TTL_SECONDS = 30 * 60 function fail(message, status) { const err = new Error(message) err.status = status return err } export function createAnalyticsClient(opts = {}) { const cfg = { ...config.analyse, ...opts } const timeoutMs = opts.timeoutMs || 10000 async function request(method, path, body) { if (!cfg.adminToken) throw fail('Auswertung nicht eingerichtet', 503) const controller = new AbortController() const timer = setTimeout(() => controller.abort(), timeoutMs) try { const response = await fetch(`${String(cfg.url).replace(/\/$/, '')}${path}`, { method, headers: { Authorization: `Bearer ${cfg.adminToken}`, ...(body ? { 'Content-Type': 'application/json' } : {}), }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }) const data = await response.json().catch(() => ({})) if (!response.ok) throw fail(`Auswertung antwortet mit Fehler ${response.status}`, response.status) return data } catch (e) { if (e.status) throw e if (e.name === 'AbortError') throw fail('Auswertung antwortet nicht (Zeitueberschreitung)', 504) throw fail('Auswertung nicht erreichbar', 502) } finally { clearTimeout(timer) } } const sitePath = (siteKey) => `/a/admin/sites/${encodeURIComponent(siteKey)}` return { putSite(siteKey, { projectId, domains, active }) { return request('PUT', sitePath(siteKey), { projectId, domains, active }) }, deleteSite(siteKey) { return request('DELETE', sitePath(siteKey)) }, getStats(siteKey, range) { return request('GET', `${sitePath(siteKey)}/stats?range=${encodeURIComponent(range)}`) }, // Format (R3): ., byte-gleich zu analytics/src/heat.js heatToken({ siteKey, path, vp, now = Date.now() }) { if (!cfg.heatSecret) throw fail('Auswertung nicht eingerichtet', 503) const exp = Math.floor(now / 1000) + HEAT_TTL_SECONDS const part = Buffer.from(JSON.stringify({ s: siteKey, p: path, v: vp, e: exp })).toString('base64url') return `${part}.${crypto.createHmac('sha256', cfg.heatSecret).update(part).digest('hex')}` }, } } export const analyticsClient = createAnalyticsClient()