From 24c3d3f695c6f03957bb5bed8d163acc380fd58b Mon Sep 17 00:00:00 2001 From: facusturla Date: Wed, 25 Feb 2026 12:42:49 -0300 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20fix=20=5F=5Fdirname,=20remove=20process=20leak,=20n?= =?UTF-8?q?ormalize=20indentation,=20type=20activeMeta,=20use=20cross-env-?= =?UTF-8?q?shell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 220 ++++----- package.json | 41 +- vite.config.ts | 1104 ++------------------------------------------- vite/plugins.ts | 522 +++++++++++++++++++++ vite/proxy.ts | 372 +++++++++++++++ vite/variants.ts | 107 +++++ 6 files changed, 1132 insertions(+), 1234 deletions(-) create mode 100644 vite/plugins.ts create mode 100644 vite/proxy.ts create mode 100644 vite/variants.ts diff --git a/package-lock.json b/package-lock.json index 9a6f1f69e..b4c8f5477 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 bf04cc27f..49f68a08c 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", @@ -93,4 +94,4 @@ "overrides": { "fast-xml-parser": "^5.3.7" } -} +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 25f9a6b06..563ef8811 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,690 +1,46 @@ -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 { fileURLToPath } from 'url'; 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 })); - } - }); - }, - }; -} +const __dirname = fileURLToPath(new URL('.', import.meta.url)); export default defineConfig({ define: { __APP_VERSION__: JSON.stringify(pkg.version), }, plugins: [ - htmlVariantPlugin(), + htmlVariantPlugin(activeVariant, activeMeta, isDesktopBuild), polymarketPlugin(), rssProxyPlugin(), youtubeLivePlugin(), - sebufApiPlugin(), + sebufApiPlugin(__dirname), brotliPrecompressPlugin(), 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 +58,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 +65,6 @@ export default defineConfig({ skipWaiting: true, clientsClaim: true, cleanupOutdatedCaches: true, - runtimeCaching: [ { urlPattern: ({ request }: { request: Request }) => request.mode === 'navigate', @@ -804,7 +158,6 @@ export default defineConfig({ }, ], }, - devOptions: { enabled: false, }, @@ -822,13 +175,9 @@ 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,24 @@ 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'; } - // 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). + if (id.includes('/src/components/') && id.endsWith('Panel.ts')) return 'panels'; 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 +229,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 000000000..22548e46c --- /dev/null +++ b/vite/plugins.ts @@ -0,0 +1,522 @@ +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'; + +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(serverRoot: string): 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(resolve(serverRoot, 'server/router')), + import(resolve(serverRoot, 'server/cors')), + import(resolve(serverRoot, 'server/error-mapper')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/seismology/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/seismology/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/wildfire/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/wildfire/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/climate/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/climate/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/prediction/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/prediction/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/displacement/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/displacement/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/aviation/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/aviation/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/research/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/research/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/unrest/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/unrest/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/conflict/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/conflict/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/maritime/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/maritime/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/cyber/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/cyber/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/economic/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/economic/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/infrastructure/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/infrastructure/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/market/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/market/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/news/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/news/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/intelligence/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/intelligence/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/military/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/military/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/positive_events/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/positive-events/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/giving/v1/service_server')), + import(resolve(serverRoot, 'server/worldmonitor/giving/v1/handler')), + import(resolve(serverRoot, 'src/generated/server/worldmonitor/trade/v1/service_server')), + import(resolve(serverRoot, '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' })); + } + }); + }, + }; +} + +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', + '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', + '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', + 'rusi.org', 'warontherocks.com', 'www.aei.org', 'responsiblestatecraft.org', + 'www.fpri.org', 'jamestown.org', 'www.chathamhouse.org', 'ecfr.eu', 'www.gmfus.org', + 'www.wilsoncenter.org', 'www.lowyinstitute.org', 'www.mei.edu', 'www.stimson.org', + 'www.cnas.org', 'carnegieendowment.org', 'www.rand.org', 'fas.org', + 'www.armscontrol.org', 'www.nti.org', 'thebulletin.org', 'www.iss.europa.eu', + 'www.fao.org', 'worldbank.org', 'www.imf.org', + 'www.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', + 'www.coindesk.com', 'cointelegraph.com', + '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', +]); + +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); + 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' })); + } + }); + }, + }; +} + +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 000000000..89c60ee80 --- /dev/null +++ b/vite/proxy.ts @@ -0,0 +1,372 @@ +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 + '/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 000000000..cc2f24d3d --- /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', + ], + }, +}; From 3cc1513a906be43614bde01fa6687ad0e21daf74 Mon Sep 17 00:00:00 2001 From: facusturla Date: Thu, 26 Feb 2026 16:04:32 -0300 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?shared=20RSS=20domains,=20revert=20sebufApiPlugin=20paths,=20re?= =?UTF-8?q?move=20=5F=5Fdirname=20polyfill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract RSS allowed domains into shared api/rss-allowed-domains.js (single source of truth for both prod and dev proxy) - Revert sebufApiPlugin to relative import() paths for Vite module graph compat - Remove unnecessary __dirname polyfill (Vite provides it natively) - Restore explanatory comments in manualChunks/onwarn - Document FRED_API_KEY env dependency in proxy.ts - Restore trailing newline in package.json --- api/rss-allowed-domains.js | 282 +++++++++++++++++++++++++++++++++++++ api/rss-proxy.js | Bin 11634 -> 11028 bytes package.json | 2 +- vite.config.ts | 8 +- vite/plugins.ts | 138 +++++++----------- vite/proxy.ts | 2 + 6 files changed, 338 insertions(+), 94 deletions(-) create mode 100644 api/rss-allowed-domains.js diff --git a/api/rss-allowed-domains.js b/api/rss-allowed-domains.js new file mode 100644 index 000000000..24717000f --- /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 f677acc0ae08116c65164ffd8200254b3743adbd..09a6536ff28f846638a2a8416213f1828e29fee0 100644 GIT binary patch literal 11028 zcmeI2Yj0b}5r+43f&PbMASkIcr0m#k(YS7)_>!9v1(F=KXj+@GC`*lQ73tWrqrcwv zeRjCo**)jbRvMuGP!JT)Wq0oL?#!z7NmCUie5)&vZA} z-I?CabpPk@Ran>f&%$=R`asWS8Zimy@%<$JwI|16SI-Y4-KoZXD_Upass0}7ZckK? zbvF+Oam25~uXJ@3PIY&tr%iaR@3YANUVJta*QqG(MmnAOFG$UN7`^H*AQ; zfo30wM-wje-P4m3(Qm>ajHSV2@rKh(cmL4eLAVpTWmuEO>!P?59*EateK(~0TKHTx z+v=eX-I=UeN;Q&X$S{d=)tuoz4KJMqkPy3L4ZGfpS~cOWo?xG|Xhm#zq`7!(5cS7y z1KnLLw1T$z`Jvv-MQvYiUufPuUL^`AqBoCrz?0|ELv3I2BvIK}(AZwcU=)TiW)7o| zPNY%tvU#oK_LVr9MmNIka6Nuj^yGDvqtR8{4)f?uJWfp6>|>C?AUUdH6(svEU=|ISl5@Y(cxueaOs&F<9e3k~ZRgE!xj> zA+}1JmNUzDwT$C9_cY3lj6-pK5heX7BETR<<3L$I*XLAk((@JR0JZkqG=ezJGt<+q zd8^?&aeGf+n=`nKnyYxK#QULg$~;#-H?O>ok?AwYdvafT<&`7NNSV<`H=$=&E}F-- zMEB{!Tg#)v$HkUbvb7i~tS4KFn0xVVTlQTRE7{kcZ;6%tvU;nTtV8*_JC?n)Yl`D- zN&Hyf2a@}~{%$%+UuvDpl8wR-vdOvPgt(Y1Q`YrmRX&rJ=D!kyk+}5FUmZO!_1`oS z)XUtw9Wv6iDUKyKE8D(gp)zxTV48cA!}74q1nMIu1(SJL(6REE(|Ma zwz2G3eqi0sRD7Oh**xMt5t4XslsqnkGFESWyr??v#_`}Xw5b)C$M) zR&C1_p@{cx>C?Qz_EkN#{P^beWgXLwOIF?5;y&VMlCYc)je(;1XUs2^eqatY6)kMUe ztUx4EIW5n|eZ8lbv08H|S*PKjlE-`2$1vB#W9dhqUBqE~M51gf=BGm0?WpI5?6a-t z-H820UP;jl>swv_nUk_nq;Ev)HWA^`mbiEpSN$|EWv}Tojva&{KH%{%WQybov4{QJKHC*UJ4tH6;0&e z#~pK<=$9u8xyG>;sp*$h1`;MTzNQs)*?!YlBdKc+m4~cjR0uV7B>Rm%jk?RLQ%gSh z@2HekUa8j0w$GkWi948 z`0R$B=ao5;gCz=f#rZ&fG}%}?v&`d&AxAMM`>~~z(GaFzA~KtL#6FhOH!hzTmp(mn zpTOQUG0r}>arCvO-$!h(m)>&F@U>K;)wTPV&}v(}!R54TS=zVVnJkNAuW1^`??9Cl z@z7oQQ}4f$6y)4Q=zLqad0n%H5mBkB*%=_lzSe5qsP?}eN8HghS{#SpsUGC+WxN8B z*0ko+PgJyR7+7~jpW-cVI9|Z)bZSn zcKJiBqq^duuALH3Vs}^8OL<_`8rV!fK>g8W$*vx+t1^O1T0843u^N|N7}RRCUk}dQ zlpaK(t≠^GA)eN|ktNXDR%Brh=8e|F%qRb17E~@Qjs^3ZtfwyMkoV^qcgqXY@;2 zON(6IH-6V8t;VS@s=0t5C8i|*B>DVrlJ)0QB;8WB>&m_=!^=U3OU6^@CJi6xiFG{K zf3va)XE!Grjh$vOUa^FoGx9Xo8U3C@0_z^UEa@8kaut+Uk3J;tw+CH{XUnSis zYTb02VS(ov*}B(+tH|;5&LX9jQ9_vw*BDfV~O)fJJ)FE5o7ZjoGOFOiUQeX+`9XxdcUA8fge%sHKO<1uFG%Q z4Q=x+Df@cUY?=2}*;%xGRXm)|qx*JO?aZjFf=3_ZoCu%bKDyk&rPiYAr|!6#q?uNd zI??j7o~qveUK}_H!%y@YoKjnlb5|WA9RP3mA1!;^^^T|l`$www_C?LEk@&~>=3o&G zQ}q?rRrJ|m*{*Xt3hGs~+{tMpKK-}VukD=lnL_v^`>q&brr2LKh7z1lzJs9zWpON;=QhLxhN`43jl)SR;yeXlZj@XeYpO3b@}@I z-#@;;dd>d!H#UKe8IvWOeiL>a1`hq2H$1>Ry-bU?;<=bkHpZMDA5V_hWNB^d{(YFP zCX4jXxOV)P<8P0#ZGtQ9yH0PU(>boy{~NdXa-Uvr=?$6SB>of?mKfCrrmsbgJM758 z3*q|J`PJo%kJpzMSC=Uy?2N zztX0A{MRe6KgEfBpG*EC`4yMdhHs~(5Cu#pb&6ff>P-%(KQO|vLG_4(Q8isv?I_4w z>90Ne{f7HvZOjjyuFk&wBAQ$k;{DZ67YgyL5seKj9{OwcS9}m!oIKo3zN8X79hmmg zHdDXM*yKFVMQbMrYrJhMnL8O1EB|y?Lm(;80j#*Q$L?RhJO16|$ejW*?~Ty2^9491 zQXiDAiZ`l}R_WQ*_4Vv$t?stRkFt|~$NeGgyiW!0^n`$V?qf8=HUu3}utL>bA`z5I zBeLb4mUesK!{hnu*Y7^So?pCsbNAUmOLCY#T5ndK_$R_@TrX0yrcQY<>I z3&|TS)qcUFH?7W;UU(2ft;rTjEh-W5hybr{`DYQf&95B5fy=$NSAJ+Dz(^Zv6f;JsnE^2zLV>Cf1hV}p~OZh zkg|D&d?EB&Kn?471Povd*essEF@w&y&aRi?TnSU-hgYB zs-Vzf=lD|8JR&)%FUfKemZ8Z)OI&0yxRn-jL)q0Eb6lXL*x=MHUn&*BL_xL{{2)=x z8ot(nkVt{>5rKT%DtPiX5^z#Y_NrDioSdWg09x0gv#69fd^p|eZLD96JD|{IH_s3r zyl%LWJZt1?C3eW>W#U$JPHtr41q2~(A?)PB1f6T8$WoEUi_RZ{+@{LXWaU;yi|DoL zJa%-UT7oRsTPi1@xo}s@%?jmX+v!%pXa=|=(3wU?6D2m3-rkv3D$%S|8?k8#S{?K(xcsKAGw&of!$%bG@QH-{IE)Y+~l*l1#0mB{9 zFAxM^$OQy8UNCS;A%z}8HE)cnH^`z{1=IA>Bq!ak3l6g^=-@FGt7@}EG<9@F>=;qw zI=tGFvWFG2rA#)Z`V?WXZIZm@`6`orEGfiVwaGsGMj6rDfSMy>X4?3l`I0vafshj^ z8X?GTc-1bGnCk7r1v09gB07oLaBgfOB5(Bc_Z&S)B0S1ZMFDG7xEv#id`)vGlZr04 zOLPt6v-q7<$=w!c4B$v}T*2SS0~(~+xDR5MbI7xnS&tt?McfNq7A;AF?=D+Q8(oJ) zh=kHuFE`xdmkUORnqX>MsA-P)gOnW)=8v9zj=%b-Uc$Q3PIOHXmz)&Er%p+p^Bq5F z%DXWl=Ni#3Ne^>nmqr%Cs1o*I`FuC+R-$RPhOSi!i!c@GffEG!d?^sYgQ4MeS7PCH z1k2L4YpHn}t}41&4^9-}%vmpbR>M!a9j!w=6z9S@la-#00fdzEIjWD@I2)BF*9l+GtchDy3v6UzS=7^gE zl8Yia^opKrY2$UtkWu-eu_T2Lk|F*{G$X_}vM4ISF5x3MX^`yOUZOC+N}44~Bd+;e zZjlE<4McJyptHw8YEGIIlvN4YaFGa$mXpvmjRs#aUMmQ5j$#tPW<__na1 zTA5sJb^^b5>c>wawucaN0m>KzLl=Aegi+O*R-m!6goVp}J2cX?7CEB0K=lAuf(c?5 zNdDBoXgvUNgAQQVP$;_8;C>}SaxMbZ02LiP1=x}2Lcl6_fyi$Ufk?a$ffN%SY;X4g zb+-qTR8c{^5PMeYm^O;UnF(hus{-|>M<9T={_$17;2tn3KrRH*VGu&AkxjbVsdG2I ztr7mIpl=pQOrQfPF*KqwcAdx$?cUaEly(tDxOOQ`!MBtNhXt_lqednPOf(!3fgYFS z3PHHPI&GHC_@^jg+FBMU3lppgaiW6D^E{0o38N&MaV)vzEh+*9#s;QB4}(BdbP5~NW$n0AD=va zHamGVd;I%DQWF%L7H^bf5DV}YwAv90N0B-N-u_uiBIh9TApmM$vKLdg%pUIYEx6-9G!*;R76Ob?xX{_t`Sw;Lt@0D1=O_>0Ey4> z;vo?KB9O2oOp`)S%Y!_i-%AiwwalU1mr4~3?@zwJ@9W=^tAq-@DxeHnB_JYPmjmE| z$&!V#DOJ~K5gBT5M2m$m&R&QsJQ57{?z{`NzCb}a3SO4BppfR>04BPWKC;gOAvHug zpHM{A^0e#~Cnqye)yUO&j1YOzWTLNLi32<@cJn#Cyks(Q5ftbVfcF+E1++#XFVy@N zImQgweHKGe*;Fy$HaZ{uw#fAh&3QS3f)|rSX|pbNe%sM+ZuoMyzeCnfV#WkdCTgxS z|M!scJ&&cTP?C4bC44k^ky}w&Nzrwz3sH3qVltXizoWj3J8nhS2udHHx)`*b1}ns9 z*mfmukzogdu#BN{sZ`EU&^V^p)aoPF;;Bn_c#c{gv9-i+J6OoFgV?vey2fLH=fJKT zr0O=2HyS6ytqX`LShoXK4R`$vo%W4Pqt<-O7kEvwgGV_2Vd0CFTA@2l($sng@8jYM z=1UzS@9S1+VB&=@d`7-K*OY)-6k`>-C6Y>zKLqne+@bx355DI1ydF7W?Uv|T1trfx zoD(F18ZFk|*4;l5`%_Y#p(7WKOqAT0r3n7$X9K@rDE3$s*PT%JwI{%Q>0eAj7e?s& z`#*BETv5?zk|aow-ZR?R+=6ZhM2r_}X~?1s`|tn!FNU5Ug&f6BhQ?!!(NmAG_2xtj z_yq*y2QN?tZ*_hZg|~Y@2#O>2499z}BT7p)Yd$+;12^xl*=3Ek)oh0sPzFV6>$0$2 zW_bSc>oKcxu@8#6Z4Yh-uY5?MzS{?dv+u!eSNov!`udP=TkV6+i+xZ8*F6*{kUpea z*L$Fl9^SkwjOU^zNl6fRgV4YGo`>+9@1!Z`ln+W6gdR02Z0I07hi!u?I&*;&Ht)xa7zCGM;y0X%-y79T;}Py*GWfpodDD3aUBvyF!;;m7%7qiWn7XDpyNmcbt{1 zt80O4mx&7#)FBchgZsJD2@?K;+r$GLs!Y8LYbZIeQhN&wfTySg-n41-p+qjX`n z^yD!lhhd`0|JqbE#J=NkdKgdLAjPEf54@0nU-6tm}VW8Cy4^JVC$pwmD zD0+6YZN=`O>=qu0i7+OzBk6cDiI8^Z>)^x$Nd#$yTRQDJ{e~=b$}W~7Uy)w@Gz_0J zAOX0K8Q8Q@8mU-*77SMG>_Fa8`4UvKL?wZ@Lz?9e$7TWeJVBh};RQ%xX_##v{|5;F zB?}|$NSQu+_<%E$J&+~R5`FG~fjOLNa>#xdD6&)bD*?a#63+1B%zhxhGtNWLm{%PV z_B3E0vhTiQ-+UE~`2Ra7J;#T^l!j^X$Pd11OfTFlk~Iph?ZrQ~BsRFtlaud*Ee(V0 z|C=>oUg{i)hTKkR2mn925v6}nf=L)y1e-DBMux9r@Q4z=E=iOujoiw5WOMK&kjUzfgAXMNF^!teZ(OYnEc|1zK@DF47n#4s!|5-}#KabZp z;ya(5q#w@oyhg`OCF`z8ritYKel(4KqdwEy%kk6fgiSvnDJk6i?G1Z$lASzf06cs0 zoZUTpa>&jx0v8{|{7=#zKmGonv+tj=>7V?^N0nR&_9L879l{y$-H5{nFvQI0OCjP=gVlelG3@9gs*mphfr31p zN`L6&08dE*D8j` z{SbzIU`{VYj_PU9-(UY^!#^71TG~zh z9m}Rw`V+L#htEMJ!9=|&(szA?jVG+Z-2Dnc5L$~X`ZA0{C$Eg~OEJ9@)qS7N`2ZNc zfVyXJ5PHb;x_6cpZGJz6|K)q7!RAx;zWFB52|ng(sQ^60V+UDw!a*+pfBzA3rdBTX z4)i0lA$UV3S9@>%kMQlIPD;WBn*;z3;+Z2MyKkPt(c!9I*S)4aiv_nFTJxVX1}S9p zJyX!izPgfjMVAbf;;`KVLa^QAtGm_XuyYg-!J#WLEtQyu?kl>TFRzl=KDprN|7I8d zEf#6uD>NMzQN|afJyAS0;-&Ejv#q-s9yQONq2gs`hM)15#mmABpVi=R417^oLImOr z2_Ru%T ({ }, // 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, From b5c26f5547c0c3f9710032939526d7e465c5ccd0 Mon Sep 17 00:00:00 2001 From: facusturla Date: Fri, 27 Feb 2026 17:11:50 -0300 Subject: [PATCH 3/3] fix: UTF-8 encoding for rss-proxy.js, dev proxy www. normalization and redirect security - Re-save api/rss-proxy.js as UTF-8 without BOM (was UTF-16 LE, broke Vercel edge runtime) - Add www. hostname normalization to dev RSS proxy (parity with production) - Switch dev proxy from redirect:'follow' to redirect:'manual' with explicit redirect domain validation against the allowlist (parity with production) --- api/rss-proxy.js | Bin 11028 -> 5359 bytes vite/plugins.ts | 44 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/api/rss-proxy.js b/api/rss-proxy.js index 09a6536ff28f846638a2a8416213f1828e29fee0..40e088941fcbea725d1f179231f9c72674dcad94 100644 GIT binary patch literal 5359 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≠^GA)eN|ktNXDR%Brh=8e|F%qRb17E~@Qjs^3ZtfwyMkoV^qcgqXY@;2 zON(6IH-6V8t;VS@s=0t5C8i|*B>DVrlJ)0QB;8WB>&m_=!^=U3OU6^@CJi6xiFG{K zf3va)XE!Grjh$vOUa^FoGx9Xo8U3C@0_z^UEa@8kaut+Uk3J;tw+CH{XUnSis zYTb02VS(ov*}B(+tH|;5&LX9jQ9_vw*BDfV~O)fJJ)FE5o7ZjoGOFOiUQeX+`9XxdcUA8fge%sHKO<1uFG%Q z4Q=x+Df@cUY?=2}*;%xGRXm)|qx*JO?aZjFf=3_ZoCu%bKDyk&rPiYAr|!6#q?uNd zI??j7o~qveUK}_H!%y@YoKjnlb5|WA9RP3mA1!;^^^T|l`$www_C?LEk@&~>=3o&G zQ}q?rRrJ|m*{*Xt3hGs~+{tMpKK-}VukD=lnL_v^`>q&br controller.abort(), timeout); - const response = await fetch(feedUrl, { + // 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: 'follow', + 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();