diff --git a/.gitignore b/.gitignore index 6037464b..f23ffa9f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ experiment dist node_modules devAssets +config.local.json .DS_Store .idea diff --git a/apps/widget-telegram/.gitignore b/apps/widget-telegram/.gitignore new file mode 100644 index 00000000..205a6bfb --- /dev/null +++ b/apps/widget-telegram/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.vite/ +*.local diff --git a/apps/widget-telegram/README.md b/apps/widget-telegram/README.md new file mode 100644 index 00000000..d40c28e0 --- /dev/null +++ b/apps/widget-telegram/README.md @@ -0,0 +1,176 @@ +# @vojo/widget-telegram + +Vojo Telegram bridge management widget — mounts inside `/bots/telegram` +in the Vojo client. See [`docs/plans/bots_tab.md`](../../docs/plans/bots_tab.md) +Phase 3 for product context and the matrix-widget-api contract. + +This is **not** a Telegram client. It's a small panel that drives the +mautrix-telegram bridge bot (`@telegrambot:vojo.chat`) by sending text +commands in the control DM and rendering the bot's text replies. M11 +ships only the bootstrap + a `ping` button to verify the host handshake. + +## Layout + +``` +src/ +├── bootstrap.ts Parse URL params the host appends (matches BotWidgetEmbed.ts) +├── widget-api.ts Inline matrix-widget-api postMessage transport (no SDK) +├── App.tsx UI: bootstrap card, action buttons, transcript pane +├── main.tsx Entry: init bootstrap, render App or diagnostic +└── styles.css Theme-aware CSS variables +``` + +## Local development + +**Don't touch the committed `config.json`.** Create `config.local.json` at +the project root once — gitignored, never deployed. The host's Vite dev +server overlays it on top of `/config.json` responses (see +`serveLocalConfigOverlay` in `vite.config.js`); prod builds ignore the +overlay entirely. + +```bash +# one-time: install widget deps +cd apps/widget-telegram && npm install + +# one-time: create config.local.json (gitignored) at the project root +cat > /home/ubuntu/projects/vojo/cinny/config.local.json <<'JSON' +{ + "bots": [ + { + "id": "telegram", + "experience": { + "type": "matrix-widget", + "url": "http://localhost:8081/" + } + } + ] +} +JSON +``` + +The overlay merges `bots[]` by `id`, so just `{ id, experience }` is +enough — base bot's `mxid` and `name` are preserved. Top-level fields +not present in `config.local.json` are inherited from `config.json`. + +Run both servers: + +```bash +# terminal 1 — widget on :8081 with HMR +cd apps/widget-telegram && npm run dev + +# terminal 2 — host SPA on :8080 +cd /home/ubuntu/projects/vojo/cinny && npm start +``` + +Open `http://localhost:8080/bots/telegram`. Iframe loads cross-origin +from the widget dev server, HMR works, no proxy. + +`http://localhost:*` URLs are accepted by the host's URL validator only +in dev builds (`import.meta.env.DEV` branch in +`src/app/features/bots/catalog.ts`); production builds drop the branch +via Vite's dead-code elimination, AND production-only enforces an origin +allowlist (`PROD_WIDGET_ORIGINS`) so prod can never embed `localhost` even +if config.json is poisoned. + +Deploy is unchanged. `config.local.json` is gitignored, never shipped. +You don't need to revert anything before `Deploy to vojo.chat` — there +is nothing in tracked files that points at localhost. + +Standalone preview of the widget bundle (no host, useful for visual +iteration): + +```bash +cd apps/widget-telegram +npm run dev # vite dev server on :8081 — shows missing-params banner + # without host, expected. +npm run preview # serves the production build from dist/ +``` + +## Build + +```bash +npm run build +``` + +Outputs to `apps/widget-telegram/dist/`. Deploy by rsyncing `dist/*` +into `~/vojo/widgets/telegram/` on the production host (Caddy serves +this via the `widgets.vojo.chat` block). One parent `~/vojo/widgets/` +directory hosts every bot widget — adding a second one is `mkdir +~/vojo/widgets//` plus a Caddy block, no docker-compose edit. + +## Hosting (server-side, runbook) + +1. DNS: `widgets.vojo.chat` A/AAAA → server. Verify with `dig`. +2. `~/vojo/docker-compose.yml` — Caddy `volumes:` adds (one parent mount, + future widgets reuse it): + ```yaml + - ./widgets:/var/www/widgets + ``` +3. `~/vojo/caddy/Caddyfile` — append: + ``` + widgets.vojo.chat { + encode zstd gzip + + header { + Content-Security-Policy "frame-ancestors https://vojo.chat https://localhost" + X-Content-Type-Options "nosniff" + Referrer-Policy "no-referrer" + Cache-Control "no-cache, no-store, must-revalidate" + -Server + } + + handle_path /telegram/* { + root * /var/www/widgets/telegram + try_files {path} /index.html + file_server + } + + handle { + respond "Not Found" 404 + } + } + ``` +4. `mkdir -p ~/vojo/widgets/telegram` (placeholder so cert provisioning + has something to serve), then `docker compose up -d caddy` to apply. +5. Verify directly: `curl -I https://widgets.vojo.chat/telegram/index.html` + should return 200 and the `Content-Security-Policy` header. + +## Updating the production /config.json + +Once the widget is live at `https://widgets.vojo.chat/telegram/index.html`, +add to the host repo's `config.json`: + +```json +"experience": { + "type": "matrix-widget", + "url": "https://widgets.vojo.chat/telegram/index.html" +} +``` + +## Capacitor (Android) + +`capacitor.config.ts` already has a placeholder. Uncomment and set: + +```ts +server: { allowNavigation: ['widgets.vojo.chat'] } +``` + +Without this, Android's WebView hijacks the cross-origin iframe URL into +`Intent.ACTION_VIEW` and the iframe stays blank. Rebuild the APK after. + +## Capability contract + +The widget requests EXACTLY this set (matches the host's +`BotWidgetDriver.getBotWidgetCapabilities`): + +``` +org.matrix.msc2762.timeline: +org.matrix.msc2762.send.event:m.room.message#m.text +org.matrix.msc2762.receive.event:m.room.message#m.text +org.matrix.msc2762.receive.event:m.room.message#m.notice +org.matrix.msc2762.receive.state_event:m.room.member +``` + +Anything else is silently dropped by the host. To extend the surface, +update `BotWidgetDriver.ts` upstream — that requires a security review +per Phase 2 plan §M9. diff --git a/apps/widget-telegram/index.html b/apps/widget-telegram/index.html new file mode 100644 index 00000000..72efc8eb --- /dev/null +++ b/apps/widget-telegram/index.html @@ -0,0 +1,12 @@ + + + + + + Telegram bridge — Vojo + + +
+ + + diff --git a/apps/widget-telegram/package-lock.json b/apps/widget-telegram/package-lock.json new file mode 100644 index 00000000..1b1d7515 --- /dev/null +++ b/apps/widget-telegram/package-lock.json @@ -0,0 +1,1992 @@ +{ + "name": "@vojo/widget-telegram", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vojo/widget-telegram", + "version": "0.0.1", + "dependencies": { + "preact": "10.22.1" + }, + "devDependencies": { + "@preact/preset-vite": "2.9.0", + "typescript": "5.4.5", + "vite": "5.4.19" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@preact/preset-vite": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.9.0.tgz", + "integrity": "sha512-B9yVT7AkR6owrt84K3pLNyaKSvlioKdw65VqE/zMiR6HMovPekpsrwBNs5DJhBFEd5cvLMtCjHNHZ9P7Oblveg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@prefresh/vite": "^2.4.1", + "@rollup/pluginutils": "^4.1.1", + "babel-plugin-transform-hook-names": "^1.0.2", + "debug": "^4.3.4", + "kolorist": "^1.8.0", + "magic-string": "0.30.5", + "node-html-parser": "^6.1.10", + "resolve": "^1.22.8", + "source-map": "^0.7.4", + "stack-trace": "^1.0.0-pre2" + }, + "peerDependencies": { + "@babel/core": "7.x", + "vite": "2.x || 3.x || 4.x || 5.x" + } + }, + "node_modules/@prefresh/babel-plugin": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.3.tgz", + "integrity": "sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/core": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/@prefresh/core/-/core-1.5.9.tgz", + "integrity": "sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "preact": "^10.0.0 || ^11.0.0-0" + } + }, + "node_modules/@prefresh/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/vite": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@prefresh/vite/-/vite-2.4.12.tgz", + "integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "^0.5.2", + "@prefresh/core": "^1.5.0", + "@prefresh/utils": "^1.2.0", + "@rollup/pluginutils": "^4.2.1" + }, + "peerDependencies": { + "preact": "^10.4.0 || ^11.0.0-0", + "vite": ">=2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-plugin-transform-hook-names": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", + "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.12.10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.25.tgz", + "integrity": "sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.348", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.348.tgz", + "integrity": "sha512-QC2X59nRlycQQMc4ZXjSVBX+tSgJfgRtcrYHbIZLgOV2dCvefoQGegLR7lLXKgpPpSuVmJU19LMzGrSa2C7k3Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.22.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz", + "integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "1.0.0-pre2", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz", + "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/apps/widget-telegram/package.json b/apps/widget-telegram/package.json new file mode 100644 index 00000000..d11f9685 --- /dev/null +++ b/apps/widget-telegram/package.json @@ -0,0 +1,20 @@ +{ + "name": "@vojo/widget-telegram", + "version": "0.0.1", + "private": true, + "description": "Vojo Telegram bridge management widget — mounts inside /bots/telegram", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "preact": "10.22.1" + }, + "devDependencies": { + "@preact/preset-vite": "2.9.0", + "typescript": "5.4.5", + "vite": "5.4.19" + } +} diff --git a/apps/widget-telegram/src/App.tsx b/apps/widget-telegram/src/App.tsx new file mode 100644 index 00000000..2ceb78c0 --- /dev/null +++ b/apps/widget-telegram/src/App.tsx @@ -0,0 +1,176 @@ +import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import type { WidgetBootstrap } from './bootstrap'; +import { WidgetApi, buildCapabilities, type RoomEvent } from './widget-api'; +import { createT } from './i18n'; + +// Visual canon: «Боты · Commands IDE» mockup at +// docs/design/new-direct-messages-design/project/stream-v2-dawn.jsx +// function BotsDesktop (lines 651-707) — hero with 56px square avatar in +// fleet color, 22/700 name + monospace handle, uppercase muted section +// labels, 2-col command-card grid, monospace transcript. The widget is the +// Telegram-bridge half of that mockup, mounted inside the chat slot. + +type TranscriptLine = { + id: string; + ts: number; + kind: 'from-bot' | 'from-user' | 'diag' | 'error'; + text: string; +}; + +type HandshakeState = 'waiting' | 'ok'; + +type Props = { + bootstrap: WidgetBootstrap; +}; + +const TRANSCRIPT_MAX = 200; + +const formatTime = (ts: number): string => { + const d = new Date(ts); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + const ss = String(d.getSeconds()).padStart(2, '0'); + return `${hh}:${mm}:${ss}`; +}; + +// Initial inferred from preset name: first character of the name, uppercase. +// Falls back to "T" so the avatar never renders blank for the Telegram preset. +const heroInitial = (name: string): string => { + const first = name.trim().charAt(0).toUpperCase(); + return first || 'T'; +}; + +export function App({ bootstrap }: Props) { + const [theme, setTheme] = useState<'light' | 'dark'>(bootstrap.theme); + const [handshake, setHandshake] = useState('waiting'); + const [transcript, setTranscript] = useState([]); + const [pinging, setPinging] = useState(false); + const apiRef = useRef(null); + const seenEventIds = useRef(new Set()); + + const t = useMemo(() => createT(bootstrap.clientLanguage), [bootstrap.clientLanguage]); + const capabilities = useMemo(() => buildCapabilities(bootstrap.roomId), [bootstrap.roomId]); + + useEffect(() => { + document.documentElement.dataset.theme = theme; + }, [theme]); + + const append = (line: Omit) => { + setTranscript((prev) => { + const next = [...prev, { ...line, id: `${Date.now()}-${Math.random()}`, ts: Date.now() }]; + return next.length > TRANSCRIPT_MAX ? next.slice(-TRANSCRIPT_MAX) : next; + }); + }; + + useEffect(() => { + const api = new WidgetApi(bootstrap, capabilities); + apiRef.current = api; + + api.on('ready', () => { + setHandshake('ok'); + append({ kind: 'diag', text: t('diag.ready') }); + }); + + api.on('themeChange', (name) => { + setTheme(name); + }); + + api.on('liveEvent', (ev: RoomEvent) => { + if (seenEventIds.current.has(ev.event_id)) return; + seenEventIds.current.add(ev.event_id); + const body = ev.content.body ?? ''; + const fromUser = ev.sender === bootstrap.userId; + append({ + kind: fromUser ? 'from-user' : 'from-bot', + text: `${fromUser ? '→' : '←'} ${body}`, + }); + }); + + append({ kind: 'diag', text: t('diag.connecting') }); + + return () => { + api.dispose(); + apiRef.current = null; + }; + }, [bootstrap, capabilities, t]); + + const handlePing = async () => { + const api = apiRef.current; + if (!api || pinging || handshake !== 'ok') return; + setPinging(true); + append({ kind: 'from-user', text: '→ ping' }); + try { + await api.sendText('ping'); + } catch (err) { + append({ + kind: 'error', + text: t('diag.send-failed', { message: (err as Error).message }), + }); + } finally { + // Light debounce — bridge bots flag rapid pings as flooding. + window.setTimeout(() => setPinging(false), 1500); + } + }; + + const initial = heroInitial(bootstrap.botMxid.split(':')[0].replace('@', '') || 'telegram'); + + const statusLabel = handshake === 'ok' ? t('status.ok') : t('status.waiting'); + + return ( +
+
+ +
+
+ Telegram + {bootstrap.botMxid} +
+

{t('hero.description')}

+
+
+ + {statusLabel} +
+
+ +
+ +
+ +
+

{t('hint.m11')}

+
+ +
+ +
+ {transcript.length === 0 ? ( +
{t('transcript.empty')}
+ ) : ( + transcript.map((line) => ( +
+ {formatTime(line.ts)} + {line.text} +
+ )) + )} +
+
+
+ ); +} diff --git a/apps/widget-telegram/src/bootstrap.ts b/apps/widget-telegram/src/bootstrap.ts new file mode 100644 index 00000000..0a3574b8 --- /dev/null +++ b/apps/widget-telegram/src/bootstrap.ts @@ -0,0 +1,58 @@ +// Parse the URL params the Phase 2 bot widget host appends when loading +// experience.url. Source of truth on the host side: +// src/app/features/bots/BotWidgetEmbed.ts (getBotWidgetUrl). +// Keep this in sync if the host adds params. + +export type WidgetBootstrap = { + widgetId: string; + parentUrl: string; + parentOrigin: string; + roomId: string; + userId: string; + botId: string; + botMxid: string; + theme: 'light' | 'dark'; + clientLanguage: string; +}; + +export type BootstrapResult = + | { ok: true; bootstrap: WidgetBootstrap } + | { ok: false; missing: string[] }; + +const REQUIRED = ['widgetId', 'parentUrl', 'roomId', 'userId', 'botMxid'] as const; + +export const readBootstrap = (search: string): BootstrapResult => { + const params = new URLSearchParams(search); + const get = (k: string) => params.get(k) ?? ''; + + const missing = REQUIRED.filter((k) => !params.get(k)); + if (missing.length > 0) return { ok: false, missing: [...missing] }; + + // Origin is what the widget validates against on incoming postMessage — + // see widget-api.ts. Falling back to '*' would defeat the security + // boundary, so a malformed parentUrl bails out as a missing-param error. + let parentOrigin: string; + try { + parentOrigin = new URL(get('parentUrl')).origin; + } catch { + return { ok: false, missing: ['parentUrl'] }; + } + + const themeRaw = get('theme'); + const theme: 'light' | 'dark' = themeRaw === 'dark' ? 'dark' : 'light'; + + return { + ok: true, + bootstrap: { + widgetId: get('widgetId'), + parentUrl: get('parentUrl'), + parentOrigin, + roomId: get('roomId'), + userId: get('userId'), + botId: get('botId'), + botMxid: get('botMxid'), + theme, + clientLanguage: get('clientLanguage'), + }, + }; +}; diff --git a/apps/widget-telegram/src/i18n/en.ts b/apps/widget-telegram/src/i18n/en.ts new file mode 100644 index 00000000..772a7368 --- /dev/null +++ b/apps/widget-telegram/src/i18n/en.ts @@ -0,0 +1,22 @@ +// English fallback. Mirror the RU key set; `Record` enforces +// that every RU key has an EN counterpart at compile time. + +import type { StringKey } from './ru'; + +export const EN: Record = { + 'hero.description': + 'Manage the Telegram bridge. Commands are sent as text into the control DM; replies are visible in the transcript.', + 'status.waiting': 'Connecting…', + 'status.ok': 'Ready', + 'section.check': 'Check', + 'section.transcript': 'Transcript', + 'card.ping.desc': 'Check Telegram authentication status via the bot.', + 'hint.m11': 'M11: handshake and bot connectivity check only. Login commands arrive in M12.', + 'transcript.empty': 'empty', + 'diag.connecting': 'Connecting to Vojo… awaiting capability handshake.', + 'diag.ready': 'Ready to send commands.', + 'diag.send-failed': 'send failed: {message}', + 'bootstrap.failed': 'Widget failed to start', + 'bootstrap.missing-params': 'Missing required URL params: {names}.', + 'bootstrap.embedded-only': 'This page is meant to be embedded by Vojo at {route}.', +}; diff --git a/apps/widget-telegram/src/i18n/index.ts b/apps/widget-telegram/src/i18n/index.ts new file mode 100644 index 00000000..686c299a --- /dev/null +++ b/apps/widget-telegram/src/i18n/index.ts @@ -0,0 +1,30 @@ +// Tiny i18n harness. Russian primary, English fallback (BCP-47 prefix match — +// any `en` variant). Bootstrap forwards `clientLanguage` from the host; main.tsx +// can also call `createT()` without args before bootstrap completes (falls back +// to navigator.language, then RU). + +import { RU, type StringKey } from './ru'; +import { EN } from './en'; + +const interpolate = (s: string, vars?: Record): string => { + if (!vars) return s; + return s.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? `{${k}}`); +}; + +const pickDict = (clientLanguage: string | undefined): Record => { + const lang = ( + clientLanguage || + (typeof navigator !== 'undefined' ? navigator.language : '') || + 'ru' + ).toLowerCase(); + return lang.startsWith('en') ? EN : RU; +}; + +export type T = (key: StringKey, vars?: Record) => string; + +export const createT = (clientLanguage?: string): T => { + const dict = pickDict(clientLanguage); + return (key, vars) => interpolate(dict[key], vars); +}; + +export type { StringKey }; diff --git a/apps/widget-telegram/src/i18n/ru.ts b/apps/widget-telegram/src/i18n/ru.ts new file mode 100644 index 00000000..a9f7f38c --- /dev/null +++ b/apps/widget-telegram/src/i18n/ru.ts @@ -0,0 +1,26 @@ +// Russian primary copy. To add a string: +// 1. add the key + RU value here (this file is the canonical key list — `en.ts` +// and the `StringKey` type derive from it), +// 2. add the same key + EN value in `en.ts`, +// 3. consume via `t('key', { var: 'x' })` in components. +// Interpolation uses `{name}` placeholders resolved against the second arg. + +export const RU = { + 'hero.description': + 'Управление мостом Telegram. Команды отправляются текстом в контрольный DM, ответы видны в транскрипте.', + 'status.waiting': 'Подключение…', + 'status.ok': 'Готов', + 'section.check': 'Проверка', + 'section.transcript': 'Транскрипт', + 'card.ping.desc': 'Проверить статус авторизации в Telegram через бот.', + 'hint.m11': "M11: только проверка handshake'а и связи с ботом. Команды логина появятся в M12.", + 'transcript.empty': 'пусто', + 'diag.connecting': 'Соединение с Vojo… ожидаем capability handshake.', + 'diag.ready': 'Готов отправлять команды.', + 'diag.send-failed': 'ошибка отправки: {message}', + 'bootstrap.failed': 'Widget не запустился', + 'bootstrap.missing-params': 'Отсутствуют обязательные параметры URL: {names}.', + 'bootstrap.embedded-only': 'Эта страница предназначена для встраивания Vojo по маршруту {route}.', +} as const; + +export type StringKey = keyof typeof RU; diff --git a/apps/widget-telegram/src/main.tsx b/apps/widget-telegram/src/main.tsx new file mode 100644 index 00000000..5359d0db --- /dev/null +++ b/apps/widget-telegram/src/main.tsx @@ -0,0 +1,36 @@ +import { render } from 'preact'; +import { readBootstrap } from './bootstrap'; +import { App } from './App'; +import { createT } from './i18n'; +import './styles.css'; + +const root = document.getElementById('app'); +if (!root) { + throw new Error('#app root element missing — index.html out of sync'); +} + +const result = readBootstrap(window.location.search); + +if (!result.ok) { + // Either someone opened the widget URL directly (no host params), or a + // host bug failed to provide them. Either way render a self-contained + // diagnostic instead of going silent. Bootstrap failed before we could + // read clientLanguage from the URL, so let createT fall back to + // navigator.language. + const t = createT(); + render( +
+
+ {t('bootstrap.failed')} + {t('bootstrap.missing-params', { names: result.missing.join(', ') })}{' '} + {t('bootstrap.embedded-only', { route: '/bots/telegram' })} +
+
, + root + ); +} else { + // Apply initial theme synchronously so the first paint isn't flashed + // through the wrong palette. + document.documentElement.dataset.theme = result.bootstrap.theme; + render(, root); +} diff --git a/apps/widget-telegram/src/styles.css b/apps/widget-telegram/src/styles.css new file mode 100644 index 00000000..1cfdd3f9 --- /dev/null +++ b/apps/widget-telegram/src/styles.css @@ -0,0 +1,368 @@ +/* Dawn palette — must stay in sync with + * docs/design/new-direct-messages-design/project/stream-v2-dawn.jsx + * (DAWN const, lines 4-23). The widget renders inside the Vojo chat slot + * which is itself a Dawn surface; the iframe inherits the same visual + * canon to feel like a continuation of the host. */ + +:root { + --bg: #181a20; + --bg2: #0d0e11; + --surface: #21232b; + --surface2: #2a2d36; + --divider: rgba(255, 255, 255, 0.06); + --hairline: rgba(255, 255, 255, 0.08); + --text: #e6e6e9; + --muted: rgba(230, 230, 233, 0.55); + --faint: rgba(230, 230, 233, 0.32); + --fleet: #9580ff; + --fleet-soft: #a59cff; + --green: #7dd3a8; + --amber: #d4b88a; + --rose: #c08e7b; + --section-pad-x: 40px; +} + +[data-theme='light'] { + /* Light theme is intentionally a thin remap. Vojo is dark-default; the + * theme param exists so we don't fight an explicit user/host setting, + * not because we expect daily light-mode use. */ + --bg: #f5f5f7; + --bg2: #ffffff; + --surface: #f0f0f2; + --surface2: #e8e8ec; + --divider: rgba(0, 0, 0, 0.08); + --hairline: rgba(0, 0, 0, 0.1); + --text: #1a1a1d; + --muted: rgba(26, 26, 29, 0.62); + --faint: rgba(26, 26, 29, 0.4); +} + +@media (max-width: 600px) { + :root { + --section-pad-x: 20px; + } +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + height: 100%; +} + +body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); + font: 14px/1.45 -apple-system, 'Segoe UI', 'Inter', system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +.app { + display: flex; + flex-direction: column; + min-height: 100%; + max-width: 960px; + margin: 0 auto; +} + +/* ── Hero ─────────────────────────────────────────────────────────── */ + +.hero { + display: flex; + align-items: flex-start; + gap: 18px; + padding: 36px var(--section-pad-x) 28px; + border-bottom: 1px solid var(--divider); +} + +.hero-avatar { + width: 56px; + height: 56px; + border-radius: 14px; + background: var(--fleet); + color: #0c0c0e; + font-size: 24px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.hero-body { + flex: 1; + min-width: 0; +} + +.hero-title-row { + display: flex; + align-items: baseline; + gap: 10px; + margin-bottom: 4px; + flex-wrap: wrap; +} + +.hero-name { + font-size: 22px; + font-weight: 700; + color: var(--text); +} + +.hero-handle { + font-size: 13px; + color: var(--faint); + font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', monospace; + word-break: break-all; +} + +.hero-description { + font-size: 14px; + line-height: 20px; + color: var(--muted); + max-width: 560px; +} + +.hero-status { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + border-radius: 8px; + border: 1px solid var(--divider); + font-size: 13px; + color: var(--muted); + flex-shrink: 0; + white-space: nowrap; +} + +.hero-status .dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--faint); + flex-shrink: 0; +} + +.hero-status.ok { + color: var(--green); +} +.hero-status.ok .dot { + background: var(--green); + box-shadow: 0 0 0 3px rgba(125, 211, 168, 0.16); +} +.hero-status.waiting { + color: var(--amber); +} +.hero-status.waiting .dot { + background: var(--amber); +} +.hero-status.error { + color: var(--rose); +} +.hero-status.error .dot { + background: var(--rose); +} + +@media (max-width: 600px) { + .hero { + flex-wrap: wrap; + gap: 14px; + padding-top: 24px; + padding-bottom: 18px; + } + .hero-status { + order: 3; + margin-left: 0; + } + .hero-name { + font-size: 19px; + } + .hero-avatar { + width: 48px; + height: 48px; + font-size: 20px; + } +} + +/* ── Section ──────────────────────────────────────────────────────── */ + +.section { + padding: 24px var(--section-pad-x) 20px; +} + +.section + .section { + padding-top: 4px; +} + +.section-label { + font-size: 12px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 1.4px; + font-weight: 600; + margin-bottom: 14px; +} + +/* ── Command card (single + 2-col grid both fit) ─────────────────── */ + +.command-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 10px; +} + +.command-card { + background: var(--bg2); + border: 1px solid var(--divider); + border-radius: 10px; + padding: 14px 16px; + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + text-align: left; + font: inherit; + color: inherit; + transition: border-color 0.12s, background 0.12s; +} + +.command-card:hover:not(:disabled) { + background: var(--surface); + border-color: var(--hairline); +} + +.command-card:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.command-card-body { + flex: 1; + min-width: 0; +} + +.command-card-name { + font-size: 14px; + font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', monospace; + color: var(--fleet-soft); + font-weight: 500; + margin-bottom: 3px; +} + +.command-card-desc { + font-size: 13px; + color: var(--muted); + line-height: 18px; +} + +.command-card-chevron { + color: var(--muted); + font-size: 18px; + flex-shrink: 0; + line-height: 1; +} + +/* ── Transcript ──────────────────────────────────────────────────── */ + +.transcript { + background: var(--bg2); + border: 1px solid var(--divider); + border-radius: 10px; + padding: 12px 14px; + font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', monospace; + font-size: 12.5px; + line-height: 1.55; + max-height: 360px; + overflow-y: auto; +} + +.transcript-line { + padding: 4px 0; + display: flex; + gap: 10px; + align-items: flex-start; + white-space: pre-wrap; + word-break: break-word; +} + +.transcript-line + .transcript-line { + border-top: 1px dashed var(--divider); +} + +.transcript-line .ts { + color: var(--faint); + flex-shrink: 0; + font-variant-numeric: tabular-nums; +} + +.transcript-line .body { + flex: 1; + min-width: 0; +} + +.transcript-line.from-bot .body { + color: var(--text); +} + +.transcript-line.from-user .body { + color: var(--fleet-soft); +} + +.transcript-line.diag .body { + color: var(--muted); +} + +.transcript-line.error .body { + color: var(--rose); +} + +.transcript-empty { + color: var(--faint); + text-align: center; + padding: 16px 0; + font-style: italic; +} + +/* ── Hint text ───────────────────────────────────────────────────── */ + +.hint { + font-size: 12px; + color: var(--faint); + margin-top: 8px; + line-height: 17px; +} + +/* ── Diagnostic banner (pre-bootstrap failure) ───────────────────── */ + +.error-banner { + margin: var(--section-pad-x); + padding: 14px 16px; + background: rgba(192, 142, 123, 0.08); + border: 1px solid var(--rose); + border-radius: 10px; + color: var(--rose); + font-size: 13px; + line-height: 19px; +} + +.error-banner strong { + display: block; + margin-bottom: 4px; + color: var(--rose); + font-weight: 600; +} + +.error-banner code { + background: var(--bg2); + padding: 1px 6px; + border-radius: 4px; + font-family: ui-monospace, 'JetBrains Mono', monospace; + font-size: 12px; + color: var(--text); +} diff --git a/apps/widget-telegram/src/widget-api.ts b/apps/widget-telegram/src/widget-api.ts new file mode 100644 index 00000000..d3631c82 --- /dev/null +++ b/apps/widget-telegram/src/widget-api.ts @@ -0,0 +1,229 @@ +// Minimal matrix-widget-api transport implemented inline. We don't pull +// the full SDK because: +// - it's CommonJS and forces ESM interop juggling we hit on the dev +// fixture in Phase 2 (esm.sh wrapping made WidgetApi unavailable as +// a constructor); +// - the surface we use is small: capabilities reply, theme_change reply, +// send_event request, read_events request, get_openid request, live +// event delivery via send_event toWidget. +// +// Protocol shapes match +// node_modules/matrix-widget-api/lib/transport/PostmessageTransport.ts +// (in the host repo). Default request timeout on the host transport is +// 10 s — keep that in mind for bridge-bot replies that take time. + +import type { WidgetBootstrap } from './bootstrap'; + +export type RoomEvent = { + type: string; + event_id: string; + room_id: string; + sender: string; + origin_server_ts: number; + content: { msgtype?: string; body?: string; [k: string]: unknown }; + unsigned: Record; +}; + +type ToWidgetMessage = { + api: 'toWidget'; + widgetId: string; + requestId: string; + action: string; + data: Record; + // Present when this message IS a reply to a prior toWidget request. + // Per matrix-widget-api PostmessageTransport: replies preserve the original + // `api` field and add `response`. Both directions follow the same shape. + response?: Record; +}; + +type FromWidgetMessage = { + api: 'fromWidget'; + widgetId: string; + requestId: string; + action: string; + data: Record; + response?: Record; +}; + +export type Capability = string; + +export type WidgetApiEvents = { + ready: () => void; + liveEvent: (ev: RoomEvent) => void; + themeChange: (name: 'light' | 'dark') => void; +}; + +const FROM_WIDGET_REQUEST_TIMEOUT_MS = 10_000; + +export class WidgetApi { + private readonly listeners: { [K in keyof WidgetApiEvents]?: Array } = {}; + + private readonly pending = new Map< + string, + { resolve: (v: Record) => void; reject: (e: Error) => void } + >(); + + private requestSeq = 0; + + private isReady = false; + + public constructor( + private readonly bootstrap: WidgetBootstrap, + private readonly capabilities: Capability[] + ) { + window.addEventListener('message', this.onMessage); + } + + public dispose(): void { + window.removeEventListener('message', this.onMessage); + this.pending.forEach(({ reject }) => reject(new Error('disposed'))); + this.pending.clear(); + } + + public on(event: K, listener: WidgetApiEvents[K]): void { + const list = (this.listeners[event] ??= []) as Array; + list.push(listener); + } + + public sendText(body: string): Promise<{ event_id: string }> { + return this.fromWidget('send_event', { + type: 'm.room.message', + content: { msgtype: 'm.text', body }, + }) as Promise<{ event_id: string }>; + } + + private emit( + event: K, + ...args: Parameters + ): void { + const list = this.listeners[event] as + | Array<(...a: Parameters) => void> + | undefined; + list?.forEach((fn) => fn(...args)); + } + + private nextRequestId(): string { + this.requestSeq += 1; + return `widget-tg-${Date.now()}-${this.requestSeq}`; + } + + private postToHost(msg: ToWidgetMessage | FromWidgetMessage): void { + window.parent.postMessage(msg, this.bootstrap.parentOrigin); + } + + private onMessage = (ev: MessageEvent): void => { + if (ev.origin !== this.bootstrap.parentOrigin) return; + const msg = ev.data as ToWidgetMessage | FromWidgetMessage | undefined; + if (!msg || typeof msg !== 'object') return; + if (msg.widgetId !== this.bootstrap.widgetId) return; + + if (msg.api === 'toWidget') { + this.handleToWidget(msg); + return; + } + + if (msg.api === 'fromWidget' && msg.response) { + const pending = this.pending.get(msg.requestId); + if (!pending) return; + this.pending.delete(msg.requestId); + const err = (msg.response as { error?: { message?: string } }).error; + if (err) pending.reject(new Error(err.message ?? 'request failed')); + else pending.resolve(msg.response); + } + }; + + private replyTo(msg: ToWidgetMessage, response: Record): void { + this.postToHost({ + api: msg.api, + widgetId: msg.widgetId, + requestId: msg.requestId, + action: msg.action, + data: msg.data, + response, + }); + } + + private handleToWidget(msg: ToWidgetMessage): void { + if (!msg.requestId || !msg.action) return; + switch (msg.action) { + case 'capabilities': { + this.replyTo(msg, { capabilities: this.capabilities }); + return; + } + case 'notify_capabilities': { + this.replyTo(msg, {}); + if (!this.isReady) { + this.isReady = true; + this.emit('ready'); + } + return; + } + case 'supported_api_versions': { + this.replyTo(msg, { supported_versions: ['0.0.2', 'org.matrix.msc2762'] }); + return; + } + case 'theme_change': { + const name = (msg.data?.name as string | undefined) ?? ''; + const themed: 'light' | 'dark' = name === 'dark' ? 'dark' : 'light'; + this.emit('themeChange', themed); + this.replyTo(msg, {}); + return; + } + case 'send_event': { + // Live event push from host. We forward only m.room.message — + // m.room.member state updates also arrive here but we don't + // surface them in M11. + const data = msg.data as Partial | undefined; + if (data && data.type === 'm.room.message' && data.event_id) { + this.emit('liveEvent', data as RoomEvent); + } + this.replyTo(msg, {}); + return; + } + case 'update_state': { + // Initial room state push from host (m.room.member members). + // M11 ignores this; future milestones can use it for header chrome. + this.replyTo(msg, {}); + return; + } + default: { + // Be liberal — reply empty so the host's request promise resolves. + this.replyTo(msg, {}); + } + } + } + + private fromWidget( + action: string, + data: Record + ): Promise> { + return new Promise((resolve, reject) => { + const requestId = this.nextRequestId(); + this.pending.set(requestId, { resolve, reject }); + this.postToHost({ + api: 'fromWidget', + widgetId: this.bootstrap.widgetId, + requestId, + action, + data, + }); + window.setTimeout(() => { + if (this.pending.has(requestId)) { + this.pending.delete(requestId); + reject(new Error(`${action} timed out after ${FROM_WIDGET_REQUEST_TIMEOUT_MS}ms`)); + } + }, FROM_WIDGET_REQUEST_TIMEOUT_MS); + }); + } +} + +// Capability set must match docs/plans/bots_tab.md (Phase 3 contract) and +// the host's BotWidgetDriver.getBotWidgetCapabilities. Anything else is +// silently dropped by the host's validateCapabilities — keep this aligned. +export const buildCapabilities = (roomId: string): Capability[] => [ + `org.matrix.msc2762.timeline:${roomId}`, + 'org.matrix.msc2762.send.event:m.room.message#m.text', + 'org.matrix.msc2762.receive.event:m.room.message#m.text', + 'org.matrix.msc2762.receive.event:m.room.message#m.notice', + 'org.matrix.msc2762.receive.state_event:m.room.member', +]; diff --git a/apps/widget-telegram/tsconfig.json b/apps/widget-telegram/tsconfig.json new file mode 100644 index 00000000..42f8fdbc --- /dev/null +++ b/apps/widget-telegram/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "jsxImportSource": "preact", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "useDefineForClassFields": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/apps/widget-telegram/vite.config.ts b/apps/widget-telegram/vite.config.ts new file mode 100644 index 00000000..f441de4e --- /dev/null +++ b/apps/widget-telegram/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import preact from '@preact/preset-vite'; + +// Build artefact lives at apps/widget-telegram/dist/. The deploy step +// (out of repo) rsyncs this into ~/vojo/widgets/telegram/ on the server, +// which Caddy serves from /var/www/widgets/telegram via the +// widgets.vojo.chat block (see docs/plans/bots_tab.md Phase 3). +// +// `base: './'` keeps every generated asset path relative so the same +// build can sit under /telegram/ on widgets.vojo.chat without rewrites. +export default defineConfig({ + base: './', + plugins: [preact()], + build: { + target: 'es2020', + sourcemap: true, + // Inline CSS for a single round-trip; the widget is small and the + // host's iframe handshake budget is already tight (10s default). + cssCodeSplit: false, + }, + server: { + port: 8081, + host: true, + }, +}); diff --git a/config.json b/config.json index a4c430aa..1ee27fd8 100644 --- a/config.json +++ b/config.json @@ -18,7 +18,11 @@ { "id": "telegram", "mxid": "@telegrambot:vojo.chat", - "name": "Telegram" + "name": "Telegram", + "experience": { + "type": "matrix-widget", + "url": "https://widgets.vojo.chat/telegram/index.html" + } } ], "push": { diff --git a/src/app/features/bots/catalog.ts b/src/app/features/bots/catalog.ts index 8fda997b..19d640d9 100644 --- a/src/app/features/bots/catalog.ts +++ b/src/app/features/bots/catalog.ts @@ -19,6 +19,15 @@ export type BotPreset = { const BOT_ID_RE = /^[A-Za-z0-9_-]+$/; const MXID_RE = /^@[^:\s]+:[^\s]+$/; +// Defense-in-depth allowlist of widget origins acceptable in production. The +// BotWidgetDriver capability allowlist (M9) already tightly scopes what a +// widget can do — only m.text/m.notice in the current control DM, no media, +// no OpenID, no cross-room. This origin pin is an additional layer that +// catches operator-config typos or a poisoned config.json that points the +// iframe at an unrelated host. Add new entries when onboarding new widget +// hosts; the dev branch below bypasses this check for `http://localhost:*`. +const PROD_WIDGET_ORIGINS: ReadonlySet = new Set(['https://widgets.vojo.chat']); + const isValidBotPreset = (preset: BotConfig | undefined): preset is BotPreset => typeof preset?.id === 'string' && BOT_ID_RE.test(preset.id) && @@ -63,8 +72,18 @@ const normalizeBotExperience = (experience: BotConfig['experience']): BotExperie try { const parsed = new URL(url); + // Dev-only escape hatch: accept http://localhost:* widget URLs so a Vite + // dev server in `apps/widget-/` can be embedded straight from a local + // config.json edit, no proxy or rewrites needed. Vite's dead-code + // elimination drops this branch in production builds (`import.meta.env.DEV` + // collapses to a literal `false`), so it never relaxes the prod validator. + if (import.meta.env.DEV && parsed.protocol === 'http:' && parsed.hostname === 'localhost') { + if (parsed.username || parsed.password) return undefined; + return { type, url: parsed.toString() }; + } if (parsed.protocol !== 'https:') return undefined; if (parsed.username || parsed.password) return undefined; + if (!PROD_WIDGET_ORIGINS.has(parsed.origin)) return undefined; return { type, url: parsed.toString() }; } catch { return undefined; diff --git a/vite.config.js b/vite.config.js index 7bd0c0fe..ae4accd2 100644 --- a/vite.config.js +++ b/vite.config.js @@ -78,6 +78,74 @@ const copyFiles = { ], }; +// Dev-only overlay for runtime config. The SPA fetches `/config.json` at boot +// to read homeserver list, bot presets, push gateway config, etc. — production +// ships this file unmodified from `~/vojo/cinny/config.json` on the server. +// Locally we want per-developer overrides (e.g. `bots[id=telegram].experience.url` +// → `http://localhost:8081/` for a widget dev server) WITHOUT touching the +// committed `config.json`. This middleware reads optional `config.local.json` +// at the project root and overlays it on top, then serves the merged JSON. +// +// Merge contract: +// - top-level fields: shallow override (local wins). +// - `bots[]`: merged by `id` — the local entry shallow-merges over the base +// entry with the same id, so `{ id: "telegram", experience: {...} }` is +// enough to override one field of an existing bot. Bots that exist only +// in local are appended as-is. +// +// Production builds ignore this plugin (`apply: 'serve'`) so prod +// `config.json` is served untouched by Caddy. `config.local.json` is in +// `.gitignore` and never deployed. +function mergeBotsById(base, local) { + if (!Array.isArray(local)) return base; + if (!Array.isArray(base)) return local; + const byId = new Map(); + base.forEach((bot) => { + if (bot && typeof bot.id === 'string') byId.set(bot.id, bot); + }); + local.forEach((overlay) => { + if (!overlay || typeof overlay.id !== 'string') return; + const baseBot = byId.get(overlay.id); + byId.set(overlay.id, baseBot ? { ...baseBot, ...overlay } : overlay); + }); + return [...byId.values()]; +} + +function mergeRuntimeConfig(base, local) { + if (!local || typeof local !== 'object') return base; + const merged = { ...base, ...local }; + if (Array.isArray(local.bots) || Array.isArray(base?.bots)) { + merged.bots = mergeBotsById(base?.bots, local.bots); + } + return merged; +} + +function serveLocalConfigOverlay() { + return { + name: 'vite-plugin-serve-local-config-overlay', + apply: 'serve', + configureServer(server) { + server.middlewares.use('/config.json', (req, res, next) => { + const localPath = path.resolve('config.local.json'); + if (!fs.existsSync(localPath)) { + next(); + return; + } + try { + const baseRaw = fs.readFileSync(path.resolve('config.json'), 'utf8'); + const localRaw = fs.readFileSync(localPath, 'utf8'); + const merged = mergeRuntimeConfig(JSON.parse(baseRaw), JSON.parse(localRaw)); + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Cache-Control', 'no-cache'); + res.end(JSON.stringify(merged)); + } catch (err) { + next(err); + } + }); + }, + }; +} + function serverMatrixSdkCryptoWasm(wasmFilePath) { return { name: 'vite-plugin-serve-matrix-sdk-crypto-wasm', @@ -123,6 +191,7 @@ export default defineConfig({ }, }, plugins: [ + serveLocalConfigOverlay(), serverMatrixSdkCryptoWasm('/node_modules/.vite/deps/pkg/matrix_sdk_crypto_wasm_bg.wasm'), topLevelAwait({ // The export name of top-level await promise for each chunk module