diff --git a/api/rss-allowed-domains.js b/api/rss-allowed-domains.js new file mode 100644 index 00000000..24717000 --- /dev/null +++ b/api/rss-allowed-domains.js @@ -0,0 +1,282 @@ +// Shared RSS proxy domain allowlist — single source of truth. +// Used by both the production Vercel edge function (api/rss-proxy.js) +// and the Vite dev-server RSS proxy plugin (vite/plugins.ts). + +export 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', + 'www.twz.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', + '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.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.indianewsnetwork.com', + // International Organizations + 'news.un.org', + 'www.iaea.org', + 'www.who.int', + 'www.cisa.gov', + 'www.crisisgroup.org', + // Think Tanks & Research + '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', + 'animalpolitico.com', + 'www.proceso.com.mx', + 'www.milenio.com', + 'insightcrime.org', + // Maritime + 'gcaptain.com', + // Additional + 'news.ycombinator.com', + // Finance variant + 'www.coindesk.com', + 'cointelegraph.com', + // Security advisories — government travel advisory feeds + 'travel.state.gov', + 'www.smartraveller.gov.au', + '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.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', +]; diff --git a/api/rss-proxy.js b/api/rss-proxy.js index f677acc0..40e08894 100644 --- a/api/rss-proxy.js +++ b/api/rss-proxy.js @@ -1,5 +1,6 @@ // Non-sebuf: returns XML/HTML, stays as standalone Vercel function import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; +import { ALLOWED_DOMAINS } from './rss-allowed-domains.js'; export const config = { runtime: 'edge' }; @@ -44,286 +45,7 @@ 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', - '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.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.indianewsnetwork.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', - 'animalpolitico.com', - 'www.proceso.com.mx', - 'www.milenio.com', - 'insightcrime.org', - // Additional - 'news.ycombinator.com', - // Finance variant - 'seekingalpha.com', - 'www.coindesk.com', - 'cointelegraph.com', - // Security advisories — government travel advisory feeds - 'travel.state.gov', - 'www.smartraveller.gov.au', - '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', -]; + export default async function handler(req) { const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS'); diff --git a/package-lock.json b/package-lock.json index 9a6f1f69..b4c8f547 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@types/papaparse": "^5.5.2", "@types/topojson-client": "^3.1.5", "@types/topojson-specification": "^1.0.5", + "cross-env": "^7.0.3", "esbuild": "^0.27.3", "markdownlint-cli2": "^0.20.0", "typescript": "^5.7.2", @@ -54,7 +55,6 @@ "resolved": "https://registry.npmjs.org/@amcharts/amcharts5/-/amcharts5-5.14.4.tgz", "integrity": "sha512-Tl7wQLWvsvyWVtlCIm1yhZtJviSDYjuNTnlUkO0D49GyByoK0nb9fr0DK4rUw4DVgyLcySxWBsb2lzTJm5Rd9Q==", "license": "SEE LICENSE IN LICENSE", - "peer": true, "dependencies": { "@types/d3": "^7.0.0", "@types/d3-chord": "^3.0.0", @@ -87,8 +87,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz", "integrity": "sha512-QwjxA3+YCKH3N1Rs3uSiSy1bdxlLB1uUiENXeJudBoAFvtDuswUxLcanoOaR2JYn1melDTuIXR8VhnVyI3yG/A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@apideck/better-ajv-errors": { "version": "0.3.6", @@ -131,7 +130,6 @@ "resolved": "https://registry.npmjs.org/@arcgis/lumina/-/lumina-4.34.9.tgz", "integrity": "sha512-efqO+SwR+1IYf29AATh1l2FUeypRyRINTBNkaJY+KkaFe+8gqSJ45qOmputhyzF5WTRDb7WhOYgnChjp6VYPpA==", "license": "SEE LICENSE IN LICENSE.md", - "peer": true, "dependencies": { "@arcgis/toolkit": "~4.34.9", "csstype": "^3.1.3", @@ -152,7 +150,6 @@ "resolved": "https://registry.npmjs.org/@arcgis/toolkit/-/toolkit-4.34.9.tgz", "integrity": "sha512-wFST+eVnCwmg9NyICVyn9bsBnR+TlWklsGqG3L7xqSTgfXo6TuCThE7wtTb8xWxsTBkGvImqMUgpgLuwQuTQ1g==", "license": "SEE LICENSE IN LICENSE.md", - "peer": true, "dependencies": { "tslib": "^2.8.1" } @@ -188,6 +185,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1756,6 +1754,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/aggregation-layers/-/aggregation-layers-9.2.6.tgz", "integrity": "sha512-T42ZwB63KI4+0pe2HBwMQS7qnqyv3LlqAQfRSHBlFZMzBq72SxIgk9BzhrT16uBHxFFjjMh6K5g28/UfDOXQEg==", "license": "MIT", + "peer": true, "dependencies": { "@luma.gl/constants": "^9.2.6", "@luma.gl/shadertools": "^9.2.6", @@ -1853,6 +1852,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.2.6.tgz", "integrity": "sha512-bBFfwfythPPpXS/OKUMvziQ8td84mRGMnYZfqdUvfUVltzjFtQCBQUJTzgo3LubvOzSnzo8GTWskxHaZzkqdKQ==", "license": "MIT", + "peer": true, "dependencies": { "@loaders.gl/core": "^4.2.0", "@loaders.gl/images": "^4.2.0", @@ -1878,6 +1878,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/extensions/-/extensions-9.2.6.tgz", "integrity": "sha512-HNuzo76mD6Ykc/xMEyCMH+to6/Xi+7ehG3VYToSm+R3196Ki5p58pyRHzvq9CrBDvFd3SLMe9QqRm2GTg3wn/w==", "license": "MIT", + "peer": true, "dependencies": { "@luma.gl/constants": "^9.2.6", "@luma.gl/shadertools": "^9.2.6", @@ -1894,6 +1895,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/geo-layers/-/geo-layers-9.2.6.tgz", "integrity": "sha512-Js42GcAlzH5vHWHdg/eKSmFvx1TWlhW+d6p8Y+67/iHpcCXmx/CBmpsr1ZsQ8XYc+GY8NDAmkHe5KECDJsJiDg==", "license": "MIT", + "peer": true, "dependencies": { "@loaders.gl/3d-tiles": "^4.2.0", "@loaders.gl/gis": "^4.2.0", @@ -1967,6 +1969,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/layers/-/layers-9.2.6.tgz", "integrity": "sha512-ASwL5CHm/QX+fVW+MejmtA/84RKO0BaL2/Fv9N+l+WcSII2M5s730rrTw3JgyQ66AUGFPe1SL3JDkqsUlVJ0yg==", "license": "MIT", + "peer": true, "dependencies": { "@loaders.gl/images": "^4.2.0", "@loaders.gl/schema": "^4.2.0", @@ -2005,6 +2008,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/mesh-layers/-/mesh-layers-9.2.6.tgz", "integrity": "sha512-/KjhjoQJRb9lUcDE6pZlHvcto9H5iBCJtUb1/uCb8fahzEAcZBDubAn4RUWjfRyOSmzJfQHrWdNAjflNkL87Yg==", "license": "MIT", + "peer": true, "dependencies": { "@loaders.gl/gltf": "^4.2.0", "@loaders.gl/schema": "^4.2.0", @@ -2036,6 +2040,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/widgets/-/widgets-9.2.6.tgz", "integrity": "sha512-WkKP+HB90x1qwOxs5l6Dg0d1iAvf999jJGDdGUbDVsRF7+hJDv03GZY6XKpoiEW7VfcZ1y1iU2vRwV/GHuQ57g==", "license": "MIT", + "peer": true, "dependencies": { "preact": "^10.17.0" }, @@ -2491,7 +2496,6 @@ "resolved": "https://registry.npmjs.org/@esri/arcgis-html-sanitizer/-/arcgis-html-sanitizer-4.1.0.tgz", "integrity": "sha512-einEveDJ/k1180NOp78PB/4Hje9eBy3dyOGLLtLn6bSkizpUfCwuYBIXOA7Y3F/k/BsTQXgKqUVwQ0eiscWMdA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "xss": "1.0.13" }, @@ -2504,7 +2508,6 @@ "resolved": "https://registry.npmjs.org/@esri/calcite-components/-/calcite-components-3.3.3.tgz", "integrity": "sha512-tw+EfJ3pb+Odj71W6E9GUkm8rMbNxfW1KeiI8GgsKDzhr39hMKwY+zYYFFYuO0FONxWGvAB+B8yqB0NvH7WeHw==", "license": "SEE LICENSE.md", - "peer": true, "dependencies": { "@arcgis/lumina": ">=4.34.0-next.158 <4.35.0", "@arcgis/toolkit": ">=4.34.0-next.158 <4.35.0", @@ -2528,7 +2531,6 @@ "resolved": "https://registry.npmjs.org/@esri/calcite-ui-icons/-/calcite-ui-icons-4.3.0.tgz", "integrity": "sha512-iOOuRurpjFxFVw6+aXW2JpSkRBrdOpBcbdibfPOmSPqMd1aoHBtYmYXetKoH9vfrXoBiPyO2PkDnczhsu/N9IA==", "license": "SEE LICENSE.md", - "peer": true, "bin": { "spriter": "bin/spriter.js" } @@ -2538,7 +2540,6 @@ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/utils": "^0.2.10" } @@ -2548,7 +2549,6 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" @@ -2566,7 +2566,6 @@ "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.2.tgz", "integrity": "sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA==", "license": "MIT", - "peer": true, "dependencies": { "@foliojs-fork/restructure": "^2.0.2", "brotli": "^1.2.0", @@ -2583,7 +2582,6 @@ "resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.2.tgz", "integrity": "sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg==", "license": "MIT", - "peer": true, "dependencies": { "base64-js": "1.3.1", "unicode-trie": "^2.0.0" @@ -2593,15 +2591,13 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@foliojs-fork/pdfkit": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/@foliojs-fork/pdfkit/-/pdfkit-0.15.3.tgz", "integrity": "sha512-Obc0Wmy3bm7BINFVvPhcl2rnSSK61DQrlHU8aXnAqDk9LCjWdUOPwhgD8Ywz5VtuFjRxmVOM/kQ/XLIBjDvltw==", "license": "MIT", - "peer": true, "dependencies": { "@foliojs-fork/fontkit": "^1.9.2", "@foliojs-fork/linebreak": "^1.1.1", @@ -2614,8 +2610,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz", "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@huggingface/jinja": { "version": "0.2.2", @@ -2630,8 +2625,7 @@ "version": "1.10.27", "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.27.tgz", "integrity": "sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@isaacs/cliui": { "version": "9.0.0", @@ -2708,15 +2702,13 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@lit/reactive-element": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } @@ -2781,6 +2773,7 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.4.tgz", "integrity": "sha512-cG0C5fMZ1jyW6WCsf4LoHGvaIAJCEVA/ioqKoYRwoSfXkOf+17KupK1OUQyUCw5XoRn+oWA1FulJQOYlXnb9Gw==", "license": "MIT", + "peer": true, "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/schema": "4.3.4", @@ -3032,13 +3025,15 @@ "version": "9.2.6", "resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.2.6.tgz", "integrity": "sha512-rvFFrJuSm5JIWbDHFuR4Q2s4eudO3Ggsv0TsGKn9eqvO7bBiPm/ANugHredvh3KviEyYuMZZxtfJvBdr3kzldg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@luma.gl/core": { "version": "9.2.6", "resolved": "https://registry.npmjs.org/@luma.gl/core/-/core-9.2.6.tgz", "integrity": "sha512-d8KcH8ZZcjDAodSN/G2nueA9YE2X8kMz7Q0OxDGpCww6to1MZXM3Ydate/Jqsb5DDKVgUF6yD6RL8P5jOki9Yw==", "license": "MIT", + "peer": true, "dependencies": { "@math.gl/types": "^4.1.0", "@probe.gl/env": "^4.0.8", @@ -3052,6 +3047,7 @@ "resolved": "https://registry.npmjs.org/@luma.gl/engine/-/engine-9.2.6.tgz", "integrity": "sha512-1AEDs2AUqOWh7Wl4onOhXmQF+Rz1zNdPXF+Kxm4aWl92RQ42Sh2CmTvRt2BJku83VQ91KFIEm/v3qd3Urzf+Uw==", "license": "MIT", + "peer": true, "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0", @@ -3086,6 +3082,7 @@ "resolved": "https://registry.npmjs.org/@luma.gl/shadertools/-/shadertools-9.2.6.tgz", "integrity": "sha512-4+uUbynqPUra9d/z1nQChyHmhLgmKfSMjS7kOwLB6exSnhKnpHL3+Hu9fv55qyaX50nGH3oHawhGtJ6RRvu65w==", "license": "MIT", + "peer": true, "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0", @@ -3100,6 +3097,7 @@ "resolved": "https://registry.npmjs.org/@luma.gl/webgl/-/webgl-9.2.6.tgz", "integrity": "sha512-NGBTdxJMk7j8Ygr1zuTyAvr1Tw+EpupMIQo7RelFjEsZXg6pujFqiDMM+rgxex8voCeuhWBJc7Rs+MoSqd46UQ==", "license": "MIT", + "peer": true, "dependencies": { "@luma.gl/constants": "9.2.6", "@math.gl/types": "^4.1.0", @@ -3358,14 +3356,14 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz", "integrity": "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3628,7 +3626,6 @@ "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.5.2.tgz", "integrity": "sha512-fWwImY/UH4bb2534DVSaX+Azs2yKg8slkMBHOyGeU2kKx7Xmxp6Lee0jP8p6B3d7c1gFUPB2Z976dTUtX81pQA==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@webcomponents/shadycss": "^1.9.1" } @@ -4741,7 +4738,6 @@ "resolved": "https://registry.npmjs.org/@types/d3-sankey/-/d3-sankey-0.11.2.tgz", "integrity": "sha512-U6SrTWUERSlOhnpSrgvMX64WblX1AxX6nEjI2t3mLK2USpQrnbwYYK+AS9SwiE7wgYmOsSSKoSdr8aoKBH0HgQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/d3-shape": "^1" } @@ -4750,15 +4746,13 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/d3-sankey/node_modules/@types/d3-shape": { "version": "1.3.12", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", "license": "MIT", - "peer": true, "dependencies": { "@types/d3-path": "^1" } @@ -4933,8 +4927,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz", "integrity": "sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/resolve": { "version": "1.20.2", @@ -4947,8 +4940,7 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/supercluster": { "version": "7.1.3", @@ -4963,8 +4955,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/@types/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.3.tgz", "integrity": "sha512-UNOnbTtl0nVTm8hwKaz5R5VZRvSulFMGojO5+Q7yucKxBoCaTtS4ibSQVRHo5VW5AaRo145U8p1Vfg5KrYe9Bg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/topojson-client": { "version": "3.1.5", @@ -5014,7 +5005,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/a11y-base/-/a11y-base-24.9.10.tgz", "integrity": "sha512-76KNDhKn8zyqzWwNWx0BcYNQXtEdoq0FgMR7vYz8qSj4zGvu8wf0GuQavTI7Nnia8pk0jRqT2/NZrJR3YLCLJQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", @@ -5027,7 +5017,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/checkbox/-/checkbox-24.9.10.tgz", "integrity": "sha512-08CnG3T02iHTtXD2SVrW+RHFwTOgSq9JvV8edijAxdX27cRbVJGJX2M1zupPLUEtWJEZK5uvK/2HkJzDrTjBdA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", @@ -5045,7 +5034,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/component-base/-/component-base-24.9.10.tgz", "integrity": "sha512-CM9ZligxBd+PJKLEHiz8YVvPGm5XAuJ5YzKUTmslqTo8aPgXWJBchbNyf47xL7XwIWCVy3sfNZYDHGN7zuMJ8A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", @@ -5059,7 +5047,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/field-base/-/field-base-24.9.10.tgz", "integrity": "sha512-t4x1HCOESJ7mWxgS7aiwPJVkf00MXbEs43p24JYsEWr78Ivn+4k1+5SZ2mli0HgkmVn89aUbMqkU10YpHIN4Yw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", @@ -5073,7 +5060,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/grid/-/grid-24.9.10.tgz", "integrity": "sha512-9VVnRw4bAwHVIpan8rqMfTJRQ3WbtRxoTrySczZlnQmWaQiBphaXsIdhd9DUy9OjRzteVTHnU6mtuH1aZJl8XA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", @@ -5093,7 +5079,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/icon/-/icon-24.9.10.tgz", "integrity": "sha512-3HAn5vesU9gPBN8loGjajaOxEsTkNo1xdEiRQ6s8KA81TyORBH49O4dGprnUUoRA1sOtwNcnck2WAGa7Imh+Yg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", @@ -5108,7 +5093,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/input-container/-/input-container-24.9.10.tgz", "integrity": "sha512-c/y5RXuNsb4IUFdJKhXCfvihk35N5Ztk7nBJ0XRaOTqf6I9tPgwVeq8Gj/VcHbwNBw67pv7VLxF/5OuJIsgthA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@polymer/polymer": "^3.0.0", "@vaadin/component-base": "~24.9.10", @@ -5123,7 +5107,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/lit-renderer/-/lit-renderer-24.9.10.tgz", "integrity": "sha512-1GLggQZyG5qh2OtuidiKVOS83GS9qGWuGgZk2u676AirH/rcsg6O4sABstrNCU/TTOLeo1rTfPC6j0DiC9uXfg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "lit": "^3.0.0" } @@ -5133,7 +5116,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/text-field/-/text-field-24.9.10.tgz", "integrity": "sha512-8kJKH7EdAuvdRXO+ckOLhIvy/syFa0PM7JD/y20kSZC5MWQx7wCbXH4uKddHj8JUnak217WcZfvcJ6GaD2lmWA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", @@ -5151,15 +5133,13 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@vaadin/vaadin-development-mode-detector/-/vaadin-development-mode-detector-2.0.7.tgz", "integrity": "sha512-9FhVhr0ynSR3X2ao+vaIEttcNU5XfzCbxtmYOV8uIRnUCtNgbvMOIcyGBvntsX9I5kvIP2dV3cFAOG9SILJzEA==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@vaadin/vaadin-lumo-styles": { "version": "24.9.10", "resolved": "https://registry.npmjs.org/@vaadin/vaadin-lumo-styles/-/vaadin-lumo-styles-24.9.10.tgz", "integrity": "sha512-NXUxrl537GrwJG07usUwyDYPVL7aUEBZALGLiTJ+A0om69q155hbpFchPPVexLjBHRn8y7Cdnox+VH/TIJBqBw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@polymer/polymer": "^3.0.0", "@vaadin/component-base": "~24.9.10", @@ -5172,7 +5152,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/vaadin-material-styles/-/vaadin-material-styles-24.9.10.tgz", "integrity": "sha512-jkDiWqqHHGPQ/SqILUheb2Nf/yRssosxu42Qe/e3N8j+Hc2uJb3yN4k9DuR8S2dmfGR3WKi16kWxaXKwlkXMYQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@polymer/polymer": "^3.0.0", "@vaadin/component-base": "~24.9.10", @@ -5184,7 +5163,6 @@ "resolved": "https://registry.npmjs.org/@vaadin/vaadin-themable-mixin/-/vaadin-themable-mixin-24.9.10.tgz", "integrity": "sha512-2JG9hmM9REQx2GSzZ6/16/fIgBhNP+btil896GFTsj9ZTwMcPTyvZ7/uP8B8Gnm6MGoyGr0nNoeE9/M3dNpGPQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "lit": "^3.0.0", @@ -5197,7 +5175,6 @@ "integrity": "sha512-8r4TNknD7OJQADe3VygeofFR7UNAXZ2/jjBFP5dgI8+2uMfnuGYgbuHivasKr9WSQ64sPej6m8rDoM1uSllXjQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@vaadin/vaadin-development-mode-detector": "^2.0.0" }, @@ -5247,8 +5224,7 @@ "version": "1.11.2", "resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.11.2.tgz", "integrity": "sha512-vRq+GniJAYSBmTRnhCYPAPq6THYqovJ/gzGThWbgEZUQaBccndGTi1hdiUP15HzEco0I6t4RCtXyX0rsSmwgPw==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xenova/transformers": { "version": "2.17.2", @@ -5295,7 +5271,6 @@ "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.15.tgz", "integrity": "sha512-HZKJLFe4eGVgCe9J87PnijY7T1Zn638bEHS+Fm/ygHZozRpefzWcOYfPaP52S8pqk9g4xN3+LzMDl3Lv9dLglA==", "license": "BSD-3-Clause", - "peer": true, "engines": { "bun": ">=0.7.0", "deno": ">=1.0.0", @@ -5330,6 +5305,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5725,6 +5701,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5919,7 +5896,6 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.8" } @@ -5929,7 +5905,6 @@ "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" @@ -5943,7 +5918,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "^2.0.0" }, @@ -5956,7 +5930,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.20" } @@ -5966,7 +5939,6 @@ "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "^2.0.0" }, @@ -6009,7 +5981,6 @@ "resolved": "https://registry.npmjs.org/composed-offset-position/-/composed-offset-position-0.0.6.tgz", "integrity": "sha512-Q7dLompI6lUwd7LWyIcP66r4WcS9u7AL2h8HaeipiRfCRPLMWqRx8fYsjb4OHi6UQFifO7XtNC2IlEJ1ozIFxw==", "license": "MIT", - "peer": true, "peerDependencies": { "@floating-ui/utils": "^0.2.5" } @@ -6577,6 +6548,25 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6604,8 +6594,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/crypto-random-string": { "version": "2.0.0", @@ -6621,15 +6610,13 @@ "version": "0.0.10", "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3": { "version": "7.9.0", @@ -6925,7 +6912,6 @@ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" @@ -6936,7 +6922,6 @@ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "internmap": "^1.0.0" } @@ -6945,15 +6930,13 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/d3-sankey/node_modules/d3-shape": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "d3-path": "1" } @@ -6962,8 +6945,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/d3-scale": { "version": "4.0.2", @@ -6999,6 +6981,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -7072,7 +7055,6 @@ "resolved": "https://registry.npmjs.org/d3-voronoi-map/-/d3-voronoi-map-2.1.1.tgz", "integrity": "sha512-mCXfz/kD9IQxjHaU2IMjkO8fSo4J6oysPR2iL+omDsCy1i1Qn6BQ/e4hEAW8C6ms2kfuHwqtbNom80Hih94YsA==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "d3-dispatch": "2.*", "d3-polygon": "2.*", @@ -7084,29 +7066,25 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/d3-voronoi-map/node_modules/d3-polygon": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz", "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/d3-voronoi-map/node_modules/d3-timer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/d3-voronoi-treemap": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/d3-voronoi-treemap/-/d3-voronoi-treemap-1.1.2.tgz", "integrity": "sha512-7odu9HdG/yLPWwzDteJq4yd9Q/NwgQV7IE/u36VQtcCK7k1sZwDqbkHCeMKNTBsq5mQjDwolTsrXcU0j8ZEMCA==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "d3-voronoi-map": "2.*" } @@ -7116,7 +7094,6 @@ "resolved": "https://registry.npmjs.org/d3-weighted-voronoi/-/d3-weighted-voronoi-1.1.3.tgz", "integrity": "sha512-C3WdvSKl9aqhAy+f3QT3PPsQG6V+ajDfYO3BSclQDSD+araW2xDBFIH67aKzsSuuuKaX8K2y2dGq1fq/dWTVig==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "d3-array": "2", "d3-polygon": "2" @@ -7127,7 +7104,6 @@ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "internmap": "^1.0.0" } @@ -7136,15 +7112,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz", "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/d3-weighted-voronoi/node_modules/internmap": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/d3-zoom": { "version": "3.0.0", @@ -7308,7 +7282,6 @@ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", "license": "MIT", - "peer": true, "dependencies": { "is-arguments": "^1.1.1", "is-date-object": "^1.0.5", @@ -7435,8 +7408,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { "version": "3.3.1", @@ -7656,7 +7628,6 @@ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", "license": "MIT", - "peer": true, "workspaces": [ "docs", "benchmarks" @@ -7925,15 +7896,13 @@ "version": "4.6.13", "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/focus-trap": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -8488,7 +8457,6 @@ "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.27.tgz", "integrity": "sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==", "license": "MIT", - "peer": true, "dependencies": { "@interactjs/types": "1.10.27" } @@ -8548,7 +8516,6 @@ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -9081,8 +9048,7 @@ "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/js-tokens": { "version": "4.0.0", @@ -9293,7 +9259,6 @@ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", @@ -9305,7 +9270,6 @@ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@types/trusted-types": "^2.0.2" } @@ -9355,7 +9319,6 @@ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" } @@ -9511,6 +9474,7 @@ "integrity": "sha512-esPk+8Qvx/f0bzI7YelUeZp+jCtFOk3KjZ7s9iBQZ6HlymSXoTtWGiIRZP05/9Oy2ehIoIjenVwndxGtxOIJYQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "globby": "15.0.0", "js-yaml": "4.1.1", @@ -9548,7 +9512,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-16.3.0.tgz", "integrity": "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -9560,8 +9523,7 @@ "version": "2.32.7", "resolved": "https://registry.npmjs.org/markerjs2/-/markerjs2-2.32.7.tgz", "integrity": "sha512-HeFRZjmc43DOG3lSQp92z49cq2oCYpYn2pX++SkJAW1Dij4xJtRquVRf+cXeSZQWDX3ufns1Ry/bGk+zveP7rA==", - "license": "SEE LICENSE IN LICENSE", - "peer": true + "license": "SEE LICENSE IN LICENSE" }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -10333,7 +10295,6 @@ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" @@ -10603,7 +10564,6 @@ "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.23.tgz", "integrity": "sha512-A/IksoKb/ikOZH1edSDJ/2zBbqJKDghD4+fXn3rT7quvCJDlsZMs3NmIB3eajLMMFU9Bd3bZPVvlUMXhvFI+bQ==", "license": "MIT", - "peer": true, "dependencies": { "@foliojs-fork/linebreak": "^1.1.2", "@foliojs-fork/pdfkit": "^0.15.3", @@ -10619,7 +10579,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -10707,15 +10666,13 @@ "node_modules/png-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", - "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==", - "peer": true + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" }, "node_modules/polylabel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz", "integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==", "license": "ISC", - "peer": true, "dependencies": { "tinyqueue": "^2.0.3" } @@ -10724,8 +10681,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/possible-typed-array-names": { "version": "1.1.0", @@ -11404,7 +11360,6 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", "license": "BlueOak-1.0.0", - "peer": true, "engines": { "node": ">=11.0.0" } @@ -11413,15 +11368,13 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "7.7.3", @@ -11759,8 +11712,7 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/source-map": { "version": "0.8.0-beta.0", @@ -12035,8 +11987,7 @@ "url": "https://opencollective.com/leaverou" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/supercluster": { "version": "8.0.1", @@ -12064,15 +12015,13 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/tabbable": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tar-fs": { "version": "3.1.1", @@ -12147,6 +12096,7 @@ "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -12194,7 +12144,6 @@ "resolved": "https://registry.npmjs.org/timezone-groups/-/timezone-groups-0.10.4.tgz", "integrity": "sha512-AnkJYrbb7uPkDCEqGeVJiawZNiwVlSkkeX4jZg1gTEguClhyX+/Ezn07KB6DT29tG3UN418ldmS/W6KqGOTDjg==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.12.0" } @@ -12203,8 +12152,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -12276,8 +12224,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -12296,7 +12243,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=16" }, @@ -12388,6 +12334,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12473,7 +12420,6 @@ "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", "license": "MIT", - "peer": true, "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" @@ -12494,7 +12440,6 @@ "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", "license": "MIT", - "peer": true, "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" @@ -12504,8 +12449,7 @@ "version": "0.2.9", "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.3.0", @@ -13495,6 +13439,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -13675,7 +13620,6 @@ "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-2.0.3.tgz", "integrity": "sha512-6gRk4NY/Jvg67xn7OzJuxLRsGgiXBaPUQplVJ/9l99uIugxh4FTOewYz5ic8WScj7Xx/2WvhENiQKwkK9RpE4w==", "license": "MIT", - "peer": true, "dependencies": { "sax": "^1.4.3" }, @@ -13688,7 +13632,6 @@ "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.13.tgz", "integrity": "sha512-clu7dxTm1e8Mo5fz3n/oW3UCXBfV89xZ72jM8yzo1vR/pIS0w3sgB3XV2H8Vm6zfGnHL0FzvLJPJEBhd86/z4Q==", "license": "MIT", - "peer": true, "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" @@ -13704,8 +13647,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/yallist": { "version": "3.1.1", diff --git a/package.json b/package.json index bf04cc27..d6458ef9 100644 --- a/package.json +++ b/package.json @@ -9,37 +9,37 @@ "version:sync": "node scripts/sync-desktop-version.mjs", "version:check": "node scripts/sync-desktop-version.mjs --check", "dev": "vite", - "dev:tech": "VITE_VARIANT=tech vite", - "dev:finance": "VITE_VARIANT=finance vite", - "dev:happy": "VITE_VARIANT=happy vite", + "dev:tech": "cross-env VITE_VARIANT=tech vite", + "dev:finance": "cross-env VITE_VARIANT=finance vite", + "dev:happy": "cross-env VITE_VARIANT=happy vite", "build": "tsc && vite build", "build:sidecar-sebuf": "node scripts/build-sidecar-sebuf.mjs", "build:desktop": "node scripts/build-sidecar-sebuf.mjs && tsc && vite build", - "build:full": "VITE_VARIANT=full tsc && VITE_VARIANT=full vite build", - "build:tech": "VITE_VARIANT=tech tsc && VITE_VARIANT=tech vite build", - "build:finance": "VITE_VARIANT=finance tsc && VITE_VARIANT=finance vite build", - "build:happy": "VITE_VARIANT=happy tsc && VITE_VARIANT=happy vite build", + "build:full": "cross-env-shell VITE_VARIANT=full \"tsc && vite build\"", + "build:tech": "cross-env-shell VITE_VARIANT=tech \"tsc && vite build\"", + "build:finance": "cross-env-shell VITE_VARIANT=finance \"tsc && vite build\"", + "build:happy": "cross-env-shell VITE_VARIANT=happy \"tsc && vite build\"", "typecheck": "tsc --noEmit", "tauri": "tauri", "preview": "vite preview", - "test:e2e:full": "VITE_VARIANT=full playwright test", - "test:e2e:tech": "VITE_VARIANT=tech playwright test", - "test:e2e:finance": "VITE_VARIANT=finance playwright test", - "test:e2e:runtime": "VITE_VARIANT=full playwright test e2e/runtime-fetch.spec.ts", + "test:e2e:full": "cross-env VITE_VARIANT=full playwright test", + "test:e2e:tech": "cross-env VITE_VARIANT=tech playwright test", + "test:e2e:finance": "cross-env VITE_VARIANT=finance playwright test", + "test:e2e:runtime": "cross-env VITE_VARIANT=full playwright test e2e/runtime-fetch.spec.ts", "test:e2e": "npm run test:e2e:runtime && npm run test:e2e:full && npm run test:e2e:tech && npm run test:e2e:finance", "test:data": "node --test tests/*.test.mjs", "test:feeds": "node scripts/validate-rss-feeds.mjs", "test:sidecar": "node --test src-tauri/sidecar/local-api-server.test.mjs api/_cors.test.mjs api/youtube/embed.test.mjs api/cyber-threats.test.mjs api/usni-fleet.test.mjs scripts/ais-relay-rss.test.cjs api/loaders-xml-wms-regression.test.mjs", - "test:e2e:visual:full": "VITE_VARIANT=full playwright test -g \"matches golden screenshots per layer and zoom\"", - "test:e2e:visual:tech": "VITE_VARIANT=tech playwright test -g \"matches golden screenshots per layer and zoom\"", + "test:e2e:visual:full": "cross-env VITE_VARIANT=full playwright test -g \"matches golden screenshots per layer and zoom\"", + "test:e2e:visual:tech": "cross-env VITE_VARIANT=tech playwright test -g \"matches golden screenshots per layer and zoom\"", "test:e2e:visual": "npm run test:e2e:visual:full && npm run test:e2e:visual:tech", - "test:e2e:visual:update:full": "VITE_VARIANT=full playwright test -g \"matches golden screenshots per layer and zoom\" --update-snapshots", - "test:e2e:visual:update:tech": "VITE_VARIANT=tech playwright test -g \"matches golden screenshots per layer and zoom\" --update-snapshots", + "test:e2e:visual:update:full": "cross-env VITE_VARIANT=full playwright test -g \"matches golden screenshots per layer and zoom\" --update-snapshots", + "test:e2e:visual:update:tech": "cross-env VITE_VARIANT=tech playwright test -g \"matches golden screenshots per layer and zoom\" --update-snapshots", "test:e2e:visual:update": "npm run test:e2e:visual:update:full && npm run test:e2e:visual:update:tech", - "desktop:dev": "npm run version:sync && VITE_DESKTOP_RUNTIME=1 tauri dev -f devtools", - "desktop:build:full": "npm run version:sync && VITE_VARIANT=full VITE_DESKTOP_RUNTIME=1 tauri build", - "desktop:build:tech": "npm run version:sync && VITE_VARIANT=tech VITE_DESKTOP_RUNTIME=1 tauri build --config src-tauri/tauri.tech.conf.json", - "desktop:build:finance": "npm run version:sync && VITE_VARIANT=finance VITE_DESKTOP_RUNTIME=1 tauri build --config src-tauri/tauri.finance.conf.json", + "desktop:dev": "npm run version:sync && cross-env-shell VITE_DESKTOP_RUNTIME=1 \"tauri dev -f devtools\"", + "desktop:build:full": "npm run version:sync && cross-env-shell VITE_VARIANT=full VITE_DESKTOP_RUNTIME=1 \"tauri build\"", + "desktop:build:tech": "npm run version:sync && cross-env-shell VITE_VARIANT=tech VITE_DESKTOP_RUNTIME=1 \"tauri build --config src-tauri/tauri.tech.conf.json\"", + "desktop:build:finance": "npm run version:sync && cross-env-shell VITE_VARIANT=finance VITE_DESKTOP_RUNTIME=1 \"tauri build --config src-tauri/tauri.finance.conf.json\"", "desktop:package:macos:full": "node scripts/desktop-package.mjs --os macos --variant full", "desktop:package:macos:tech": "node scripts/desktop-package.mjs --os macos --variant tech", "desktop:package:windows:full": "node scripts/desktop-package.mjs --os windows --variant full", @@ -51,6 +51,7 @@ "desktop:package": "node scripts/desktop-package.mjs" }, "devDependencies": { + "cross-env": "^7.0.3", "@playwright/test": "^1.52.0", "@tauri-apps/cli": "^2.10.0", "@types/canvas-confetti": "^1.9.0", diff --git a/vite.config.ts b/vite.config.ts index 25f9a6b0..61a70649 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,675 +1,31 @@ -import { defineConfig, type Plugin } from 'vite'; +import { defineConfig } from 'vite'; import { VitePWA } from 'vite-plugin-pwa'; -import { resolve, dirname, extname } from 'path'; -import { mkdir, readFile, writeFile } from 'fs/promises'; -import { brotliCompress } from 'zlib'; -import { promisify } from 'util'; +import { resolve } from 'path'; import pkg from './package.json'; +// Modularized configurations +import { VARIANT_META } from './vite/variants'; +import { proxyConfig } from './vite/proxy'; +import { + brotliPrecompressPlugin, + htmlVariantPlugin, + polymarketPlugin, + rssProxyPlugin, + youtubeLivePlugin, + sebufApiPlugin +} from './vite/plugins'; + const isE2E = process.env.VITE_E2E === '1'; const isDesktopBuild = process.env.VITE_DESKTOP_RUNTIME === '1'; - -const brotliCompressAsync = promisify(brotliCompress); -const BROTLI_EXTENSIONS = new Set(['.js', '.mjs', '.css', '.html', '.svg', '.json', '.txt', '.xml', '.wasm']); - -function brotliPrecompressPlugin(): Plugin { - return { - name: 'brotli-precompress', - apply: 'build', - async writeBundle(outputOptions, bundle) { - const outDir = outputOptions.dir; - if (!outDir) return; - - await Promise.all(Object.keys(bundle).map(async (fileName) => { - const extension = extname(fileName).toLowerCase(); - if (!BROTLI_EXTENSIONS.has(extension)) return; - - const sourcePath = resolve(outDir, fileName); - const compressedPath = `${sourcePath}.br`; - const sourceBuffer = await readFile(sourcePath); - if (sourceBuffer.length < 1024) return; - - const compressedBuffer = await brotliCompressAsync(sourceBuffer); - await mkdir(dirname(compressedPath), { recursive: true }); - await writeFile(compressedPath, compressedBuffer); - })); - }, - }; -} - -const VARIANT_META: Record = { - full: { - title: 'World Monitor - Real-Time Global Intelligence Dashboard', - description: 'Real-time global intelligence dashboard with live news, markets, military tracking, infrastructure monitoring, and geopolitical data. OSINT in one view.', - keywords: 'global intelligence, geopolitical dashboard, world news, market data, military bases, nuclear facilities, undersea cables, conflict zones, real-time monitoring, situation awareness, OSINT, flight tracking, AIS ships, earthquake monitor, protest tracker, power outages, oil prices, government spending, polymarket predictions', - url: 'https://worldmonitor.app/', - siteName: 'World Monitor', - shortName: 'WorldMonitor', - subject: 'Real-Time Global Intelligence and Situation Awareness', - classification: 'Intelligence Dashboard, OSINT Tool, News Aggregator', - categories: ['news', 'productivity'], - features: [ - 'Real-time news aggregation', - 'Stock market tracking', - 'Military flight monitoring', - 'Ship AIS tracking', - 'Earthquake alerts', - 'Protest tracking', - 'Power outage monitoring', - 'Oil price analytics', - 'Government spending data', - 'Prediction markets', - 'Infrastructure monitoring', - 'Geopolitical intelligence', - ], - }, - tech: { - title: 'Tech Monitor - Real-Time AI & Tech Industry Dashboard', - description: 'Real-time AI and tech industry dashboard tracking tech giants, AI labs, startup ecosystems, funding rounds, and tech events worldwide.', - keywords: 'tech dashboard, AI industry, startup ecosystem, tech companies, AI labs, venture capital, tech events, tech conferences, cloud infrastructure, datacenters, tech layoffs, funding rounds, unicorns, FAANG, tech HQ, accelerators, Y Combinator, tech news', - url: 'https://tech.worldmonitor.app/', - siteName: 'Tech Monitor', - shortName: 'TechMonitor', - subject: 'AI, Tech Industry, and Startup Ecosystem Intelligence', - classification: 'Tech Dashboard, AI Tracker, Startup Intelligence', - categories: ['news', 'business'], - features: [ - 'Tech news aggregation', - 'AI lab tracking', - 'Startup ecosystem mapping', - 'Tech HQ locations', - 'Conference & event calendar', - 'Cloud infrastructure monitoring', - 'Datacenter mapping', - 'Tech layoff tracking', - 'Funding round analytics', - 'Tech stock tracking', - 'Service status monitoring', - ], - }, - happy: { - title: 'Happy Monitor - Good News & Global Progress', - description: 'Curated positive news, progress data, and uplifting stories from around the world.', - keywords: 'good news, positive news, global progress, happy news, uplifting stories, human achievement, science breakthroughs, conservation wins', - url: 'https://happy.worldmonitor.app/', - siteName: 'Happy Monitor', - shortName: 'HappyMonitor', - subject: 'Good News, Global Progress, and Human Achievement', - classification: 'Positive News Dashboard, Progress Tracker', - categories: ['news', 'lifestyle'], - features: [ - 'Curated positive news', - 'Global progress tracking', - 'Live humanity counters', - 'Science breakthrough feed', - 'Conservation tracker', - 'Renewable energy dashboard', - ], - }, - finance: { - title: 'Finance Monitor - Real-Time Markets & Trading Dashboard', - description: 'Real-time finance and trading dashboard tracking global markets, stock exchanges, central banks, commodities, forex, crypto, and economic indicators worldwide.', - keywords: 'finance dashboard, trading dashboard, stock market, forex, commodities, central banks, crypto, economic indicators, market news, financial centers, stock exchanges, bonds, derivatives, fintech, hedge funds, IPO tracker, market analysis', - url: 'https://finance.worldmonitor.app/', - siteName: 'Finance Monitor', - shortName: 'FinanceMonitor', - subject: 'Global Markets, Trading, and Financial Intelligence', - classification: 'Finance Dashboard, Market Tracker, Trading Intelligence', - categories: ['finance', 'news'], - features: [ - 'Real-time market data', - 'Stock exchange mapping', - 'Central bank monitoring', - 'Commodity price tracking', - 'Forex & currency news', - 'Crypto & digital assets', - 'Economic indicator alerts', - 'IPO & earnings tracking', - 'Financial center mapping', - 'Sector heatmap', - 'Market radar signals', - ], - }, -}; - const activeVariant = process.env.VITE_VARIANT || 'full'; const activeMeta = VARIANT_META[activeVariant] || VARIANT_META.full; -function htmlVariantPlugin(): Plugin { - return { - name: 'html-variant', - transformIndexHtml(html) { - let result = html - .replace(/.*?<\/title>/, `<title>${activeMeta.title}`) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(//, ``) - .replace(/"name": "World Monitor"/, `"name": "${activeMeta.siteName}"`) - .replace(/"alternateName": "WorldMonitor"/, `"alternateName": "${activeMeta.siteName.replace(' ', '')}"`) - .replace(/"url": "https:\/\/worldmonitor\.app\/"/, `"url": "${activeMeta.url}"`) - .replace(/"description": "Real-time global intelligence dashboard with live news, markets, military tracking, infrastructure monitoring, and geopolitical data."/, `"description": "${activeMeta.description}"`) - .replace(/"featureList": \[[\s\S]*?\]/, `"featureList": ${JSON.stringify(activeMeta.features, null, 8).replace(/\n/g, '\n ')}`); - - // Theme-color meta — warm cream for happy variant - if (activeVariant === 'happy') { - result = result.replace( - //, - '' - ); - } - - // Inject build-time variant into the inline script so data-variant is set before CSS loads. - // Force the variant (don't let stale localStorage override the build-time setting). - if (activeVariant !== 'full') { - result = result.replace( - /if\(v\)document\.documentElement\.dataset\.variant=v;/, - `v='${activeVariant}';document.documentElement.dataset.variant=v;` - ); - } - - // Desktop CSP: inject localhost wildcard for dynamic sidecar port. - // Web builds intentionally exclude localhost to avoid exposing attack surface. - if (isDesktopBuild) { - result = result - .replace( - /connect-src 'self' https: http:\/\/localhost:5173/, - "connect-src 'self' https: http://localhost:5173 http://127.0.0.1:*" - ) - .replace( - /frame-src 'self'/, - "frame-src 'self' http://127.0.0.1:*" - ); - } - - // Favicon variant paths — replace /favico/ paths with variant-specific subdirectory - if (activeVariant !== 'full') { - result = result - .replace(/\/favico\/favicon/g, `/favico/${activeVariant}/favicon`) - .replace(/\/favico\/apple-touch-icon/g, `/favico/${activeVariant}/apple-touch-icon`) - .replace(/\/favico\/android-chrome/g, `/favico/${activeVariant}/android-chrome`) - .replace(/\/favico\/og-image/g, `/favico/${activeVariant}/og-image`); - } - - return result; - }, - }; -} - -function polymarketPlugin(): Plugin { - const GAMMA_BASE = 'https://gamma-api.polymarket.com'; - const ALLOWED_ORDER = ['volume', 'liquidity', 'startDate', 'endDate', 'spread']; - - return { - name: 'polymarket-dev', - configureServer(server) { - server.middlewares.use(async (req, res, next) => { - if (!req.url?.startsWith('/api/polymarket')) return next(); - - const url = new URL(req.url, 'http://localhost'); - const endpoint = url.searchParams.get('endpoint') || 'markets'; - const closed = ['true', 'false'].includes(url.searchParams.get('closed') ?? '') ? url.searchParams.get('closed') : 'false'; - const order = ALLOWED_ORDER.includes(url.searchParams.get('order') ?? '') ? url.searchParams.get('order') : 'volume'; - const ascending = ['true', 'false'].includes(url.searchParams.get('ascending') ?? '') ? url.searchParams.get('ascending') : 'false'; - const rawLimit = parseInt(url.searchParams.get('limit') ?? '', 10); - const limit = isNaN(rawLimit) ? 50 : Math.max(1, Math.min(100, rawLimit)); - - const params = new URLSearchParams({ closed: closed!, order: order!, ascending: ascending!, limit: String(limit) }); - if (endpoint === 'events') { - const tag = (url.searchParams.get('tag') ?? '').replace(/[^a-z0-9-]/gi, '').slice(0, 100); - if (tag) params.set('tag_slug', tag); - } - - const gammaUrl = `${GAMMA_BASE}/${endpoint === 'events' ? 'events' : 'markets'}?${params}`; - - res.setHeader('Content-Type', 'application/json'); - try { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), 8000); - const resp = await fetch(gammaUrl, { headers: { Accept: 'application/json' }, signal: controller.signal }); - clearTimeout(timer); - if (!resp.ok) throw new Error(`HTTP ${resp.status}`); - const data = await resp.text(); - res.setHeader('Cache-Control', 'public, max-age=120'); - res.setHeader('X-Polymarket-Source', 'gamma'); - res.end(data); - } catch { - // Expected: Cloudflare JA3 blocks server-side TLS — return empty array - res.setHeader('Cache-Control', 'public, max-age=300'); - res.end('[]'); - } - }); - }, - }; -} - -/** - * Vite dev server plugin for sebuf API routes. - * - * Intercepts requests matching /api/{domain}/v1/* and routes them through - * the same handler pipeline as the Vercel catch-all gateway. Other /api/* - * paths fall through to existing proxy rules. - */ -function sebufApiPlugin(): Plugin { - // Cache router across requests (H-13 fix). Invalidated by Vite's module graph on HMR. - let cachedRouter: Awaited> | null = null; - let cachedCorsMod: any = null; - - async function buildRouter() { - const [ - routerMod, corsMod, errorMod, - seismologyServerMod, seismologyHandlerMod, - wildfireServerMod, wildfireHandlerMod, - climateServerMod, climateHandlerMod, - predictionServerMod, predictionHandlerMod, - displacementServerMod, displacementHandlerMod, - aviationServerMod, aviationHandlerMod, - researchServerMod, researchHandlerMod, - unrestServerMod, unrestHandlerMod, - conflictServerMod, conflictHandlerMod, - maritimeServerMod, maritimeHandlerMod, - cyberServerMod, cyberHandlerMod, - economicServerMod, economicHandlerMod, - infrastructureServerMod, infrastructureHandlerMod, - marketServerMod, marketHandlerMod, - newsServerMod, newsHandlerMod, - intelligenceServerMod, intelligenceHandlerMod, - militaryServerMod, militaryHandlerMod, - positiveEventsServerMod, positiveEventsHandlerMod, - givingServerMod, givingHandlerMod, - tradeServerMod, tradeHandlerMod, - ] = await Promise.all([ - import('./server/router'), - import('./server/cors'), - import('./server/error-mapper'), - import('./src/generated/server/worldmonitor/seismology/v1/service_server'), - import('./server/worldmonitor/seismology/v1/handler'), - import('./src/generated/server/worldmonitor/wildfire/v1/service_server'), - import('./server/worldmonitor/wildfire/v1/handler'), - import('./src/generated/server/worldmonitor/climate/v1/service_server'), - import('./server/worldmonitor/climate/v1/handler'), - import('./src/generated/server/worldmonitor/prediction/v1/service_server'), - import('./server/worldmonitor/prediction/v1/handler'), - import('./src/generated/server/worldmonitor/displacement/v1/service_server'), - import('./server/worldmonitor/displacement/v1/handler'), - import('./src/generated/server/worldmonitor/aviation/v1/service_server'), - import('./server/worldmonitor/aviation/v1/handler'), - import('./src/generated/server/worldmonitor/research/v1/service_server'), - import('./server/worldmonitor/research/v1/handler'), - import('./src/generated/server/worldmonitor/unrest/v1/service_server'), - import('./server/worldmonitor/unrest/v1/handler'), - import('./src/generated/server/worldmonitor/conflict/v1/service_server'), - import('./server/worldmonitor/conflict/v1/handler'), - import('./src/generated/server/worldmonitor/maritime/v1/service_server'), - import('./server/worldmonitor/maritime/v1/handler'), - import('./src/generated/server/worldmonitor/cyber/v1/service_server'), - import('./server/worldmonitor/cyber/v1/handler'), - import('./src/generated/server/worldmonitor/economic/v1/service_server'), - import('./server/worldmonitor/economic/v1/handler'), - import('./src/generated/server/worldmonitor/infrastructure/v1/service_server'), - import('./server/worldmonitor/infrastructure/v1/handler'), - import('./src/generated/server/worldmonitor/market/v1/service_server'), - import('./server/worldmonitor/market/v1/handler'), - import('./src/generated/server/worldmonitor/news/v1/service_server'), - import('./server/worldmonitor/news/v1/handler'), - import('./src/generated/server/worldmonitor/intelligence/v1/service_server'), - import('./server/worldmonitor/intelligence/v1/handler'), - import('./src/generated/server/worldmonitor/military/v1/service_server'), - import('./server/worldmonitor/military/v1/handler'), - import('./src/generated/server/worldmonitor/positive_events/v1/service_server'), - import('./server/worldmonitor/positive-events/v1/handler'), - import('./src/generated/server/worldmonitor/giving/v1/service_server'), - import('./server/worldmonitor/giving/v1/handler'), - import('./src/generated/server/worldmonitor/trade/v1/service_server'), - import('./server/worldmonitor/trade/v1/handler'), - ]); - - const serverOptions = { onError: errorMod.mapErrorToResponse }; - const allRoutes = [ - ...seismologyServerMod.createSeismologyServiceRoutes(seismologyHandlerMod.seismologyHandler, serverOptions), - ...wildfireServerMod.createWildfireServiceRoutes(wildfireHandlerMod.wildfireHandler, serverOptions), - ...climateServerMod.createClimateServiceRoutes(climateHandlerMod.climateHandler, serverOptions), - ...predictionServerMod.createPredictionServiceRoutes(predictionHandlerMod.predictionHandler, serverOptions), - ...displacementServerMod.createDisplacementServiceRoutes(displacementHandlerMod.displacementHandler, serverOptions), - ...aviationServerMod.createAviationServiceRoutes(aviationHandlerMod.aviationHandler, serverOptions), - ...researchServerMod.createResearchServiceRoutes(researchHandlerMod.researchHandler, serverOptions), - ...unrestServerMod.createUnrestServiceRoutes(unrestHandlerMod.unrestHandler, serverOptions), - ...conflictServerMod.createConflictServiceRoutes(conflictHandlerMod.conflictHandler, serverOptions), - ...maritimeServerMod.createMaritimeServiceRoutes(maritimeHandlerMod.maritimeHandler, serverOptions), - ...cyberServerMod.createCyberServiceRoutes(cyberHandlerMod.cyberHandler, serverOptions), - ...economicServerMod.createEconomicServiceRoutes(economicHandlerMod.economicHandler, serverOptions), - ...infrastructureServerMod.createInfrastructureServiceRoutes(infrastructureHandlerMod.infrastructureHandler, serverOptions), - ...marketServerMod.createMarketServiceRoutes(marketHandlerMod.marketHandler, serverOptions), - ...newsServerMod.createNewsServiceRoutes(newsHandlerMod.newsHandler, serverOptions), - ...intelligenceServerMod.createIntelligenceServiceRoutes(intelligenceHandlerMod.intelligenceHandler, serverOptions), - ...militaryServerMod.createMilitaryServiceRoutes(militaryHandlerMod.militaryHandler, serverOptions), - ...positiveEventsServerMod.createPositiveEventsServiceRoutes(positiveEventsHandlerMod.positiveEventsHandler, serverOptions), - ...givingServerMod.createGivingServiceRoutes(givingHandlerMod.givingHandler, serverOptions), - ...tradeServerMod.createTradeServiceRoutes(tradeHandlerMod.tradeHandler, serverOptions), - ]; - cachedCorsMod = corsMod; - return routerMod.createRouter(allRoutes); - } - - return { - name: 'sebuf-api', - configureServer(server) { - // Invalidate cached router on HMR updates to server/ files - server.watcher.on('change', (file) => { - if (file.includes('/server/') || file.includes('/src/generated/server/')) { - cachedRouter = null; - } - }); - - server.middlewares.use(async (req, res, next) => { - // Only intercept sebuf routes: /api/{domain}/v1/* (domain may contain hyphens) - if (!req.url || !/^\/api\/[a-z-]+\/v1\//.test(req.url)) { - return next(); - } - - try { - // Build router once, reuse across requests (H-13 fix) - if (!cachedRouter) { - cachedRouter = await buildRouter(); - } - const router = cachedRouter; - const corsMod = cachedCorsMod; - - // Convert Connect IncomingMessage to Web Standard Request - const port = server.config.server.port || 3000; - const url = new URL(req.url, `http://localhost:${port}`); - - // Read body for POST requests - let body: string | undefined; - if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') { - const chunks: Buffer[] = []; - for await (const chunk of req) { - chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); - } - body = Buffer.concat(chunks).toString(); - } - - // Extract headers from IncomingMessage - const headers: Record = {}; - for (const [key, value] of Object.entries(req.headers)) { - if (typeof value === 'string') { - headers[key] = value; - } else if (Array.isArray(value)) { - headers[key] = value.join(', '); - } - } - - const webRequest = new Request(url.toString(), { - method: req.method, - headers, - body: body || undefined, - }); - - const corsHeaders = corsMod.getCorsHeaders(webRequest); - - // OPTIONS preflight - if (req.method === 'OPTIONS') { - res.statusCode = 204; - for (const [key, value] of Object.entries(corsHeaders)) { - res.setHeader(key, value); - } - res.end(); - return; - } - - // Origin check - if (corsMod.isDisallowedOrigin(webRequest)) { - res.statusCode = 403; - res.setHeader('Content-Type', 'application/json'); - for (const [key, value] of Object.entries(corsHeaders)) { - res.setHeader(key, value); - } - res.end(JSON.stringify({ error: 'Origin not allowed' })); - return; - } - - // Route matching - const matchedHandler = router.match(webRequest); - if (!matchedHandler) { - res.statusCode = 404; - res.setHeader('Content-Type', 'application/json'); - for (const [key, value] of Object.entries(corsHeaders)) { - res.setHeader(key, value); - } - res.end(JSON.stringify({ error: 'Not found' })); - return; - } - - // Execute handler - const response = await matchedHandler(webRequest); - - // Write response - res.statusCode = response.status; - response.headers.forEach((value, key) => { - res.setHeader(key, value); - }); - for (const [key, value] of Object.entries(corsHeaders)) { - res.setHeader(key, value); - } - res.end(await response.text()); - } catch (err) { - console.error('[sebuf-api] Error:', err); - res.statusCode = 500; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ error: 'Internal server error' })); - } - }); - }, - }; -} - -// RSS proxy allowlist — duplicated from api/rss-proxy.js for dev mode. -// Keep in sync when adding new domains. -const RSS_PROXY_ALLOWED_DOMAINS = new Set([ - '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', '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 - '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', - 'www.producthunt.com', 'www.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', - // Regional & international - 'english.alarabiya.net', 'www.arabnews.com', 'www.timesofisrael.com', 'www.haaretz.com', - 'www.scmp.com', 'kyivindependent.com', 'www.themoscowtimes.com', 'feeds.24.com', - 'feeds.capi24.com', 'www.france24.com', 'www.euronews.com', 'www.lemonde.fr', - 'rss.dw.com', 'www.africanews.com', 'www.lasillavacia.com', 'www.channelnewsasia.com', - 'www.thehindu.com', 'news.un.org', 'www.iaea.org', 'www.who.int', 'www.cisa.gov', - 'www.crisisgroup.org', - // Think tanks - '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', - // Regional locale feeds - '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', - 'news.ycombinator.com', - // Finance variant - 'www.coindesk.com', 'cointelegraph.com', - // Happy variant — positive news sources - 'www.goodnewsnetwork.org', 'www.positive.news', 'reasonstobecheerful.world', - 'www.optimistdaily.com', 'www.sunnyskyz.com', 'www.huffpost.com', - 'www.sciencedaily.com', 'feeds.nature.com', 'www.livescience.com', 'www.newscientist.com', -]); - -function rssProxyPlugin(): Plugin { - return { - name: 'rss-proxy', - configureServer(server) { - server.middlewares.use(async (req, res, next) => { - if (!req.url?.startsWith('/api/rss-proxy')) { - return next(); - } - - const url = new URL(req.url, 'http://localhost'); - const feedUrl = url.searchParams.get('url'); - if (!feedUrl) { - res.statusCode = 400; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ error: 'Missing url parameter' })); - return; - } - - try { - const parsed = new URL(feedUrl); - if (!RSS_PROXY_ALLOWED_DOMAINS.has(parsed.hostname)) { - res.statusCode = 403; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ error: `Domain not allowed: ${parsed.hostname}` })); - return; - } - - const controller = new AbortController(); - const timeout = feedUrl.includes('news.google.com') ? 20000 : 12000; - const timer = setTimeout(() => controller.abort(), timeout); - - const response = await fetch(feedUrl, { - signal: controller.signal, - 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, */*', - }, - redirect: 'follow', - }); - clearTimeout(timer); - - const data = await response.text(); - res.statusCode = response.status; - res.setHeader('Content-Type', 'application/xml'); - res.setHeader('Cache-Control', 'public, max-age=300'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.end(data); - } catch (error: any) { - console.error('[rss-proxy]', feedUrl, error.message); - res.statusCode = error.name === 'AbortError' ? 504 : 502; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ error: error.name === 'AbortError' ? 'Feed timeout' : 'Failed to fetch feed' })); - } - }); - }, - }; -} - -function youtubeLivePlugin(): Plugin { - return { - name: 'youtube-live', - configureServer(server) { - server.middlewares.use(async (req, res, next) => { - if (!req.url?.startsWith('/api/youtube/live')) { - return next(); - } - - const url = new URL(req.url, 'http://localhost'); - const channel = url.searchParams.get('channel'); - - if (!channel) { - res.statusCode = 400; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ error: 'Missing channel parameter' })); - return; - } - - try { - const channelHandle = channel.startsWith('@') ? channel : `@${channel}`; - const liveUrl = `https://www.youtube.com/${channelHandle}/live`; - - const ytRes = await fetch(liveUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - }, - redirect: 'follow', - }); - - if (!ytRes.ok) { - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Cache-Control', 'public, max-age=300'); - res.end(JSON.stringify({ videoId: null, channel })); - return; - } - - const html = await ytRes.text(); - - // Scope both fields to the same videoDetails block so we don't - // combine a videoId from one object with isLive from another. - let videoId: string | null = null; - const detailsIdx = html.indexOf('"videoDetails"'); - if (detailsIdx !== -1) { - const block = html.substring(detailsIdx, detailsIdx + 5000); - const vidMatch = block.match(/"videoId":"([a-zA-Z0-9_-]{11})"/); - const liveMatch = block.match(/"isLive"\s*:\s*true/); - if (vidMatch && liveMatch) { - videoId = vidMatch[1]; - } - } - - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Cache-Control', 'public, max-age=300'); - res.end(JSON.stringify({ videoId, isLive: videoId !== null, channel })); - } catch (error) { - console.error(`[YouTube Live] Error:`, error); - res.statusCode = 500; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ error: 'Failed to fetch', videoId: null })); - } - }); - }, - }; -} - export default defineConfig({ define: { __APP_VERSION__: JSON.stringify(pkg.version), }, plugins: [ - htmlVariantPlugin(), + htmlVariantPlugin(activeVariant, activeMeta, isDesktopBuild), polymarketPlugin(), rssProxyPlugin(), youtubeLivePlugin(), @@ -678,13 +34,11 @@ export default defineConfig({ VitePWA({ registerType: 'autoUpdate', injectRegister: false, - includeAssets: [ 'favico/favicon.ico', 'favico/apple-touch-icon.png', 'favico/favicon-32x32.png', ], - manifest: { name: `${activeMeta.siteName} - ${activeMeta.subject}`, short_name: activeMeta.shortName, @@ -702,7 +56,6 @@ export default defineConfig({ { src: '/favico/android-chrome-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, ], }, - workbox: { globPatterns: ['**/*.{js,css,ico,png,svg,woff2}'], globIgnores: ['**/ml*.js', '**/onnx*.wasm', '**/locale-*.js'], @@ -710,7 +63,6 @@ export default defineConfig({ skipWaiting: true, clientsClaim: true, cleanupOutdatedCaches: true, - runtimeCaching: [ { urlPattern: ({ request }: { request: Request }) => request.mode === 'navigate', @@ -804,7 +156,6 @@ export default defineConfig({ }, ], }, - devOptions: { enabled: false, }, @@ -823,12 +174,10 @@ export default defineConfig({ }, build: { // Geospatial bundles (maplibre/deck) are expected to be large even when split. - // Raise warning threshold to reduce noisy false alarms in CI. chunkSizeWarningLimit: 1200, rollupOptions: { onwarn(warning, warn) { // onnxruntime-web ships a minified browser bundle that intentionally uses eval. - // Keep build logs focused by filtering this known third-party warning only. if ( warning.code === 'EVAL' && typeof warning.id === 'string' @@ -836,7 +185,6 @@ export default defineConfig({ ) { return; } - warn(warning); }, input: { @@ -847,47 +195,26 @@ export default defineConfig({ output: { manualChunks(id) { if (id.includes('node_modules')) { - if (id.includes('/@xenova/transformers/')) { - return 'transformers'; - } - if (id.includes('/onnxruntime-web/')) { - return 'onnxruntime'; - } - if (id.includes('/maplibre-gl/')) { - return 'maplibre'; - } + if (id.includes('/@xenova/transformers/')) return 'transformers'; + if (id.includes('/onnxruntime-web/')) return 'onnxruntime'; + if (id.includes('/maplibre-gl/')) return 'maplibre'; if ( id.includes('/@deck.gl/') || id.includes('/@luma.gl/') || id.includes('/@loaders.gl/') || id.includes('/@math.gl/') || id.includes('/h3-js/') - ) { - return 'deck-stack'; - } - if (id.includes('/d3/')) { - return 'd3'; - } - if (id.includes('/topojson-client/')) { - return 'topojson'; - } - if (id.includes('/i18next')) { - return 'i18n'; - } - if (id.includes('/@sentry/')) { - return 'sentry'; - } - } - if (id.includes('/src/components/') && id.endsWith('Panel.ts')) { - return 'panels'; + ) return 'deck-stack'; + if (id.includes('/d3/')) return 'd3'; + if (id.includes('/topojson-client/')) return 'topojson'; + if (id.includes('/i18next')) return 'i18n'; + if (id.includes('/@sentry/')) return 'sentry'; } + if (id.includes('/src/components/') && id.endsWith('Panel.ts')) return 'panels'; // Give lazy-loaded locale chunks a recognizable prefix so the - // service worker can exclude them from precache (en.json is - // statically imported into the main bundle). + // service-worker can glob-ignore them (locale-*.js). const localeMatch = id.match(/\/locales\/(\w+)\.json$/); - if (localeMatch && localeMatch[1] !== 'en') { - return `locale-${localeMatch[1]}`; - } + if (localeMatch && localeMatch[1] !== 'en') return `locale-${localeMatch[1]}`; return undefined; }, }, @@ -904,377 +231,6 @@ export default defineConfig({ '**/.playwright-mcp/**', ], }, - proxy: { - // Yahoo Finance API - '/api/yahoo': { - target: 'https://query1.finance.yahoo.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/yahoo/, ''), - }, - // Polymarket handled by polymarketPlugin() — no prod proxy needed - // USGS Earthquake API - '/api/earthquake': { - target: 'https://earthquake.usgs.gov', - changeOrigin: true, - timeout: 30000, - rewrite: (path) => path.replace(/^\/api\/earthquake/, ''), - configure: (proxy) => { - proxy.on('error', (err) => { - console.log('Earthquake proxy error:', err.message); - }); - }, - }, - // PizzINT - Pentagon Pizza Index - '/api/pizzint': { - target: 'https://www.pizzint.watch', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/pizzint/, '/api'), - configure: (proxy) => { - proxy.on('error', (err) => { - console.log('PizzINT proxy error:', err.message); - }); - }, - }, - // FRED Economic Data - handled by Vercel serverless function in prod - // In dev, we proxy to the API directly with the key from .env - '/api/fred-data': { - target: 'https://api.stlouisfed.org', - changeOrigin: true, - rewrite: (path) => { - const url = new URL(path, 'http://localhost'); - const seriesId = url.searchParams.get('series_id'); - const start = url.searchParams.get('observation_start'); - const end = url.searchParams.get('observation_end'); - const apiKey = process.env.FRED_API_KEY || ''; - return `/fred/series/observations?series_id=${seriesId}&api_key=${apiKey}&file_type=json&sort_order=desc&limit=10${start ? `&observation_start=${start}` : ''}${end ? `&observation_end=${end}` : ''}`; - }, - }, - // RSS Feeds - BBC - '/rss/bbc': { - target: 'https://feeds.bbci.co.uk', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/bbc/, ''), - }, - // RSS Feeds - Guardian - '/rss/guardian': { - target: 'https://www.theguardian.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/guardian/, ''), - }, - // RSS Feeds - NPR - '/rss/npr': { - target: 'https://feeds.npr.org', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/npr/, ''), - }, - // RSS Feeds - AP News - '/rss/apnews': { - target: 'https://rsshub.app/apnews', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/apnews/, ''), - }, - // RSS Feeds - Al Jazeera - '/rss/aljazeera': { - target: 'https://www.aljazeera.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/aljazeera/, ''), - }, - // RSS Feeds - CNN - '/rss/cnn': { - target: 'http://rss.cnn.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/cnn/, ''), - }, - // RSS Feeds - Hacker News - '/rss/hn': { - target: 'https://hnrss.org', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/hn/, ''), - }, - // RSS Feeds - Ars Technica - '/rss/arstechnica': { - target: 'https://feeds.arstechnica.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/arstechnica/, ''), - }, - // RSS Feeds - The Verge - '/rss/verge': { - target: 'https://www.theverge.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/verge/, ''), - }, - // RSS Feeds - CNBC - '/rss/cnbc': { - target: 'https://www.cnbc.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/cnbc/, ''), - }, - // RSS Feeds - MarketWatch - '/rss/marketwatch': { - target: 'https://feeds.marketwatch.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/marketwatch/, ''), - }, - // RSS Feeds - Defense/Intel sources - '/rss/defenseone': { - target: 'https://www.defenseone.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/defenseone/, ''), - }, - '/rss/warontherocks': { - target: 'https://warontherocks.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/warontherocks/, ''), - }, - '/rss/breakingdefense': { - target: 'https://breakingdefense.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/breakingdefense/, ''), - }, - '/rss/bellingcat': { - target: 'https://www.bellingcat.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/bellingcat/, ''), - }, - // RSS Feeds - TechCrunch (layoffs) - '/rss/techcrunch': { - target: 'https://techcrunch.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/techcrunch/, ''), - }, - // Google News RSS - '/rss/googlenews': { - target: 'https://news.google.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/googlenews/, ''), - }, - // AI Company Blogs - '/rss/openai': { - target: 'https://openai.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/openai/, ''), - }, - '/rss/anthropic': { - target: 'https://www.anthropic.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/anthropic/, ''), - }, - '/rss/googleai': { - target: 'https://blog.google', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/googleai/, ''), - }, - '/rss/deepmind': { - target: 'https://deepmind.google', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/deepmind/, ''), - }, - '/rss/huggingface': { - target: 'https://huggingface.co', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/huggingface/, ''), - }, - '/rss/techreview': { - target: 'https://www.technologyreview.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/techreview/, ''), - }, - '/rss/arxiv': { - target: 'https://rss.arxiv.org', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/arxiv/, ''), - }, - // Government - '/rss/whitehouse': { - target: 'https://www.whitehouse.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/whitehouse/, ''), - }, - '/rss/statedept': { - target: 'https://www.state.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/statedept/, ''), - }, - '/rss/state': { - target: 'https://www.state.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/state/, ''), - }, - '/rss/defense': { - target: 'https://www.defense.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/defense/, ''), - }, - '/rss/justice': { - target: 'https://www.justice.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/justice/, ''), - }, - '/rss/cdc': { - target: 'https://tools.cdc.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/cdc/, ''), - }, - '/rss/fema': { - target: 'https://www.fema.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/fema/, ''), - }, - '/rss/dhs': { - target: 'https://www.dhs.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/dhs/, ''), - }, - '/rss/fedreserve': { - target: 'https://www.federalreserve.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/fedreserve/, ''), - }, - '/rss/sec': { - target: 'https://www.sec.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/sec/, ''), - }, - '/rss/treasury': { - target: 'https://home.treasury.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/treasury/, ''), - }, - '/rss/cisa': { - target: 'https://www.cisa.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/cisa/, ''), - }, - // Think Tanks - '/rss/brookings': { - target: 'https://www.brookings.edu', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/brookings/, ''), - }, - '/rss/cfr': { - target: 'https://www.cfr.org', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/cfr/, ''), - }, - '/rss/csis': { - target: 'https://www.csis.org', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/csis/, ''), - }, - // Defense - '/rss/warzone': { - target: 'https://www.thedrive.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/warzone/, ''), - }, - '/rss/defensegov': { - target: 'https://www.defense.gov', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/defensegov/, ''), - }, - // Security - '/rss/krebs': { - target: 'https://krebsonsecurity.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/krebs/, ''), - }, - // Finance - '/rss/yahoonews': { - target: 'https://finance.yahoo.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/yahoonews/, ''), - }, - // Diplomat - '/rss/diplomat': { - target: 'https://thediplomat.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/diplomat/, ''), - }, - // VentureBeat - '/rss/venturebeat': { - target: 'https://venturebeat.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/venturebeat/, ''), - }, - // Foreign Policy - '/rss/foreignpolicy': { - target: 'https://foreignpolicy.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/foreignpolicy/, ''), - }, - // Financial Times - '/rss/ft': { - target: 'https://www.ft.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/ft/, ''), - }, - // Reuters - '/rss/reuters': { - target: 'https://www.reutersagency.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/rss\/reuters/, ''), - }, - // Cloudflare Radar - Internet outages - '/api/cloudflare-radar': { - target: 'https://api.cloudflare.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/cloudflare-radar/, ''), - }, - // NGA Maritime Safety Information - Navigation Warnings - '/api/nga-msi': { - target: 'https://msi.nga.mil', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/nga-msi/, ''), - }, - // GDELT GEO 2.0 API - Global event data - '/api/gdelt': { - target: 'https://api.gdeltproject.org', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/gdelt/, ''), - }, - // AISStream WebSocket proxy for live vessel tracking - '/ws/aisstream': { - target: 'wss://stream.aisstream.io', - changeOrigin: true, - ws: true, - rewrite: (path) => path.replace(/^\/ws\/aisstream/, ''), - }, - // FAA NASSTATUS - Airport delays and closures - '/api/faa': { - target: 'https://nasstatus.faa.gov', - changeOrigin: true, - secure: true, - rewrite: (path) => path.replace(/^\/api\/faa/, ''), - configure: (proxy) => { - proxy.on('error', (err) => { - console.log('FAA NASSTATUS proxy error:', err.message); - }); - }, - }, - // OpenSky Network - Aircraft tracking (military flight detection) - '/api/opensky': { - target: 'https://opensky-network.org/api', - changeOrigin: true, - secure: true, - rewrite: (path) => path.replace(/^\/api\/opensky/, ''), - configure: (proxy) => { - proxy.on('error', (err) => { - console.log('OpenSky proxy error:', err.message); - }); - }, - }, - // ADS-B Exchange - Military aircraft tracking (backup/supplement) - '/api/adsb-exchange': { - target: 'https://adsbexchange.com/api', - changeOrigin: true, - secure: true, - rewrite: (path) => path.replace(/^\/api\/adsb-exchange/, ''), - configure: (proxy) => { - proxy.on('error', (err) => { - console.log('ADS-B Exchange proxy error:', err.message); - }); - }, - }, - }, + proxy: proxyConfig(), }, }); diff --git a/vite/plugins.ts b/vite/plugins.ts new file mode 100644 index 00000000..5fb35e26 --- /dev/null +++ b/vite/plugins.ts @@ -0,0 +1,518 @@ +import { type Plugin } from 'vite'; +import { VitePWA } from 'vite-plugin-pwa'; +import { resolve, dirname, extname } from 'path'; +import { mkdir, readFile, writeFile } from 'fs/promises'; +import { brotliCompress } from 'zlib'; +import { promisify } from 'util'; +import { type VariantMeta } from './variants'; +import { ALLOWED_DOMAINS } from '../api/rss-allowed-domains.js'; + +const brotliCompressAsync = promisify(brotliCompress); +const BROTLI_EXTENSIONS = new Set(['.js', '.mjs', '.css', '.html', '.svg', '.json', '.txt', '.xml', '.wasm']); + +export function brotliPrecompressPlugin(): Plugin { + return { + name: 'brotli-precompress', + apply: 'build', + async writeBundle(outputOptions, bundle) { + const outDir = outputOptions.dir; + if (!outDir) return; + + await Promise.all(Object.keys(bundle).map(async (fileName) => { + const extension = extname(fileName).toLowerCase(); + if (!BROTLI_EXTENSIONS.has(extension)) return; + + const sourcePath = resolve(outDir, fileName); + const compressedPath = `${sourcePath}.br`; + const sourceBuffer = await readFile(sourcePath); + if (sourceBuffer.length < 1024) return; + + const compressedBuffer = await brotliCompressAsync(sourceBuffer); + await mkdir(dirname(compressedPath), { recursive: true }); + await writeFile(compressedPath, compressedBuffer); + })); + }, + }; +} + +export function htmlVariantPlugin(activeVariant: string, activeMeta: VariantMeta, isDesktopBuild: boolean): Plugin { + return { + name: 'html-variant', + transformIndexHtml(html) { + let result = html + .replace(/.*?<\/title>/, `<title>${activeMeta.title}`) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(//, ``) + .replace(/"name": "World Monitor"/, `"name": "${activeMeta.siteName}"`) + .replace(/"alternateName": "WorldMonitor"/, `"alternateName": "${activeMeta.siteName.replace(' ', '')}"`) + .replace(/"url": "https:\/\/worldmonitor\.app\/"/, `"url": "${activeMeta.url}"`) + .replace(/"description": "Real-time global intelligence dashboard with live news, markets, military tracking, infrastructure monitoring, and geopolitical data."/, `"description": "${activeMeta.description}"`) + .replace(/"featureList": \[[\s\S]*?\]/, `"featureList": ${JSON.stringify(activeMeta.features, null, 8).replace(/\n/g, '\n ')}`); + + // Theme-color meta — warm cream for happy variant + if (activeVariant === 'happy') { + result = result.replace( + //, + '' + ); + } + + // Inject build-time variant into the inline script so data-variant is set before CSS loads. + // Force the variant (don't let stale localStorage override the build-time setting). + if (activeVariant !== 'full') { + result = result.replace( + /if\(v\)document\.documentElement\.dataset\.variant=v;/, + `v='${activeVariant}';document.documentElement.dataset.variant=v;` + ); + } + + // Desktop CSP: inject localhost wildcard for dynamic sidecar port. + // Web builds intentionally exclude localhost to avoid exposing attack surface. + if (isDesktopBuild) { + result = result + .replace( + /connect-src 'self' https: http:\/\/localhost:5173/, + "connect-src 'self' https: http://localhost:5173 http://127.0.0.1:*" + ) + .replace( + /frame-src 'self'/, + "frame-src 'self' http://127.0.0.1:*" + ); + } + + // Favicon variant paths — replace /favico/ paths with variant-specific subdirectory + if (activeVariant !== 'full') { + result = result + .replace(/\/favico\/favicon/g, `/favico/${activeVariant}/favicon`) + .replace(/\/favico\/apple-touch-icon/g, `/favico/${activeVariant}/apple-touch-icon`) + .replace(/\/favico\/android-chrome/g, `/favico/${activeVariant}/android-chrome`) + .replace(/\/favico\/og-image/g, `/favico/${activeVariant}/og-image`); + } + + return result; + }, + }; +} + +export function polymarketPlugin(): Plugin { + const GAMMA_BASE = 'https://gamma-api.polymarket.com'; + const ALLOWED_ORDER = ['volume', 'liquidity', 'startDate', 'endDate', 'spread']; + + return { + name: 'polymarket-dev', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + if (!req.url?.startsWith('/api/polymarket')) return next(); + + const url = new URL(req.url, 'http://localhost'); + const endpoint = url.searchParams.get('endpoint') || 'markets'; + const closed = ['true', 'false'].includes(url.searchParams.get('closed') ?? '') ? url.searchParams.get('closed') : 'false'; + const order = ALLOWED_ORDER.includes(url.searchParams.get('order') ?? '') ? url.searchParams.get('order') : 'volume'; + const ascending = ['true', 'false'].includes(url.searchParams.get('ascending') ?? '') ? url.searchParams.get('ascending') : 'false'; + const rawLimit = parseInt(url.searchParams.get('limit') ?? '', 10); + const limit = isNaN(rawLimit) ? 50 : Math.max(1, Math.min(100, rawLimit)); + + const params = new URLSearchParams({ closed: closed!, order: order!, ascending: ascending!, limit: String(limit) }); + if (endpoint === 'events') { + const tag = (url.searchParams.get('tag') ?? '').replace(/[^a-z0-9-]/gi, '').slice(0, 100); + if (tag) params.set('tag_slug', tag); + } + + const gammaUrl = `${GAMMA_BASE}/${endpoint === 'events' ? 'events' : 'markets'}?${params}`; + + res.setHeader('Content-Type', 'application/json'); + try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 8000); + const resp = await fetch(gammaUrl, { headers: { Accept: 'application/json' }, signal: controller.signal }); + clearTimeout(timer); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const data = await resp.text(); + res.setHeader('Cache-Control', 'public, max-age=120'); + res.setHeader('X-Polymarket-Source', 'gamma'); + res.end(data); + } catch { + res.setHeader('Cache-Control', 'public, max-age=300'); + res.end('[]'); + } + }); + }, + }; +} + +export function sebufApiPlugin(): Plugin { + let cachedRouter: any = null; + let cachedCorsMod: any = null; + + async function buildRouter() { + const [ + routerMod, corsMod, errorMod, + seismologyServerMod, seismologyHandlerMod, + wildfireServerMod, wildfireHandlerMod, + climateServerMod, climateHandlerMod, + predictionServerMod, predictionHandlerMod, + displacementServerMod, displacementHandlerMod, + aviationServerMod, aviationHandlerMod, + researchServerMod, researchHandlerMod, + unrestServerMod, unrestHandlerMod, + conflictServerMod, conflictHandlerMod, + maritimeServerMod, maritimeHandlerMod, + cyberServerMod, cyberHandlerMod, + economicServerMod, economicHandlerMod, + infrastructureServerMod, infrastructureHandlerMod, + marketServerMod, marketHandlerMod, + newsServerMod, newsHandlerMod, + intelligenceServerMod, intelligenceHandlerMod, + militaryServerMod, militaryHandlerMod, + positiveEventsServerMod, positiveEventsHandlerMod, + givingServerMod, givingHandlerMod, + tradeServerMod, tradeHandlerMod, + ] = await Promise.all([ + import('./server/router'), + import('./server/cors'), + import('./server/error-mapper'), + import('./src/generated/server/worldmonitor/seismology/v1/service_server'), + import('./server/worldmonitor/seismology/v1/handler'), + import('./src/generated/server/worldmonitor/wildfire/v1/service_server'), + import('./server/worldmonitor/wildfire/v1/handler'), + import('./src/generated/server/worldmonitor/climate/v1/service_server'), + import('./server/worldmonitor/climate/v1/handler'), + import('./src/generated/server/worldmonitor/prediction/v1/service_server'), + import('./server/worldmonitor/prediction/v1/handler'), + import('./src/generated/server/worldmonitor/displacement/v1/service_server'), + import('./server/worldmonitor/displacement/v1/handler'), + import('./src/generated/server/worldmonitor/aviation/v1/service_server'), + import('./server/worldmonitor/aviation/v1/handler'), + import('./src/generated/server/worldmonitor/research/v1/service_server'), + import('./server/worldmonitor/research/v1/handler'), + import('./src/generated/server/worldmonitor/unrest/v1/service_server'), + import('./server/worldmonitor/unrest/v1/handler'), + import('./src/generated/server/worldmonitor/conflict/v1/service_server'), + import('./server/worldmonitor/conflict/v1/handler'), + import('./src/generated/server/worldmonitor/maritime/v1/service_server'), + import('./server/worldmonitor/maritime/v1/handler'), + import('./src/generated/server/worldmonitor/cyber/v1/service_server'), + import('./server/worldmonitor/cyber/v1/handler'), + import('./src/generated/server/worldmonitor/economic/v1/service_server'), + import('./server/worldmonitor/economic/v1/handler'), + import('./src/generated/server/worldmonitor/infrastructure/v1/service_server'), + import('./server/worldmonitor/infrastructure/v1/handler'), + import('./src/generated/server/worldmonitor/market/v1/service_server'), + import('./server/worldmonitor/market/v1/handler'), + import('./src/generated/server/worldmonitor/news/v1/service_server'), + import('./server/worldmonitor/news/v1/handler'), + import('./src/generated/server/worldmonitor/intelligence/v1/service_server'), + import('./server/worldmonitor/intelligence/v1/handler'), + import('./src/generated/server/worldmonitor/military/v1/service_server'), + import('./server/worldmonitor/military/v1/handler'), + import('./src/generated/server/worldmonitor/positive_events/v1/service_server'), + import('./server/worldmonitor/positive-events/v1/handler'), + import('./src/generated/server/worldmonitor/giving/v1/service_server'), + import('./server/worldmonitor/giving/v1/handler'), + import('./src/generated/server/worldmonitor/trade/v1/service_server'), + import('./server/worldmonitor/trade/v1/handler'), + ]); + + const serverOptions = { onError: errorMod.mapErrorToResponse }; + const allRoutes = [ + ...seismologyServerMod.createSeismologyServiceRoutes(seismologyHandlerMod.seismologyHandler, serverOptions), + ...wildfireServerMod.createWildfireServiceRoutes(wildfireHandlerMod.wildfireHandler, serverOptions), + ...climateServerMod.createClimateServiceRoutes(climateHandlerMod.climateHandler, serverOptions), + ...predictionServerMod.createPredictionServiceRoutes(predictionHandlerMod.predictionHandler, serverOptions), + ...displacementServerMod.createDisplacementServiceRoutes(displacementHandlerMod.displacementHandler, serverOptions), + ...aviationServerMod.createAviationServiceRoutes(aviationHandlerMod.aviationHandler, serverOptions), + ...researchServerMod.createResearchServiceRoutes(researchHandlerMod.researchHandler, serverOptions), + ...unrestServerMod.createUnrestServiceRoutes(unrestHandlerMod.unrestHandler, serverOptions), + ...conflictServerMod.createConflictServiceRoutes(conflictHandlerMod.conflictHandler, serverOptions), + ...maritimeServerMod.createMaritimeServiceRoutes(maritimeHandlerMod.maritimeHandler, serverOptions), + ...cyberServerMod.createCyberServiceRoutes(cyberHandlerMod.cyberHandler, serverOptions), + ...economicServerMod.createEconomicServiceRoutes(economicHandlerMod.economicHandler, serverOptions), + ...infrastructureServerMod.createInfrastructureServiceRoutes(infrastructureHandlerMod.infrastructureHandler, serverOptions), + ...marketServerMod.createMarketServiceRoutes(marketHandlerMod.marketHandler, serverOptions), + ...newsServerMod.createNewsServiceRoutes(newsHandlerMod.newsHandler, serverOptions), + ...intelligenceServerMod.createIntelligenceServiceRoutes(intelligenceHandlerMod.intelligenceHandler, serverOptions), + ...militaryServerMod.createMilitaryServiceRoutes(militaryHandlerMod.militaryHandler, serverOptions), + ...positiveEventsServerMod.createPositiveEventsServiceRoutes(positiveEventsHandlerMod.positiveEventsHandler, serverOptions), + ...givingServerMod.createGivingServiceRoutes(givingHandlerMod.givingHandler, serverOptions), + ...tradeServerMod.createTradeServiceRoutes(tradeHandlerMod.tradeHandler, serverOptions), + ]; + cachedCorsMod = corsMod; + return routerMod.createRouter(allRoutes); + } + + return { + name: 'sebuf-api', + configureServer(server) { + server.watcher.on('change', (file) => { + if (file.includes('/server/') || file.includes('/src/generated/server/')) { + cachedRouter = null; + } + }); + + server.middlewares.use(async (req, res, next) => { + if (!req.url || !/^\/api\/[a-z-]+\/v1\//.test(req.url)) { + return next(); + } + + try { + if (!cachedRouter) { + cachedRouter = await buildRouter(); + } + const router = cachedRouter; + const corsMod = cachedCorsMod; + + const port = server.config.server.port || 3000; + const url = new URL(req.url, `http://localhost:${port}`); + + let body: string | undefined; + if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') { + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); + } + body = Buffer.concat(chunks).toString(); + } + + const headers: Record = {}; + for (const [key, value] of Object.entries(req.headers)) { + if (typeof value === 'string') { + headers[key] = value; + } else if (Array.isArray(value)) { + headers[key] = value.join(', '); + } + } + + const webRequest = new Request(url.toString(), { + method: req.method, + headers, + body: body || undefined, + }); + + const corsHeaders = corsMod.getCorsHeaders(webRequest); + + if (req.method === 'OPTIONS') { + res.statusCode = 204; + for (const [key, value] of Object.entries(corsHeaders)) { + res.setHeader(key, value); + } + res.end(); + return; + } + + if (corsMod.isDisallowedOrigin(webRequest)) { + res.statusCode = 403; + res.setHeader('Content-Type', 'application/json'); + for (const [key, value] of Object.entries(corsHeaders)) { + res.setHeader(key, value); + } + res.end(JSON.stringify({ error: 'Origin not allowed' })); + return; + } + + const matchedHandler = router.match(webRequest); + if (!matchedHandler) { + res.statusCode = 404; + res.setHeader('Content-Type', 'application/json'); + for (const [key, value] of Object.entries(corsHeaders)) { + res.setHeader(key, value); + } + res.end(JSON.stringify({ error: 'Not found' })); + return; + } + + const response = await matchedHandler(webRequest); + + res.statusCode = response.status; + response.headers.forEach((value, key) => { + res.setHeader(key, value); + }); + for (const [key, value] of Object.entries(corsHeaders)) { + res.setHeader(key, value); + } + res.end(await response.text()); + } catch (err) { + console.error('[sebuf-api] Error:', err); + res.statusCode = 500; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: 'Internal server error' })); + } + }); + }, + }; +} + +// RSS proxy domain allowlist — imported from shared single source of truth +// (api/rss-allowed-domains.js) to prevent dev/prod drift. +const RSS_PROXY_ALLOWED_DOMAINS = new Set(ALLOWED_DOMAINS); + +export function rssProxyPlugin(): Plugin { + return { + name: 'rss-proxy', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + if (!req.url?.startsWith('/api/rss-proxy')) { + return next(); + } + + const url = new URL(req.url, 'http://localhost'); + const feedUrl = url.searchParams.get('url'); + if (!feedUrl) { + res.statusCode = 400; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: 'Missing url parameter' })); + return; + } + + try { + const parsed = new URL(feedUrl); + // Normalize hostname the same way production does (api/rss-proxy.js): + // check the raw hostname, the bare (no-www) form, and the www-prefixed form. + const hostname = parsed.hostname; + const bare = hostname.replace(/^www\./, ''); + const withWww = hostname.startsWith('www.') ? hostname : `www.${hostname}`; + if (!RSS_PROXY_ALLOWED_DOMAINS.has(hostname) + && !RSS_PROXY_ALLOWED_DOMAINS.has(bare) + && !RSS_PROXY_ALLOWED_DOMAINS.has(withWww)) { + res.statusCode = 403; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: `Domain not allowed: ${parsed.hostname}` })); + return; + } + + const controller = new AbortController(); + const timeout = feedUrl.includes('news.google.com') ? 20000 : 12000; + const timer = setTimeout(() => controller.abort(), timeout); + + // Use redirect:'manual' and validate redirect targets against the + // allowlist, matching the security behaviour of the production proxy. + let response = await fetch(feedUrl, { + signal: controller.signal, + 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, */*', + }, + redirect: 'manual', + }); + + // Follow up to 5 redirects, validating each target + let redirects = 0; + while (response.status >= 300 && response.status < 400 && redirects < 5) { + const location = response.headers.get('location'); + if (!location) break; + const redirectUrl = new URL(location, feedUrl); + const rHost = redirectUrl.hostname; + const rBare = rHost.replace(/^www\./, ''); + const rWww = rHost.startsWith('www.') ? rHost : `www.${rHost}`; + if (!RSS_PROXY_ALLOWED_DOMAINS.has(rHost) + && !RSS_PROXY_ALLOWED_DOMAINS.has(rBare) + && !RSS_PROXY_ALLOWED_DOMAINS.has(rWww)) { + res.statusCode = 403; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: `Redirect to disallowed domain: ${rHost}` })); + clearTimeout(timer); + return; + } + response = await fetch(redirectUrl.href, { + signal: controller.signal, + 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, */*', + }, + redirect: 'manual', + }); + redirects++; + } + clearTimeout(timer); + + const data = await response.text(); + res.statusCode = response.status; + res.setHeader('Content-Type', 'application/xml'); + res.setHeader('Cache-Control', 'public, max-age=300'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end(data); + } catch (error: any) { + console.error('[rss-proxy]', feedUrl, error.message); + res.statusCode = error.name === 'AbortError' ? 504 : 502; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: error.name === 'AbortError' ? 'Feed timeout' : 'Failed to fetch feed' })); + } + }); + }, + }; +} + +export function youtubeLivePlugin(): Plugin { + return { + name: 'youtube-live', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + if (!req.url?.startsWith('/api/youtube/live')) { + return next(); + } + + const url = new URL(req.url, 'http://localhost'); + const channel = url.searchParams.get('channel'); + + if (!channel) { + res.statusCode = 400; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: 'Missing channel parameter' })); + return; + } + + try { + const channelHandle = channel.startsWith('@') ? channel : `@${channel}`; + const liveUrl = `https://www.youtube.com/${channelHandle}/live`; + + const ytRes = await fetch(liveUrl, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }, + redirect: 'follow', + }); + + if (!ytRes.ok) { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Cache-Control', 'public, max-age=300'); + res.end(JSON.stringify({ videoId: null, channel })); + return; + } + + const html = await ytRes.text(); + + let videoId: string | null = null; + const detailsIdx = html.indexOf('"videoDetails"'); + if (detailsIdx !== -1) { + const block = html.substring(detailsIdx, detailsIdx + 5000); + const vidMatch = block.match(/"videoId":"([a-zA-Z0-9_-]{11})"/); + const liveMatch = block.match(/"isLive"\s*:\s*true/); + if (vidMatch && liveMatch) { + videoId = vidMatch[1]; + } + } + + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Cache-Control', 'public, max-age=300'); + res.end(JSON.stringify({ videoId, isLive: videoId !== null, channel })); + } catch (error) { + console.error(`[YouTube Live] Error:`, error); + res.statusCode = 500; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: 'Failed to fetch', videoId: null })); + } + }); + }, + }; +} diff --git a/vite/proxy.ts b/vite/proxy.ts new file mode 100644 index 00000000..d1e9d77c --- /dev/null +++ b/vite/proxy.ts @@ -0,0 +1,374 @@ +export const proxyConfig = () => ({ + // Yahoo Finance API + '/api/yahoo': { + target: 'https://query1.finance.yahoo.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/api\/yahoo/, ''), + }, + // Polymarket handled by polymarketPlugin() — no prod proxy needed + // USGS Earthquake API + '/api/earthquake': { + target: 'https://earthquake.usgs.gov', + changeOrigin: true, + timeout: 30000, + rewrite: (path: string) => path.replace(/^\/api\/earthquake/, ''), + configure: (proxy: any) => { + proxy.on('error', (err: any) => { + console.log('Earthquake proxy error:', err.message); + }); + }, + }, + // PizzINT - Pentagon Pizza Index + '/api/pizzint': { + target: 'https://www.pizzint.watch', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/api\/pizzint/, '/api'), + configure: (proxy: any) => { + proxy.on('error', (err: any) => { + console.log('PizzINT proxy error:', err.message); + }); + }, + }, + // FRED Economic Data - handled by Vercel serverless function in prod + // In dev, we proxy to the API directly with the key from .env + // NOTE: Reads FRED_API_KEY from process.env at proxy-creation time. + // The key must be set in .env before starting the dev server. + '/api/fred-data': { + target: 'https://api.stlouisfed.org', + changeOrigin: true, + rewrite: (path: string) => { + const url = new URL(path, 'http://localhost'); + const seriesId = url.searchParams.get('series_id'); + const start = url.searchParams.get('observation_start'); + const end = url.searchParams.get('observation_end'); + const apiKey = process.env.FRED_API_KEY || ''; + return `/fred/series/observations?series_id=${seriesId}&api_key=${apiKey}&file_type=json&sort_order=desc&limit=10${start ? `&observation_start=${start}` : ''}${end ? `&observation_end=${end}` : ''}`; + }, + }, + // RSS Feeds - BBC + '/rss/bbc': { + target: 'https://feeds.bbci.co.uk', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/bbc/, ''), + }, + // RSS Feeds - Guardian + '/rss/guardian': { + target: 'https://www.theguardian.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/guardian/, ''), + }, + // RSS Feeds - NPR + '/rss/npr': { + target: 'https://feeds.npr.org', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/npr/, ''), + }, + // RSS Feeds - AP News + '/rss/apnews': { + target: 'https://rsshub.app/apnews', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/apnews/, ''), + }, + // RSS Feeds - Al Jazeera + '/rss/aljazeera': { + target: 'https://www.aljazeera.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/aljazeera/, ''), + }, + // RSS Feeds - CNN + '/rss/cnn': { + target: 'http://rss.cnn.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/cnn/, ''), + }, + // RSS Feeds - Hacker News + '/rss/hn': { + target: 'https://hnrss.org', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/hn/, ''), + }, + // RSS Feeds - Ars Technica + '/rss/arstechnica': { + target: 'https://feeds.arstechnica.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/arstechnica/, ''), + }, + // RSS Feeds - The Verge + '/rss/verge': { + target: 'https://www.theverge.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/verge/, ''), + }, + // RSS Feeds - CNBC + '/rss/cnbc': { + target: 'https://www.cnbc.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/cnbc/, ''), + }, + // RSS Feeds - MarketWatch + '/rss/marketwatch': { + target: 'https://feeds.marketwatch.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/marketwatch/, ''), + }, + // RSS Feeds - Defense/Intel sources + '/rss/defenseone': { + target: 'https://www.defenseone.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/defenseone/, ''), + }, + '/rss/warontherocks': { + target: 'https://warontherocks.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/warontherocks/, ''), + }, + '/rss/breakingdefense': { + target: 'https://breakingdefense.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/breakingdefense/, ''), + }, + '/rss/bellingcat': { + target: 'https://www.bellingcat.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/bellingcat/, ''), + }, + // RSS Feeds - TechCrunch (layoffs) + '/rss/techcrunch': { + target: 'https://techcrunch.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/techcrunch/, ''), + }, + // Google News RSS + '/rss/googlenews': { + target: 'https://news.google.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/googlenews/, ''), + }, + // AI Company Blogs + '/rss/openai': { + target: 'https://openai.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/openai/, ''), + }, + '/rss/anthropic': { + target: 'https://www.anthropic.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/anthropic/, ''), + }, + '/rss/googleai': { + target: 'https://blog.google', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/googleai/, ''), + }, + '/rss/deepmind': { + target: 'https://deepmind.google', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/deepmind/, ''), + }, + '/rss/huggingface': { + target: 'https://huggingface.co', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/huggingface/, ''), + }, + '/rss/techreview': { + target: 'https://www.technologyreview.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/techreview/, ''), + }, + '/rss/arxiv': { + target: 'https://rss.arxiv.org', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/arxiv/, ''), + }, + // Government + '/rss/whitehouse': { + target: 'https://www.whitehouse.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/whitehouse/, ''), + }, + '/rss/statedept': { + target: 'https://www.state.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/statedept/, ''), + }, + '/rss/state': { + target: 'https://www.state.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/state/, ''), + }, + '/rss/defense': { + target: 'https://www.defense.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/defense/, ''), + }, + '/rss/justice': { + target: 'https://www.justice.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/justice/, ''), + }, + '/rss/cdc': { + target: 'https://tools.cdc.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/cdc/, ''), + }, + '/rss/fema': { + target: 'https://www.fema.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/fema/, ''), + }, + '/rss/dhs': { + target: 'https://www.dhs.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/dhs/, ''), + }, + '/rss/fedreserve': { + target: 'https://www.federalreserve.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/fedreserve/, ''), + }, + '/rss/sec': { + target: 'https://www.sec.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/sec/, ''), + }, + '/rss/treasury': { + target: 'https://home.treasury.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/treasury/, ''), + }, + '/rss/cisa': { + target: 'https://www.cisa.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/cisa/, ''), + }, + // Think Tanks + '/rss/brookings': { + target: 'https://www.brookings.edu', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/brookings/, ''), + }, + '/rss/cfr': { + target: 'https://www.cfr.org', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/cfr/, ''), + }, + '/rss/csis': { + target: 'https://www.csis.org', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/csis/, ''), + }, + // Defense + '/rss/warzone': { + target: 'https://www.thedrive.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/warzone/, ''), + }, + '/rss/defensegov': { + target: 'https://www.defense.gov', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/defensegov/, ''), + }, + // Security + '/rss/krebs': { + target: 'https://krebsonsecurity.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/krebs/, ''), + }, + // Finance + '/rss/yahoonews': { + target: 'https://finance.yahoo.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/yahoonews/, ''), + }, + // Diplomat + '/rss/diplomat': { + target: 'https://thediplomat.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/diplomat/, ''), + }, + // VentureBeat + '/rss/venturebeat': { + target: 'https://venturebeat.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/venturebeat/, ''), + }, + // Foreign Policy + '/rss/foreignpolicy': { + target: 'https://foreignpolicy.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/foreignpolicy/, ''), + }, + // Financial Times + '/rss/ft': { + target: 'https://www.ft.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/ft/, ''), + }, + // Reuters + '/rss/reuters': { + target: 'https://www.reutersagency.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/rss\/reuters/, ''), + }, + // Cloudflare Radar - Internet outages + '/api/cloudflare-radar': { + target: 'https://api.cloudflare.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/api\/cloudflare-radar/, ''), + }, + // NGA Maritime Safety Information - Navigation Warnings + '/api/nga-msi': { + target: 'https://msi.nga.mil', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/api\/nga-msi/, ''), + }, + // GDELT GEO 2.0 API - Global event data + '/api/gdelt': { + target: 'https://api.gdeltproject.org', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/api\/gdelt/, ''), + }, + // AISStream WebSocket proxy for live vessel tracking + '/ws/aisstream': { + target: 'wss://stream.aisstream.io', + changeOrigin: true, + ws: true, + rewrite: (path: string) => path.replace(/^\/ws\/aisstream/, ''), + }, + // FAA NASSTATUS - Airport delays and closures + '/api/faa': { + target: 'https://nasstatus.faa.gov', + changeOrigin: true, + secure: true, + rewrite: (path: string) => path.replace(/^\/api\/faa/, ''), + configure: (proxy: any) => { + proxy.on('error', (err: any) => { + console.log('FAA NASSTATUS proxy error:', err.message); + }); + }, + }, + // OpenSky Network - Aircraft tracking (military flight detection) + '/api/opensky': { + target: 'https://opensky-network.org/api', + changeOrigin: true, + secure: true, + rewrite: (path: string) => path.replace(/^\/api\/opensky/, ''), + configure: (proxy: any) => { + proxy.on('error', (err: any) => { + console.log('OpenSky proxy error:', err.message); + }); + }, + }, + // ADS-B Exchange - Military aircraft tracking (backup/supplement) + '/api/adsb-exchange': { + target: 'https://adsbexchange.com/api', + changeOrigin: true, + secure: true, + rewrite: (path: string) => path.replace(/^\/api\/adsb-exchange/, ''), + configure: (proxy: any) => { + proxy.on('error', (err: any) => { + console.log('ADS-B Exchange proxy error:', err.message); + }); + }, + }, +}); diff --git a/vite/variants.ts b/vite/variants.ts new file mode 100644 index 00000000..cc2f24d3 --- /dev/null +++ b/vite/variants.ts @@ -0,0 +1,107 @@ +export interface VariantMeta { + title: string; + description: string; + keywords: string; + url: string; + siteName: string; + shortName: string; + subject: string; + classification: string; + categories: string[]; + features: string[]; +} + +export const VARIANT_META: Record = { + full: { + title: 'World Monitor - Real-Time Global Intelligence Dashboard', + description: 'Real-time global intelligence dashboard with live news, markets, military tracking, infrastructure monitoring, and geopolitical data. OSINT in one view.', + keywords: 'global intelligence, geopolitical dashboard, world news, market data, military bases, nuclear facilities, undersea cables, conflict zones, real-time monitoring, situation awareness, OSINT, flight tracking, AIS ships, earthquake monitor, protest tracker, power outages, oil prices, government spending, polymarket predictions', + url: 'https://worldmonitor.app/', + siteName: 'World Monitor', + shortName: 'WorldMonitor', + subject: 'Real-Time Global Intelligence and Situation Awareness', + classification: 'Intelligence Dashboard, OSINT Tool, News Aggregator', + categories: ['news', 'productivity'], + features: [ + 'Real-time news aggregation', + 'Stock market tracking', + 'Military flight monitoring', + 'Ship AIS tracking', + 'Earthquake alerts', + 'Protest tracking', + 'Power outage monitoring', + 'Oil price analytics', + 'Government spending data', + 'Prediction markets', + 'Infrastructure monitoring', + 'Geopolitical intelligence', + ], + }, + tech: { + title: 'Tech Monitor - Real-Time AI & Tech Industry Dashboard', + description: 'Real-time AI and tech industry dashboard tracking tech giants, AI labs, startup ecosystems, funding rounds, and tech events worldwide.', + keywords: 'tech dashboard, AI industry, startup ecosystem, tech companies, AI labs, venture capital, tech events, tech conferences, cloud infrastructure, datacenters, tech layoffs, funding rounds, unicorns, FAANG, tech HQ, accelerators, Y Combinator, tech news', + url: 'https://tech.worldmonitor.app/', + siteName: 'Tech Monitor', + shortName: 'TechMonitor', + subject: 'AI, Tech Industry, and Startup Ecosystem Intelligence', + classification: 'Tech Dashboard, AI Tracker, Startup Intelligence', + categories: ['news', 'business'], + features: [ + 'Tech news aggregation', + 'AI lab tracking', + 'Startup ecosystem mapping', + 'Tech HQ locations', + 'Conference & event calendar', + 'Cloud infrastructure monitoring', + 'Datacenter mapping', + 'Tech layoff tracking', + 'Funding round analytics', + 'Tech stock tracking', + 'Service status monitoring', + ], + }, + happy: { + title: 'Happy Monitor - Good News & Global Progress', + description: 'Curated positive news, progress data, and uplifting stories from around the world.', + keywords: 'good news, positive news, global progress, happy news, uplifting stories, human achievement, science breakthroughs, conservation wins', + url: 'https://happy.worldmonitor.app/', + siteName: 'Happy Monitor', + shortName: 'HappyMonitor', + subject: 'Good News, Global Progress, and Human Achievement', + classification: 'Positive News Dashboard, Progress Tracker', + categories: ['news', 'lifestyle'], + features: [ + 'Curated positive news', + 'Global progress tracking', + 'Live humanity counters', + 'Science breakthrough feed', + 'Conservation tracker', + 'Renewable energy dashboard', + ], + }, + finance: { + title: 'Finance Monitor - Real-Time Markets & Trading Dashboard', + description: 'Real-time finance and trading dashboard tracking global markets, stock exchanges, central banks, commodities, forex, crypto, and economic indicators worldwide.', + keywords: 'finance dashboard, trading dashboard, stock market, forex, commodities, central banks, crypto, economic indicators, market news, financial centers, stock exchanges, bonds, derivatives, fintech, hedge funds, IPO tracker, market analysis', + url: 'https://finance.worldmonitor.app/', + siteName: 'Finance Monitor', + shortName: 'FinanceMonitor', + subject: 'Global Markets, Trading, and Financial Intelligence', + classification: 'Finance Dashboard, Market Tracker, Trading Intelligence', + categories: ['finance', 'news'], + features: [ + 'Real-time market data', + 'Stock exchange mapping', + 'Central bank monitoring', + 'Commodity price tracking', + 'Forex & currency news', + 'Crypto & digital assets', + 'Economic indicator alerts', + 'IPO & earnings tracking', + 'Financial center mapping', + 'Sector heatmap', + 'Market radar signals', + ], + }, +};