SIGN IN SIGN UP
koala73 / worldmonitor UNCLAIMED

Real-time global intelligence dashboard — AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface

44764 0 0 TypeScript
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';
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)
2026-03-04 18:42:00 +04:00
import RSS_ALLOWED_DOMAINS from './_rss-allowed-domains.js';
import { jsonResponse } from './_json-response.js';
export const config = { runtime: 'edge' };
// Domains that consistently block Vercel edge IPs — skip direct fetch,
// go straight to Railway relay to avoid wasted invocation + timeout.
const RELAY_ONLY_DOMAINS = new Set([
'rss.cnn.com',
'www.defensenews.com',
'layoffs.fyi',
'news.un.org',
'www.cisa.gov',
'www.iaea.org',
'www.who.int',
'www.crisisgroup.org',
'english.alarabiya.net',
'www.arabnews.com',
'www.timesofisrael.com',
'www.scmp.com',
'kyivindependent.com',
'www.themoscowtimes.com',
'feeds.24.com',
'feeds.capi24.com',
'islandtimes.org',
'www.atlanticcouncil.org',
]);
const DIRECT_FETCH_HEADERS = Object.freeze({
'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',
});
async function fetchViaRailway(feedUrl, timeoutMs) {
const relayBaseUrl = getRelayBaseUrl();
if (!relayBaseUrl) return null;
const relayUrl = `${relayBaseUrl}/rss?url=${encodeURIComponent(feedUrl)}`;
return fetchWithTimeout(relayUrl, {
headers: getRelayHeaders({
'Accept': 'application/rss+xml, application/xml, text/xml, */*',
'User-Agent': 'WorldMonitor-RSS-Proxy/1.0',
}),
}, timeoutMs);
}
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)
2026-03-04 18:42:00 +04:00
// Allowed RSS feed domains — shared source of truth (shared/rss-allowed-domains.js)
const ALLOWED_DOMAINS = RSS_ALLOWED_DOMAINS;
function isAllowedDomain(hostname) {
const bare = hostname.replace(/^www\./, '');
const withWww = hostname.startsWith('www.') ? hostname : `www.${hostname}`;
return ALLOWED_DOMAINS.includes(hostname) || ALLOWED_DOMAINS.includes(bare) || ALLOWED_DOMAINS.includes(withWww);
}
Triage security alerts (#1903) * fix(cors): use ACAO: * for bootstrap to fix CF cache origin pinning CF ignores Vary: Origin and pins the first request's ACAO header on the cached response. Preview deployments from *.vercel.app got ACAO: worldmonitor.app from CF's cache, blocking CORS. Bootstrap data is fully public (world events, market prices, seismic data) so ACAO: * is safe and allows CF to cache one entry valid for all origins. isDisallowedOrigin() still gates non-cache paths. * chore: finish security triage * fix(aviation): update isArray callback signature for fast-xml-parser 5.5.x fast-xml-parser bumped from 5.4.2 to 5.5.7 changed the isArray callback's second parameter type from string to unknown. Guard with typeof check before calling .test() to satisfy the new type contract. * docs: fix MD032 blank lines around lists in tradingview-screener-integration * fix(security): address code review findings from PR #1903 - api/_json-response.js: add recursion depth limit (20) to sanitizeJsonValue and strip Error.cause chain alongside stack/stackTrace - scripts/ais-relay.cjs: extract WORLD_BANK_COUNTRY_ALLOWLIST to module level to eliminate duplicate; clamp years param to [1,30] to prevent unbounded World Bank date ranges - src-tauri/sidecar/local-api-server.mjs: use JSON.stringify for vq value in inline JS, consistent with safeVideoId/safeOrigin handling - src/services/story-share.ts: simplify sanitizeStoryType to use typed array instead of repeated as-casts * fix(desktop): use parent window origin for YouTube embed postMessage Sidecar youtube-embed route was targeting the iframe's own localhost origin for all window.parent.postMessage calls, so browsers dropped yt-ready/ yt-state/yt-error on Tauri builds where the parent is tauri://localhost or asset://localhost. LiveNewsPanel and LiveWebcamsPanel already pass parentOrigin=window.location.origin in the embed URL; the sidecar now reads, validates, and uses it as the postMessage target for all player event messages. The YT API playerVars origin/widget_referrer continue to use the sidecar's own localhost origin which YouTube requires. Also restore World Bank relay to a generic proxy: replace TECH_INDICATORS membership check with a format-only regex so any valid indicator code (NY.GDP.MKTP.CD etc.) is accepted, not just the 16 tech-sector codes.
2026-03-20 12:37:24 +04:00
function isGoogleNewsFeedUrl(feedUrl) {
try {
return new URL(feedUrl).hostname === 'news.google.com';
} catch {
return false;
}
}
export default async function handler(req) {
const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS');
if (isDisallowedOrigin(req)) {
return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders);
}
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}
if (req.method !== 'GET') {
return jsonResponse({ error: 'Method not allowed' }, 405, corsHeaders);
}
const keyCheck = validateApiKey(req);
if (keyCheck.required && !keyCheck.valid) {
return jsonResponse({ error: keyCheck.error }, 401, corsHeaders);
}
const rateLimitResponse = await checkRateLimit(req, corsHeaders);
if (rateLimitResponse) return rateLimitResponse;
const requestUrl = new URL(req.url);
const feedUrl = requestUrl.searchParams.get('url');
if (!feedUrl) {
return jsonResponse({ error: 'Missing url parameter' }, 400, corsHeaders);
}
try {
const parsedUrl = new URL(feedUrl);
// Security: Check if domain is allowed (normalize www prefix)
const hostname = parsedUrl.hostname;
if (!isAllowedDomain(hostname)) {
return jsonResponse({ error: 'Domain not allowed' }, 403, corsHeaders);
}
const isRelayOnly = RELAY_ONLY_DOMAINS.has(hostname);
// Google News is slow - use longer timeout
Triage security alerts (#1903) * fix(cors): use ACAO: * for bootstrap to fix CF cache origin pinning CF ignores Vary: Origin and pins the first request's ACAO header on the cached response. Preview deployments from *.vercel.app got ACAO: worldmonitor.app from CF's cache, blocking CORS. Bootstrap data is fully public (world events, market prices, seismic data) so ACAO: * is safe and allows CF to cache one entry valid for all origins. isDisallowedOrigin() still gates non-cache paths. * chore: finish security triage * fix(aviation): update isArray callback signature for fast-xml-parser 5.5.x fast-xml-parser bumped from 5.4.2 to 5.5.7 changed the isArray callback's second parameter type from string to unknown. Guard with typeof check before calling .test() to satisfy the new type contract. * docs: fix MD032 blank lines around lists in tradingview-screener-integration * fix(security): address code review findings from PR #1903 - api/_json-response.js: add recursion depth limit (20) to sanitizeJsonValue and strip Error.cause chain alongside stack/stackTrace - scripts/ais-relay.cjs: extract WORLD_BANK_COUNTRY_ALLOWLIST to module level to eliminate duplicate; clamp years param to [1,30] to prevent unbounded World Bank date ranges - src-tauri/sidecar/local-api-server.mjs: use JSON.stringify for vq value in inline JS, consistent with safeVideoId/safeOrigin handling - src/services/story-share.ts: simplify sanitizeStoryType to use typed array instead of repeated as-casts * fix(desktop): use parent window origin for YouTube embed postMessage Sidecar youtube-embed route was targeting the iframe's own localhost origin for all window.parent.postMessage calls, so browsers dropped yt-ready/ yt-state/yt-error on Tauri builds where the parent is tauri://localhost or asset://localhost. LiveNewsPanel and LiveWebcamsPanel already pass parentOrigin=window.location.origin in the embed URL; the sidecar now reads, validates, and uses it as the postMessage target for all player event messages. The YT API playerVars origin/widget_referrer continue to use the sidecar's own localhost origin which YouTube requires. Also restore World Bank relay to a generic proxy: replace TECH_INDICATORS membership check with a format-only regex so any valid indicator code (NY.GDP.MKTP.CD etc.) is accepted, not just the 16 tech-sector codes.
2026-03-20 12:37:24 +04:00
const isGoogleNews = isGoogleNewsFeedUrl(feedUrl);
const timeout = isGoogleNews ? 20000 : 12000;
const fetchDirect = async () => {
const response = await fetchWithTimeout(feedUrl, {
headers: DIRECT_FETCH_HEADERS,
redirect: 'manual',
}, timeout);
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location');
if (location) {
const redirectUrl = new URL(location, feedUrl);
// Apply the same www-normalization as the initial domain check so that
// canonical redirects (e.g. bbc.co.uk → www.bbc.co.uk) are not
// incorrectly rejected when only one form is in the allowlist.
const rHost = redirectUrl.hostname;
if (!isAllowedDomain(rHost)) {
throw new Error('Redirect to disallowed domain');
}
return fetchWithTimeout(redirectUrl.href, {
headers: DIRECT_FETCH_HEADERS,
}, timeout);
}
}
return response;
};
let response;
let usedRelay = false;
if (isRelayOnly) {
// Skip direct fetch entirely — these domains block Vercel IPs
response = await fetchViaRailway(feedUrl, timeout);
usedRelay = !!response;
if (!response) throw new Error(`Railway relay unavailable for relay-only domain: ${hostname}`);
} else {
try {
response = await fetchDirect();
} catch (directError) {
response = await fetchViaRailway(feedUrl, timeout);
usedRelay = !!response;
if (!response) throw directError;
}
if (!response.ok && !usedRelay) {
const relayResponse = await fetchViaRailway(feedUrl, timeout);
feat: harness engineering P0 - linting, testing, architecture docs (#1587) * feat: harness engineering P0 - linting, testing, architecture docs Add foundational infrastructure for agent-first development: - AGENTS.md: agent entry point with progressive disclosure to deeper docs - ARCHITECTURE.md: 12-section system reference with source-file refs and ownership rule - Biome 2.4.7 linter with project-tuned rules, CI workflow (lint-code.yml) - Architectural boundary lint enforcing forward-only dependency direction (lint-boundaries.mjs) - Unit test CI workflow (test.yml), all 1083 tests passing - Fixed 9 pre-existing test failures (bootstrap sync, deploy-config headers, globe parity, redis mocks, geometry URL, import.meta.env null safety) - Fixed 12 architectural boundary violations (types moved to proper layers) - Added 3 missing cache tier entries in gateway.ts - Synced cache-keys.ts with bootstrap.js - Renamed docs/architecture.mdx to "Design Philosophy" with cross-references - Deprecated legacy docs/Docs_To_Review/ARCHITECTURE.md - Harness engineering roadmap tracking doc * fix: address PR review feedback on harness-engineering-p0 - countries-geojson.test.mjs: skip gracefully when CDN unreachable instead of failing CI on network issues - country-geometry-overrides.test.mts: relax timing assertion (250ms -> 2000ms) for constrained CI environments - lint-boundaries.mjs: implement the documented api/ boundary check (was documented but missing, causing false green) * fix(lint): scan api/ .ts files in boundary check The api/ boundary check only scanned .js/.mjs files, missing the 25 sebuf RPC .ts edge functions. Now scans .ts files with correct rules: - Legacy .js: fully self-contained (no server/ or src/ imports) - RPC .ts: may import server/ and src/generated/ (bundled at deploy), but blocks imports from src/ application code * fix(lint): detect import() type expressions in boundary lint - Move AppContext back to app/app-context.ts (aggregate type that references components/services/utils belongs at the top, not types/) - Move HappyContentCategory and TechHQ to types/ (simple enums/interfaces) - Boundary lint now catches import('@/layer') expressions, not just from '@/layer' imports - correlation-engine imports of AppContext marked boundary-ignore (type-only imports of top-level aggregate)
2026-03-14 21:29:21 +04:00
if (relayResponse?.ok) {
response = relayResponse;
}
}
}
const data = await response.text();
const isSuccess = response.status >= 200 && response.status < 300;
// Relay-only feeds are slow-updating institutional sources — cache longer
const cdnTtl = isRelayOnly ? 3600 : 900;
const swr = isRelayOnly ? 7200 : 1800;
const sie = isRelayOnly ? 14400 : 3600;
const browserTtl = isRelayOnly ? 600 : 180;
return new Response(data, {
status: response.status,
headers: {
'Content-Type': response.headers.get('content-type') || 'application/xml',
'Cache-Control': isSuccess
? `public, max-age=${browserTtl}, s-maxage=${cdnTtl}, stale-while-revalidate=${swr}, stale-if-error=${sie}`
: 'public, max-age=15, s-maxage=60, stale-while-revalidate=120',
...(isSuccess && { 'CDN-Cache-Control': `public, s-maxage=${cdnTtl}, stale-while-revalidate=${swr}, stale-if-error=${sie}` }),
...corsHeaders,
},
});
} catch (error) {
const isTimeout = error.name === 'AbortError';
console.error('RSS proxy error:', feedUrl, error.message);
return jsonResponse({
error: isTimeout ? 'Feed timeout' : 'Failed to fetch feed',
details: error.message,
url: feedUrl
}, isTimeout ? 504 : 502, corsHeaders);
}
}