mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-05-09 16:51:59 +02:00
- Tighten CORS regex to block worldmonitorEVIL.vercel.app spoofing - Move sidecar /api/local-env-update behind token auth + add key allowlist - Add postMessage origin/source validation in LiveNewsPanel - Replace postMessage wildcard '*' targetOrigin with specific origin - Add isDisallowedOrigin() check to 25 API endpoints missing it - Migrate gdelt-geo & EIA from custom CORS to shared _cors.js - Add CORS to firms-fires, stock-index, youtube/live endpoints - Tighten youtube/embed.js ALLOWED_ORIGINS regex - Remove 'unsafe-inline' from CSP script-src - Add iframe sandbox attribute to YouTube embed - Validate meta-tags URL query params with regex allowlist
165 lines
5.2 KiB
JavaScript
165 lines
5.2 KiB
JavaScript
import { getCachedJson, setCachedJson, hashString } from './_upstash-cache.js';
|
|
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
|
|
|
export const config = {
|
|
runtime: 'edge',
|
|
};
|
|
|
|
const GROQ_API_URL = 'https://api.groq.com/openai/v1/chat/completions';
|
|
const MODEL = 'llama-3.1-8b-instant';
|
|
const CACHE_TTL_SECONDS = 86400;
|
|
const CACHE_VERSION = 'v1';
|
|
|
|
const VALID_LEVELS = ['critical', 'high', 'medium', 'low', 'info'];
|
|
const VALID_CATEGORIES = [
|
|
'conflict', 'protest', 'disaster', 'diplomatic', 'economic',
|
|
'terrorism', 'cyber', 'health', 'environmental', 'military',
|
|
'crime', 'infrastructure', 'tech', 'general',
|
|
];
|
|
|
|
export default async function handler(request) {
|
|
const corsHeaders = getCorsHeaders(request, 'GET, OPTIONS');
|
|
|
|
if (request.method === 'OPTIONS') {
|
|
return new Response(null, { status: 204, headers: corsHeaders });
|
|
}
|
|
|
|
if (request.method !== 'GET') {
|
|
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
|
status: 405,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
if (isDisallowedOrigin(request)) {
|
|
return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
|
|
status: 403,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const apiKey = process.env.GROQ_API_KEY;
|
|
if (!apiKey) {
|
|
return new Response(JSON.stringify({ fallback: true, skipped: true, reason: 'GROQ_API_KEY not configured' }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const url = new URL(request.url);
|
|
const title = url.searchParams.get('title');
|
|
const variant = url.searchParams.get('variant') || 'full';
|
|
|
|
if (!title) {
|
|
return new Response(JSON.stringify({ error: 'title param required' }), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const cacheKey = `classify:${CACHE_VERSION}:${hashString(title.toLowerCase() + ':' + variant)}`;
|
|
|
|
try {
|
|
const cached = await getCachedJson(cacheKey);
|
|
if (cached && typeof cached === 'object' && cached.level) {
|
|
return new Response(JSON.stringify({
|
|
level: cached.level,
|
|
category: cached.category,
|
|
confidence: 0.9,
|
|
source: 'llm',
|
|
cached: true,
|
|
}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const isTech = variant === 'tech';
|
|
const systemPrompt = `You classify news headlines into threat level and category. Return ONLY valid JSON, no other text.
|
|
|
|
Levels: critical, high, medium, low, info
|
|
Categories: conflict, protest, disaster, diplomatic, economic, terrorism, cyber, health, environmental, military, crime, infrastructure, tech, general
|
|
|
|
${isTech ? 'Focus: technology, startups, AI, cybersecurity. Most tech news is "low" or "info" unless it involves outages, breaches, or major disruptions.' : 'Focus: geopolitical events, conflicts, disasters, diplomacy. Classify by real-world severity and impact.'}
|
|
|
|
Return: {"level":"...","category":"..."}`;
|
|
|
|
const response = await fetch(GROQ_API_URL, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
model: MODEL,
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: title },
|
|
],
|
|
temperature: 0,
|
|
max_tokens: 50,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error('[Classify] Groq error:', response.status);
|
|
return new Response(JSON.stringify({ fallback: true }), {
|
|
status: response.status,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const data = await response.json();
|
|
const raw = data.choices?.[0]?.message?.content?.trim();
|
|
if (!raw) {
|
|
return new Response(JSON.stringify({ fallback: true }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch {
|
|
console.warn('[Classify] Invalid JSON from LLM:', raw);
|
|
return new Response(JSON.stringify({ fallback: true }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const level = VALID_LEVELS.includes(parsed.level) ? parsed.level : null;
|
|
const category = VALID_CATEGORIES.includes(parsed.category) ? parsed.category : null;
|
|
if (!level || !category) {
|
|
return new Response(JSON.stringify({ fallback: true }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
await setCachedJson(cacheKey, { level, category, timestamp: Date.now() }, CACHE_TTL_SECONDS);
|
|
|
|
return new Response(JSON.stringify({
|
|
level,
|
|
category,
|
|
confidence: 0.9,
|
|
source: 'llm',
|
|
cached: false,
|
|
}), {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
|
|
},
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('[Classify] Error:', error.message);
|
|
return new Response(JSON.stringify({ fallback: true }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
}
|