diff --git a/.husky/pre-push b/.husky/pre-push index 3d306681f..b30d0c710 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -9,5 +9,18 @@ for f in scripts/*.cjs; do [ -f "$f" ] && node -c "$f" || exit 1 done +echo "Running edge function bundle check..." +for f in api/*.js; do + case "$(basename "$f")" in _*) continue;; esac + npx esbuild "$f" --bundle --format=esm --platform=browser --outfile=/dev/null 2>/dev/null || { + echo "ERROR: esbuild failed to bundle $f — this will break Vercel deployment" + npx esbuild "$f" --bundle --format=esm --platform=browser --outfile=/dev/null + exit 1 + } +done + +echo "Running edge function tests..." +node --test tests/edge-functions.test.mjs || exit 1 + echo "Running version sync check..." npm run version:check || exit 1 diff --git a/api/_rss-allowed-domains.js b/api/_rss-allowed-domains.js new file mode 100644 index 000000000..3c622512e --- /dev/null +++ b/api/_rss-allowed-domains.js @@ -0,0 +1,269 @@ +// Edge-compatible ESM wrapper for shared RSS allowed domains. +// Source of truth: shared/rss-allowed-domains.json +// NOTE: Cannot use `import ... with { type: 'json' }` — Vercel esbuild doesn't support import attributes. +export default [ + "feeds.bbci.co.uk", + "www.theguardian.com", + "feeds.npr.org", + "news.google.com", + "www.aljazeera.com", + "www.aljazeera.net", + "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", + "asia.nikkei.com", + "www.cfr.org", + "www.csis.org", + "www.politico.com", + "www.brookings.edu", + "layoffs.fyi", + "www.defensenews.com", + "www.militarytimes.com", + "taskandpurpose.com", + "news.usni.org", + "www.oryxspioenkop.com", + "www.gov.uk", + "www.foreignaffairs.com", + "www.atlanticcouncil.org", + "www.zdnet.com", + "www.techmeme.com", + "www.darkreading.com", + "www.schneier.com", + "www.ransomware.live", + "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", + "www.producthunt.com", + "www.axios.com", + "api.axios.com", + "github.blog", + "githubnext.com", + "mshibanami.github.io", + "www.engadget.com", + "news.mit.edu", + "dev.events", + "www.ycombinator.com", + "a16z.com", + "review.firstround.com", + "www.sequoiacap.com", + "www.nfx.com", + "www.aaronsw.com", + "bothsidesofthetable.com", + "www.lennysnewsletter.com", + "stratechery.com", + "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", + "pitchbook.com", + "www.cbinsights.com", + "www.techstars.com", + "asharqbusiness.com", + "asharq.com", + "www.omanobserver.om", + "english.alarabiya.net", + "www.arabnews.com", + "www.timesofisrael.com", + "www.haaretz.com", + "www.scmp.com", + "kyivindependent.com", + "www.themoscowtimes.com", + "feeds.24.com", + "feeds.news24.com", + "feeds.capi24.com", + "www.france24.com", + "www.euronews.com", + "de.euronews.com", + "es.euronews.com", + "fr.euronews.com", + "it.euronews.com", + "pt.euronews.com", + "ru.euronews.com", + "www.lemonde.fr", + "rss.dw.com", + "www.bild.de", + "www.africanews.com", + "fr.africanews.com", + "www.premiumtimesng.com", + "www.vanguardngr.com", + "www.channelstv.com", + "dailytrust.com", + "www.thisdaylive.com", + "www.naftemporiki.gr", + "www.in.gr", + "www.iefimerida.gr", + "www.lasillavacia.com", + "www.channelnewsasia.com", + "japantoday.com", + "www.thehindu.com", + "indianexpress.com", + "www.twz.com", + "gcaptain.com", + "news.un.org", + "www.iaea.org", + "www.who.int", + "www.cisa.gov", + "www.crisisgroup.org", + "rusi.org", + "warontherocks.com", + "www.aei.org", + "responsiblestatecraft.org", + "www.fpri.org", + "jamestown.org", + "www.chathamhouse.org", + "ecfr.eu", + "www.gmfus.org", + "www.wilsoncenter.org", + "www.lowyinstitute.org", + "www.mei.edu", + "www.stimson.org", + "www.cnas.org", + "carnegieendowment.org", + "www.rand.org", + "fas.org", + "www.armscontrol.org", + "www.nti.org", + "thebulletin.org", + "www.iss.europa.eu", + "www.fao.org", + "worldbank.org", + "www.imf.org", + "www.bbc.com", + "www.spiegel.de", + "www.tagesschau.de", + "newsfeed.zeit.de", + "feeds.elpais.com", + "e00-elmundo.uecdn.es", + "www.repubblica.it", + "www.ansa.it", + "xml2.corriereobjects.it", + "feeds.nos.nl", + "www.nrc.nl", + "www.telegraaf.nl", + "www.dn.se", + "www.svd.se", + "www.svt.se", + "www.asahi.com", + "www.clarin.com", + "oglobo.globo.com", + "feeds.folha.uol.com.br", + "www.eltiempo.com", + "www.eluniversal.com.mx", + "www.jeuneafrique.com", + "www.lorientlejour.com", + "www.hurriyet.com.tr", + "tvn24.pl", + "www.polsatnews.pl", + "www.rp.pl", + "meduza.io", + "novayagazeta.eu", + "www.bangkokpost.com", + "vnexpress.net", + "www.abc.net.au", + "islandtimes.org", + "www.brasilparalelo.com.br", + "mexiconewsdaily.com", + "insightcrime.org", + "www.primicias.ec", + "www.infobae.com", + "www.eluniverso.com", + "news.ycombinator.com", + "www.coindesk.com", + "cointelegraph.com", + "travel.state.gov", + "smartraveller.gov.au", + "www.smartraveller.gov.au", + "www.safetravel.govt.nz", + "th.usembassy.gov", + "ae.usembassy.gov", + "de.usembassy.gov", + "ua.usembassy.gov", + "mx.usembassy.gov", + "in.usembassy.gov", + "pk.usembassy.gov", + "co.usembassy.gov", + "pl.usembassy.gov", + "bd.usembassy.gov", + "it.usembassy.gov", + "do.usembassy.gov", + "mm.usembassy.gov", + "wwwnc.cdc.gov", + "www.ecdc.europa.eu", + "www.afro.who.int", + "www.goodnewsnetwork.org", + "www.positive.news", + "reasonstobecheerful.world", + "www.optimistdaily.com", + "www.upworthy.com", + "www.dailygood.org", + "www.goodgoodgood.co", + "www.good.is", + "www.sunnyskyz.com", + "thebetterindia.com", + "singularityhub.com", + "humanprogress.org", + "greatergood.berkeley.edu", + "www.onlygoodnewsdaily.com", + "www.sciencedaily.com", + "feeds.nature.com", + "www.nature.com", + "www.livescience.com", + "www.newscientist.com", + "www.pbs.org", + "feeds.abcnews.com", + "feeds.nbcnews.com", + "www.cbsnews.com", + "moxie.foxnews.com", + "feeds.content.dowjones.io", + "thehill.com" +]; diff --git a/api/rss-proxy.js b/api/rss-proxy.js index f9df5348d..a5e96209b 100644 --- a/api/rss-proxy.js +++ b/api/rss-proxy.js @@ -2,6 +2,7 @@ import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; import { validateApiKey } from './_api-key.js'; import { checkRateLimit } from './_rate-limit.js'; import { getRelayBaseUrl, getRelayHeaders, fetchWithTimeout } from './_relay.js'; +import RSS_ALLOWED_DOMAINS from './_rss-allowed-domains.js'; export const config = { runtime: 'edge' }; @@ -40,296 +41,8 @@ async function fetchViaRailway(feedUrl, timeoutMs) { }, timeoutMs); } -// 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', - 'www.aljazeera.net', - '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', - 'asia.nikkei.com', - 'www.cfr.org', - 'www.csis.org', - 'www.politico.com', - 'www.brookings.edu', - 'layoffs.fyi', - 'www.defensenews.com', - 'www.militarytimes.com', - 'taskandpurpose.com', - 'news.usni.org', - 'www.oryxspioenkop.com', - 'www.gov.uk', - 'www.foreignaffairs.com', - 'www.atlanticcouncil.org', - // Tech variant domains - 'www.zdnet.com', - 'www.techmeme.com', - 'www.darkreading.com', - 'www.schneier.com', - 'www.ransomware.live', - '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', - 'api.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 - 'asharqbusiness.com', - 'asharq.com', - 'www.omanobserver.om', - 'english.alarabiya.net', - 'www.arabnews.com', - 'www.timesofisrael.com', - 'www.haaretz.com', - 'www.scmp.com', - 'kyivindependent.com', - 'www.themoscowtimes.com', - 'feeds.24.com', - 'feeds.news24.com', // News24 main feed domain - 'feeds.capi24.com', // News24 redirect destination - // International News Sources - 'www.france24.com', - 'www.euronews.com', - 'de.euronews.com', - 'es.euronews.com', - 'fr.euronews.com', - 'it.euronews.com', - 'pt.euronews.com', - 'ru.euronews.com', - 'www.lemonde.fr', - 'rss.dw.com', - 'www.bild.de', - 'www.africanews.com', - 'fr.africanews.com', - // Nigeria - 'www.premiumtimesng.com', - 'www.vanguardngr.com', - 'www.channelstv.com', - 'dailytrust.com', - 'www.thisdaylive.com', - // Greek - 'www.naftemporiki.gr', - 'www.in.gr', - 'www.iefimerida.gr', - 'www.lasillavacia.com', - 'www.channelnewsasia.com', - 'japantoday.com', - 'www.thehindu.com', - 'indianexpress.com', - 'www.twz.com', - 'gcaptain.com', - // International Organizations - 'news.un.org', - 'www.iaea.org', - 'www.who.int', - 'www.cisa.gov', - 'www.crisisgroup.org', - // Think Tanks & Research (Added 2026-01-29) - 'rusi.org', - 'warontherocks.com', - 'www.aei.org', - 'responsiblestatecraft.org', - 'www.fpri.org', - 'jamestown.org', - 'www.chathamhouse.org', - 'ecfr.eu', - 'www.gmfus.org', - 'www.wilsoncenter.org', - 'www.lowyinstitute.org', - 'www.mei.edu', - 'www.stimson.org', - 'www.cnas.org', - 'carnegieendowment.org', - 'www.rand.org', - 'fas.org', - 'www.armscontrol.org', - 'www.nti.org', - 'thebulletin.org', - 'www.iss.europa.eu', - // Economic & Food Security - 'www.fao.org', - 'worldbank.org', - 'www.imf.org', - // International news (various languages) - 'www.bbc.com', - 'www.spiegel.de', - 'www.tagesschau.de', - 'newsfeed.zeit.de', - 'feeds.elpais.com', - 'e00-elmundo.uecdn.es', - 'www.repubblica.it', - 'www.ansa.it', - 'xml2.corriereobjects.it', - 'feeds.nos.nl', - 'www.nrc.nl', - 'www.telegraaf.nl', - 'www.dn.se', - 'www.svd.se', - 'www.svt.se', - 'www.asahi.com', - 'www.clarin.com', - 'oglobo.globo.com', - 'feeds.folha.uol.com.br', - 'www.eltiempo.com', - 'www.eluniversal.com.mx', - 'www.jeuneafrique.com', - 'www.lorientlejour.com', - // Regional locale feeds (tr, pl, ru, th, vi, pt) - 'www.hurriyet.com.tr', - 'tvn24.pl', - 'www.polsatnews.pl', - 'www.rp.pl', - 'meduza.io', - 'novayagazeta.eu', - 'www.bangkokpost.com', - 'vnexpress.net', - 'www.abc.net.au', - 'islandtimes.org', - 'www.brasilparalelo.com.br', - // Mexico & LatAm Security - 'mexiconewsdaily.com', - 'insightcrime.org', - 'www.primicias.ec', - 'www.infobae.com', - 'www.eluniverso.com', - // Additional - 'news.ycombinator.com', - // Finance variant - 'seekingalpha.com', - 'www.coindesk.com', - 'cointelegraph.com', - // Security advisories — government travel advisory feeds - 'travel.state.gov', - 'www.safetravel.govt.nz', - // US Embassy security alerts - 'th.usembassy.gov', - 'ae.usembassy.gov', - 'de.usembassy.gov', - 'ua.usembassy.gov', - 'mx.usembassy.gov', - 'in.usembassy.gov', - 'pk.usembassy.gov', - 'co.usembassy.gov', - 'pl.usembassy.gov', - 'bd.usembassy.gov', - 'it.usembassy.gov', - 'do.usembassy.gov', - 'mm.usembassy.gov', - // Health advisories - 'wwwnc.cdc.gov', - 'www.ecdc.europa.eu', - 'www.who.int', - 'www.afro.who.int', - // Happy variant — positive news sources - 'www.goodnewsnetwork.org', - 'www.positive.news', - 'reasonstobecheerful.world', - 'www.optimistdaily.com', - 'www.upworthy.com', - 'www.dailygood.org', - 'www.goodgoodgood.co', - 'www.good.is', - 'www.sunnyskyz.com', - 'thebetterindia.com', - 'singularityhub.com', - 'humanprogress.org', - 'greatergood.berkeley.edu', - 'www.onlygoodnewsdaily.com', - 'www.sciencedaily.com', - 'feeds.nature.com', - 'www.nature.com', - 'www.livescience.com', - 'www.newscientist.com', - // US broadcast & print news - 'www.pbs.org', - 'feeds.abcnews.com', - 'feeds.nbcnews.com', - 'www.cbsnews.com', - 'moxie.foxnews.com', - 'feeds.content.dowjones.io', - 'thehill.com', -]; +// Allowed RSS feed domains — shared source of truth (shared/rss-allowed-domains.js) +const ALLOWED_DOMAINS = RSS_ALLOWED_DOMAINS; export default async function handler(req) { const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS'); diff --git a/middleware.ts b/middleware.ts index c828b98ce..ce3aef946 100644 --- a/middleware.ts +++ b/middleware.ts @@ -63,7 +63,8 @@ export default function middleware(request: Request) { if (path === '/' && SOCIAL_PREVIEW_UA.test(ua)) { const variant = VARIANT_HOST_MAP[host]; if (variant && isAllowedHost(host)) { - const og = VARIANT_OG[variant]; + const og = VARIANT_OG[variant] as { title: string; description: string; image: string; url: string } | undefined; + if (!og) return; const html = `
diff --git a/scripts/ais-relay.cjs b/scripts/ais-relay.cjs index 467cd05ff..2302f8d9b 100644 --- a/scripts/ais-relay.cjs +++ b/scripts/ais-relay.cjs @@ -17,6 +17,7 @@ const { readFileSync } = require('fs'); const crypto = require('crypto'); const v8 = require('v8'); const { WebSocketServer, WebSocket } = require('ws'); +const RSS_ALLOWED_DOMAINS = new Set(require('../shared/rss-allowed-domains.cjs')); // Log effective heap limit at startup (verifies NODE_OPTIONS=--max-old-space-size is active) const _heapStats = v8.getHeapStatistics(); @@ -2199,7 +2200,15 @@ function gzipSyncBuffer(body) { } } -function getClientIp(req) { +function getClientIp(req, isPublic = false) { + if (isPublic) { + // Public routes: only trust CF-Connecting-IP (set by Cloudflare, not spoofable). + // x-real-ip is excluded — client-spoofable on unauthenticated endpoints. + const cfIp = req.headers['cf-connecting-ip']; + if (typeof cfIp === 'string' && cfIp.trim()) return cfIp.trim(); + return req.socket?.remoteAddress || 'unknown'; + } + // Authenticated routes: x-real-ip is safe because auth token validates the caller const xRealIp = req.headers['x-real-ip']; if (typeof xRealIp === 'string' && xRealIp.trim()) { return xRealIp.trim(); @@ -2207,7 +2216,6 @@ function getClientIp(req) { const xff = req.headers['x-forwarded-for']; if (typeof xff === 'string' && xff) { const parts = xff.split(',').map((part) => part.trim()).filter(Boolean); - // Proxy chain order is client,proxy1,proxy2...; use first hop as client IP. if (parts.length > 0) return parts[0]; } return req.socket?.remoteAddress || 'unknown'; @@ -2258,12 +2266,12 @@ function getRateLimitForPath(pathname) { return RELAY_RATE_LIMIT_MAX; } -function consumeRateLimit(req, pathname) { +function consumeRateLimit(req, pathname, isPublic = false) { const maxRequests = getRateLimitForPath(pathname); if (!Number.isFinite(maxRequests) || maxRequests <= 0) return { limited: false, limit: 0, remaining: 0, resetInMs: 0 }; const now = Date.now(); - const ip = getClientIp(req); + const ip = getClientIp(req, isPublic); const key = `${getRouteGroup(pathname)}:${ip}`; const existing = requestRateBuckets.get(key); if (!existing || now >= existing.resetAt) { @@ -4363,6 +4371,11 @@ const ALLOWED_ORIGINS = [ function getCorsOrigin(req) { const origin = req.headers.origin || ''; if (ALLOWED_ORIGINS.includes(origin)) return origin; + // Wildcard: any *.worldmonitor.app subdomain (for variant subdomains) + try { + const url = new URL(origin); + if (url.hostname.endsWith('.worldmonitor.app') && url.protocol === 'https:') return origin; + } catch { /* invalid origin — fall through */ } // Optional: allow Vercel preview deployments when explicitly enabled. if (ALLOW_VERCEL_PREVIEW_ORIGINS && origin.endsWith('.vercel.app')) return origin; return ''; @@ -4371,9 +4384,14 @@ function getCorsOrigin(req) { const server = http.createServer(async (req, res) => { const pathname = (req.url || '/').split('?')[0]; const corsOrigin = getCorsOrigin(req); + // Always emit Vary: Origin on /rss (browser-direct via CDN) to prevent + // cached no-CORS responses from being served to browser clients. + const isRssRoute = pathname.startsWith('/rss'); if (corsOrigin) { res.setHeader('Access-Control-Allow-Origin', corsOrigin); res.setHeader('Vary', 'Origin'); + } else if (isRssRoute) { + res.setHeader('Vary', 'Origin'); } res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', `Content-Type, Authorization, ${RELAY_AUTH_HEADER}`); @@ -4388,13 +4406,16 @@ const server = http.createServer(async (req, res) => { // served to unauthenticated requests from edge cache. This is acceptable — all proxied data // is public (RSS, WorldBank, UCDP, Polymarket, OpenSky, AIS). Auth exists for abuse // prevention (rate limiting), not data protection. Cloudflare WAF provides edge-level protection. - const isPublicRoute = pathname === '/health' || pathname === '/'; + const isPublicRoute = pathname === '/health' || pathname === '/' || isRssRoute; if (!isPublicRoute) { if (!isAuthorizedRequest(req)) { return safeEnd(res, 401, { 'Content-Type': 'application/json' }, JSON.stringify({ error: 'Unauthorized', time: Date.now() })); } - const rl = consumeRateLimit(req, pathname); + } + // Rate limiting applies to all non-health routes (including public /rss) + if (pathname !== '/health' && pathname !== '/') { + const rl = consumeRateLimit(req, pathname, isPublicRoute); if (rl.limited) { const retryAfterSec = Math.max(1, Math.ceil(rl.resetInMs / 1000)); return safeEnd(res, 429, { @@ -4620,31 +4641,7 @@ const server = http.createServer(async (req, res) => { return res.end(JSON.stringify({ error: 'Missing url parameter' })); } - // Allow domains that block Vercel IPs (must match feeds.ts railwayRss usage) - const allowedDomains = [ - // Original - 'rss.cnn.com', - 'www.defensenews.com', - 'layoffs.fyi', - // International Organizations - 'news.un.org', - 'www.cisa.gov', - 'www.iaea.org', - 'www.who.int', - 'www.crisisgroup.org', - // Middle East & Regional News - 'english.alarabiya.net', - 'www.arabnews.com', - 'www.timesofisrael.com', - 'www.scmp.com', - 'kyivindependent.com', - 'www.themoscowtimes.com', - // Africa - 'feeds.24.com', - 'feeds.capi24.com', // News24 redirect destination - 'islandtimes.org', - 'www.atlanticcouncil.org', - ]; + // Domain allowlist from shared source of truth (shared/rss-allowed-domains.js) const parsed = new URL(feedUrl); // Block deprecated/stale feed domains — stale clients still request these const blockedDomains = ['rsshub.app']; @@ -4652,7 +4649,7 @@ const server = http.createServer(async (req, res) => { res.writeHead(410, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Feed deprecated' })); } - if (!allowedDomains.includes(parsed.hostname)) { + if (!RSS_ALLOWED_DOMAINS.has(parsed.hostname)) { res.writeHead(403, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Domain not allowed on Railway proxy' })); } diff --git a/shared/rss-allowed-domains.cjs b/shared/rss-allowed-domains.cjs new file mode 100644 index 000000000..fb2ac7105 --- /dev/null +++ b/shared/rss-allowed-domains.cjs @@ -0,0 +1,2 @@ +// CJS wrapper — source of truth is rss-allowed-domains.json +module.exports = require('./rss-allowed-domains.json'); diff --git a/shared/rss-allowed-domains.json b/shared/rss-allowed-domains.json new file mode 100644 index 000000000..f226f5f0c --- /dev/null +++ b/shared/rss-allowed-domains.json @@ -0,0 +1,266 @@ +[ + "feeds.bbci.co.uk", + "www.theguardian.com", + "feeds.npr.org", + "news.google.com", + "www.aljazeera.com", + "www.aljazeera.net", + "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", + "asia.nikkei.com", + "www.cfr.org", + "www.csis.org", + "www.politico.com", + "www.brookings.edu", + "layoffs.fyi", + "www.defensenews.com", + "www.militarytimes.com", + "taskandpurpose.com", + "news.usni.org", + "www.oryxspioenkop.com", + "www.gov.uk", + "www.foreignaffairs.com", + "www.atlanticcouncil.org", + "www.zdnet.com", + "www.techmeme.com", + "www.darkreading.com", + "www.schneier.com", + "www.ransomware.live", + "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", + "www.producthunt.com", + "www.axios.com", + "api.axios.com", + "github.blog", + "githubnext.com", + "mshibanami.github.io", + "www.engadget.com", + "news.mit.edu", + "dev.events", + "www.ycombinator.com", + "a16z.com", + "review.firstround.com", + "www.sequoiacap.com", + "www.nfx.com", + "www.aaronsw.com", + "bothsidesofthetable.com", + "www.lennysnewsletter.com", + "stratechery.com", + "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", + "pitchbook.com", + "www.cbinsights.com", + "www.techstars.com", + "asharqbusiness.com", + "asharq.com", + "www.omanobserver.om", + "english.alarabiya.net", + "www.arabnews.com", + "www.timesofisrael.com", + "www.haaretz.com", + "www.scmp.com", + "kyivindependent.com", + "www.themoscowtimes.com", + "feeds.24.com", + "feeds.news24.com", + "feeds.capi24.com", + "www.france24.com", + "www.euronews.com", + "de.euronews.com", + "es.euronews.com", + "fr.euronews.com", + "it.euronews.com", + "pt.euronews.com", + "ru.euronews.com", + "www.lemonde.fr", + "rss.dw.com", + "www.bild.de", + "www.africanews.com", + "fr.africanews.com", + "www.premiumtimesng.com", + "www.vanguardngr.com", + "www.channelstv.com", + "dailytrust.com", + "www.thisdaylive.com", + "www.naftemporiki.gr", + "www.in.gr", + "www.iefimerida.gr", + "www.lasillavacia.com", + "www.channelnewsasia.com", + "japantoday.com", + "www.thehindu.com", + "indianexpress.com", + "www.twz.com", + "gcaptain.com", + "news.un.org", + "www.iaea.org", + "www.who.int", + "www.cisa.gov", + "www.crisisgroup.org", + "rusi.org", + "warontherocks.com", + "www.aei.org", + "responsiblestatecraft.org", + "www.fpri.org", + "jamestown.org", + "www.chathamhouse.org", + "ecfr.eu", + "www.gmfus.org", + "www.wilsoncenter.org", + "www.lowyinstitute.org", + "www.mei.edu", + "www.stimson.org", + "www.cnas.org", + "carnegieendowment.org", + "www.rand.org", + "fas.org", + "www.armscontrol.org", + "www.nti.org", + "thebulletin.org", + "www.iss.europa.eu", + "www.fao.org", + "worldbank.org", + "www.imf.org", + "www.bbc.com", + "www.spiegel.de", + "www.tagesschau.de", + "newsfeed.zeit.de", + "feeds.elpais.com", + "e00-elmundo.uecdn.es", + "www.repubblica.it", + "www.ansa.it", + "xml2.corriereobjects.it", + "feeds.nos.nl", + "www.nrc.nl", + "www.telegraaf.nl", + "www.dn.se", + "www.svd.se", + "www.svt.se", + "www.asahi.com", + "www.clarin.com", + "oglobo.globo.com", + "feeds.folha.uol.com.br", + "www.eltiempo.com", + "www.eluniversal.com.mx", + "www.jeuneafrique.com", + "www.lorientlejour.com", + "www.hurriyet.com.tr", + "tvn24.pl", + "www.polsatnews.pl", + "www.rp.pl", + "meduza.io", + "novayagazeta.eu", + "www.bangkokpost.com", + "vnexpress.net", + "www.abc.net.au", + "islandtimes.org", + "www.brasilparalelo.com.br", + "mexiconewsdaily.com", + "insightcrime.org", + "www.primicias.ec", + "www.infobae.com", + "www.eluniverso.com", + "news.ycombinator.com", + "www.coindesk.com", + "cointelegraph.com", + "travel.state.gov", + "smartraveller.gov.au", + "www.smartraveller.gov.au", + "www.safetravel.govt.nz", + "th.usembassy.gov", + "ae.usembassy.gov", + "de.usembassy.gov", + "ua.usembassy.gov", + "mx.usembassy.gov", + "in.usembassy.gov", + "pk.usembassy.gov", + "co.usembassy.gov", + "pl.usembassy.gov", + "bd.usembassy.gov", + "it.usembassy.gov", + "do.usembassy.gov", + "mm.usembassy.gov", + "wwwnc.cdc.gov", + "www.ecdc.europa.eu", + "www.afro.who.int", + "www.goodnewsnetwork.org", + "www.positive.news", + "reasonstobecheerful.world", + "www.optimistdaily.com", + "www.upworthy.com", + "www.dailygood.org", + "www.goodgoodgood.co", + "www.good.is", + "www.sunnyskyz.com", + "thebetterindia.com", + "singularityhub.com", + "humanprogress.org", + "greatergood.berkeley.edu", + "www.onlygoodnewsdaily.com", + "www.sciencedaily.com", + "feeds.nature.com", + "www.nature.com", + "www.livescience.com", + "www.newscientist.com", + "www.pbs.org", + "feeds.abcnews.com", + "feeds.nbcnews.com", + "www.cbsnews.com", + "moxie.foxnews.com", + "feeds.content.dowjones.io", + "thehill.com" +] diff --git a/src/config/feeds.ts b/src/config/feeds.ts index c7de44045..3dda93a64 100644 --- a/src/config/feeds.ts +++ b/src/config/feeds.ts @@ -1,12 +1,9 @@ import type { Feed } from '@/types'; import { SITE_VARIANT } from './variant'; +import { rssProxyUrl } from '@/utils'; -// Helper to create RSS proxy URL (Vercel) -const rss = (url: string) => `/api/rss-proxy?url=${encodeURIComponent(url)}`; - -// Keep dedicated alias for feeds historically fetched through Railway. -// `rss-proxy` now handles secure server-side fallback. -const railwayRss = (url: string) => rss(url); +const rss = rssProxyUrl; +const railwayRss = rssProxyUrl; // Source tier system for prioritization (lower = more authoritative) // Tier 1: Wire services - fastest, most reliable breaking news diff --git a/src/config/variants/finance.ts b/src/config/variants/finance.ts index 66eb958c3..57d7aa6f0 100644 --- a/src/config/variants/finance.ts +++ b/src/config/variants/finance.ts @@ -21,8 +21,9 @@ export { // Finance-specific FEEDS configuration import type { Feed } from '@/types'; +import { rssProxyUrl } from '@/utils'; -const rss = (url: string) => `/api/rss-proxy?url=${encodeURIComponent(url)}`; +const rss = rssProxyUrl; export const FEEDS: Record