mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
perf(rss): route RSS direct to Railway, skip Vercel middleman (#961)
* perf(rss): route RSS direct to Railway, skip Vercel middleman
Vercel /api/rss-proxy has 65% error rate (207K failed invocations/12h).
Route browser RSS requests directly to Railway (proxy.worldmonitor.app)
via Cloudflare CDN, eliminating Vercel as middleman.
- Add VITE_RSS_DIRECT_TO_RELAY feature flag (default off) for staged rollout
- Centralize RSS proxy URL in rssProxyUrl() with desktop/dev/prod routing
- Make Railway /rss public (skip auth, keep rate limiting with CF-Connecting-IP)
- Add wildcard *.worldmonitor.app CORS + always emit Vary: Origin on /rss
- Extract ~290 RSS domains to shared/rss-allowed-domains.cjs (single source of truth)
- Convert Railway domain check to Set for O(1) lookups
- Remove rss-proxy from KEYED_CLOUD_API_PATTERN (no longer needs API key header)
- Add edge function test for shared domain list import
* fix(edge): replace node:module with JSON import for edge-compatible RSS domains
api/_rss-allowed-domains.js used createRequire from node:module which is
unsupported in Vercel Edge Runtime, breaking all edge functions (including
api/gpsjam). Replaced with JSON import attribute syntax that works in both
esbuild (Vercel build) and Node.js 22+ (tests).
Also fixed middleware.ts TS18048 error where VARIANT_OG[variant] could be
undefined.
* test(edge): add guard against node: built-in imports in api/ files
Scans ALL api/*.js files (including _ helpers) for node: module imports
which are unsupported in Vercel Edge Runtime. This would have caught the
createRequire(node:module) bug before it reached Vercel.
* fix(edge): inline domain array and remove NextResponse reference
- Replace `import ... with { type: 'json' }` in _rss-allowed-domains.js
with inline array — Vercel esbuild doesn't support import attributes
- Replace `NextResponse.next()` with bare `return` in middleware.ts —
NextResponse was never imported
* ci(pre-push): add esbuild bundle check and edge function tests
The pre-push hook now catches Vercel build failures locally:
- esbuild bundles each api/*.js entrypoint (catches import attribute
syntax, missing modules, and other bundler errors)
- runs edge function test suite (node: imports, module isolation)
This commit is contained in:
@@ -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
|
||||
|
||||
269
api/_rss-allowed-domains.js
Normal file
269
api/_rss-allowed-domains.js
Normal file
@@ -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"
|
||||
];
|
||||
293
api/rss-proxy.js
293
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');
|
||||
|
||||
@@ -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 = `<!DOCTYPE html><html><head>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:title" content="${og.title}"/>
|
||||
|
||||
@@ -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' }));
|
||||
}
|
||||
|
||||
2
shared/rss-allowed-domains.cjs
Normal file
2
shared/rss-allowed-domains.cjs
Normal file
@@ -0,0 +1,2 @@
|
||||
// CJS wrapper — source of truth is rss-allowed-domains.json
|
||||
module.exports = require('./rss-allowed-domains.json');
|
||||
266
shared/rss-allowed-domains.json
Normal file
266
shared/rss-allowed-domains.json
Normal file
@@ -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"
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, Feed[]> = {
|
||||
// Core Markets & Trading News (all free RSS / Google News proxies)
|
||||
|
||||
@@ -24,8 +24,9 @@ export {
|
||||
|
||||
// Tech-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<string, Feed[]> = {
|
||||
// Core Tech News
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SITE_VARIANT } from '@/config/variant';
|
||||
|
||||
const WS_API_URL = import.meta.env.VITE_WS_API_URL || '';
|
||||
const KEYED_CLOUD_API_PATTERN = /^\/api\/(?:[^/]+\/v1\/|bootstrap(?:\?|$)|rss-proxy(?:\?|$)|polymarket(?:\?|$)|ais-snapshot(?:\?|$))/;
|
||||
const KEYED_CLOUD_API_PATTERN = /^\/api\/(?:[^/]+\/v1\/|bootstrap(?:\?|$)|polymarket(?:\?|$)|ais-snapshot(?:\?|$))/;
|
||||
|
||||
const DEFAULT_REMOTE_HOSTS: Record<string, string> = {
|
||||
tech: WS_API_URL,
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { isDesktopRuntime } from '@/services/runtime';
|
||||
import { proxyUrl } from '@/utils';
|
||||
import { rssProxyUrl } from '@/utils';
|
||||
import { getPersistentCache, setPersistentCache } from './persistent-cache';
|
||||
import { dataFreshness } from './data-freshness';
|
||||
import { nameToCountryCode, matchCountryNamesInText } from './country-geometry';
|
||||
|
||||
function advisoryFeedUrl(feedUrl: string): string {
|
||||
if (isDesktopRuntime()) return proxyUrl(feedUrl);
|
||||
return `/api/rss-proxy?url=${encodeURIComponent(feedUrl)}`;
|
||||
}
|
||||
const advisoryFeedUrl = rssProxyUrl;
|
||||
|
||||
export interface SecurityAdvisory {
|
||||
title: string;
|
||||
|
||||
@@ -172,7 +172,7 @@ export function chunkArray<T>(items: T[], size: number): T[][] {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
export { proxyUrl, fetchWithProxy } from './proxy';
|
||||
export { proxyUrl, fetchWithProxy, rssProxyUrl } from './proxy';
|
||||
export { exportToJSON, exportToCSV, ExportPanel } from './export';
|
||||
export { buildMapUrl, parseMapUrlState } from './urlState';
|
||||
export type { ParsedMapUrlState } from './urlState';
|
||||
|
||||
@@ -4,6 +4,23 @@ import { getPersistentCache, setPersistentCache } from '../services/persistent-c
|
||||
const isDev = import.meta.env.DEV;
|
||||
const RESPONSE_CACHE_PREFIX = 'api-response:';
|
||||
|
||||
// RSS proxy: route directly to Railway relay via Cloudflare CDN when enabled.
|
||||
// Feature flag controls rollout; default off for safe staged deployment.
|
||||
const RSS_DIRECT_TO_RELAY = import.meta.env.VITE_RSS_DIRECT_TO_RELAY === 'true';
|
||||
const RSS_PROXY_BASE = isDev
|
||||
? '' // Dev uses Vite's rssProxyPlugin
|
||||
: RSS_DIRECT_TO_RELAY
|
||||
? 'https://proxy.worldmonitor.app'
|
||||
: '';
|
||||
|
||||
export function rssProxyUrl(feedUrl: string): string {
|
||||
if (isDesktopRuntime()) return proxyUrl(feedUrl);
|
||||
if (RSS_PROXY_BASE) {
|
||||
return `${RSS_PROXY_BASE}/rss?url=${encodeURIComponent(feedUrl)}`;
|
||||
}
|
||||
return `/api/rss-proxy?url=${encodeURIComponent(feedUrl)}`;
|
||||
}
|
||||
|
||||
type CachedResponsePayload = {
|
||||
url: string;
|
||||
status: number;
|
||||
|
||||
@@ -13,6 +13,34 @@ const edgeFunctions = readdirSync(apiDir)
|
||||
.filter((f) => f.endsWith('.js') && !f.startsWith('_'))
|
||||
.map((f) => ({ name: f, path: join(apiDir, f) }));
|
||||
|
||||
// ALL .js files in api/ (including helpers) — used for node: built-in checks
|
||||
const allApiFiles = readdirSync(apiDir)
|
||||
.filter((f) => f.endsWith('.js'))
|
||||
.map((f) => ({ name: f, path: join(apiDir, f) }));
|
||||
|
||||
describe('Edge Function shared helpers resolve', () => {
|
||||
it('_rss-allowed-domains.js re-exports shared domain list', async () => {
|
||||
const mod = await import(join(apiDir, '_rss-allowed-domains.js'));
|
||||
const domains = mod.default;
|
||||
assert.ok(Array.isArray(domains), 'Expected default export to be an array');
|
||||
assert.ok(domains.length > 200, `Expected 200+ domains, got ${domains.length}`);
|
||||
assert.ok(domains.includes('feeds.bbci.co.uk'), 'Expected BBC feed domain in list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Function no node: built-ins', () => {
|
||||
for (const { name, path } of allApiFiles) {
|
||||
it(`${name} does not import node: built-ins (unsupported in Vercel Edge Runtime)`, () => {
|
||||
const src = readFileSync(path, 'utf-8');
|
||||
const match = src.match(/from\s+['"]node:(\w+)['"]/);
|
||||
assert.ok(
|
||||
!match,
|
||||
`${name}: imports node:${match?.[1]} — Vercel Edge Runtime does not support node: built-in modules. Use an edge-compatible alternative.`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Edge Function module isolation', () => {
|
||||
for (const { name, path } of edgeFunctions) {
|
||||
it(`${name} does not import from ../server/ (Edge Functions cannot resolve cross-directory TS)`, () => {
|
||||
|
||||
Reference in New Issue
Block a user