export const config = { runtime: 'edge' }; // Fetch with timeout async function fetchWithTimeout(url, options, timeoutMs = 15000) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...options, signal: controller.signal }); return response; } finally { clearTimeout(timeout); } } // Allowed RSS feed domains for security const ALLOWED_DOMAINS = [ 'feeds.bbci.co.uk', 'www.theguardian.com', 'feeds.npr.org', 'news.google.com', 'www.aljazeera.com', 'rss.cnn.com', 'hnrss.org', 'feeds.arstechnica.com', 'www.theverge.com', 'www.cnbc.com', 'feeds.marketwatch.com', 'www.defenseone.com', 'breakingdefense.com', 'www.bellingcat.com', 'techcrunch.com', 'huggingface.co', 'www.technologyreview.com', 'rss.arxiv.org', 'export.arxiv.org', 'www.federalreserve.gov', 'www.sec.gov', 'www.whitehouse.gov', 'www.state.gov', 'www.defense.gov', 'home.treasury.gov', 'www.justice.gov', 'tools.cdc.gov', 'www.fema.gov', 'www.dhs.gov', 'www.thedrive.com', 'krebsonsecurity.com', 'finance.yahoo.com', 'thediplomat.com', 'venturebeat.com', 'foreignpolicy.com', 'www.ft.com', 'openai.com', 'www.reutersagency.com', 'feeds.reuters.com', 'rsshub.app', 'www.cfr.org', 'www.csis.org', 'www.politico.com', 'www.brookings.edu', 'layoffs.fyi', 'www.defensenews.com', 'www.foreignaffairs.com', 'www.atlanticcouncil.org', // Tech variant domains 'www.zdnet.com', 'www.techmeme.com', 'www.darkreading.com', 'www.schneier.com', 'rss.politico.com', 'www.anandtech.com', 'www.tomshardware.com', 'www.semianalysis.com', 'feed.infoq.com', 'thenewstack.io', 'devops.com', 'dev.to', 'lobste.rs', 'changelog.com', 'seekingalpha.com', 'news.crunchbase.com', 'www.saastr.com', 'feeds.feedburner.com', // Additional tech variant domains 'www.producthunt.com', 'www.axios.com', 'github.blog', 'githubnext.com', 'mshibanami.github.io', 'www.engadget.com', 'news.mit.edu', 'dev.events', // VC blogs 'www.ycombinator.com', 'a16z.com', 'review.firstround.com', 'www.sequoiacap.com', 'www.nfx.com', 'www.aaronsw.com', 'bothsidesofthetable.com', 'www.lennysnewsletter.com', 'stratechery.com', // Regional startup news 'www.eu-startups.com', 'tech.eu', 'sifted.eu', 'www.techinasia.com', 'kr-asia.com', 'techcabal.com', 'disrupt-africa.com', 'lavca.org', 'contxto.com', 'inc42.com', 'yourstory.com', // Funding & VC 'pitchbook.com', 'www.cbinsights.com', // Accelerators 'www.techstars.com', // Middle East & Regional News 'english.alarabiya.net', 'www.arabnews.com', 'www.timesofisrael.com', 'www.scmp.com', 'kyivindependent.com', 'www.themoscowtimes.com', 'feeds.24.com', 'feeds.capi24.com', // News24 redirect destination // International Organizations 'news.un.org', 'www.iaea.org', 'www.who.int', 'www.cisa.gov', 'www.crisisgroup.org', // Additional 'news.ycombinator.com', ]; // CORS helper - allow worldmonitor.app and Vercel preview domains function getCorsHeaders(req) { const origin = req.headers.get('origin') || '*'; const allowedPatterns = [ /^https:\/\/(.*\.)?worldmonitor\.app$/, // Matches worldmonitor.app and *.worldmonitor.app /^https:\/\/.*-elie-habib-projects\.vercel\.app$/, /^https:\/\/worldmonitor.*\.vercel\.app$/, /^http:\/\/localhost(:\d+)?$/, ]; const isAllowed = origin === '*' || allowedPatterns.some(p => p.test(origin)); return { 'Access-Control-Allow-Origin': isAllowed ? origin : 'https://worldmonitor.app', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Max-Age': '86400', }; } export default async function handler(req) { const corsHeaders = getCorsHeaders(req); // Handle CORS preflight if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); } const requestUrl = new URL(req.url); const feedUrl = requestUrl.searchParams.get('url'); if (!feedUrl) { return new Response(JSON.stringify({ error: 'Missing url parameter' }), { status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders }, }); } try { const parsedUrl = new URL(feedUrl); // Security: Check if domain is allowed if (!ALLOWED_DOMAINS.includes(parsedUrl.hostname)) { return new Response(JSON.stringify({ error: 'Domain not allowed' }), { status: 403, headers: { 'Content-Type': 'application/json', ...corsHeaders }, }); } // Google News is slow - use longer timeout const isGoogleNews = feedUrl.includes('news.google.com'); const timeout = isGoogleNews ? 20000 : 12000; const response = await fetchWithTimeout(feedUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'application/rss+xml, application/xml, text/xml, */*', 'Accept-Language': 'en-US,en;q=0.9', }, }, timeout); const data = await response.text(); return new Response(data, { status: response.status, headers: { 'Content-Type': 'application/xml', 'Cache-Control': 'public, max-age=300', ...corsHeaders, }, }); } catch (error) { const isTimeout = error.name === 'AbortError'; console.error('RSS proxy error:', feedUrl, error.message); return new Response(JSON.stringify({ error: isTimeout ? 'Feed timeout' : 'Failed to fetch feed', details: error.message, url: feedUrl }), { status: isTimeout ? 504 : 502, headers: { 'Content-Type': 'application/json', ...corsHeaders }, }); } }