Compare commits
4 Commits
codex/dung
...
codex/dung
| Author | SHA1 | Date | |
|---|---|---|---|
| 75e8fec4fe | |||
| 33dbf37ae3 | |||
| 223b43de97 | |||
| d9560f60dc |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ data/
|
|||||||
*.log
|
*.log
|
||||||
frontend/dist
|
frontend/dist
|
||||||
.webka-cache.json
|
.webka-cache.json
|
||||||
|
webka.config.json
|
||||||
|
|||||||
52
README.md
52
README.md
@@ -24,15 +24,58 @@ The terminal prints the local URL, LAN URLs when available, and the setup-token
|
|||||||
|
|
||||||
Press **Ctrl+C** to stop the app and any build subprocesses. Accounts remain in `data/`. The first login page creates the administrator account using the token from `data/setup-token`. The administrator cannot be deleted; resetting its password ends its sessions.
|
Press **Ctrl+C** to stop the app and any build subprocesses. Accounts remain in `data/`. The first login page creates the administrator account using the token from `data/setup-token`. The administrator cannot be deleted; resetting its password ends its sessions.
|
||||||
|
|
||||||
For HTTPS behind a reverse proxy, set `WEBKA_ORIGIN` to the exact public origin before launching. This override disables automatic HTTP aliases; optional `WEBKA_ALLOWED_ORIGINS` is a comma-separated list of additional trusted exact origins. Browser capture on another LAN device still requires trusted HTTPS; plain LAN HTTP permits viewing only.
|
For an existing HTTPS proxy, use the homepage setting below. Browser capture on another LAN device still requires a trusted HTTPS address.
|
||||||
|
|
||||||
For frontend hot reload, the separate development workflow remains available: run `cargo run --manifest-path backend/Cargo.toml` and `npm --prefix frontend run dev` in separate terminals, then open `http://localhost:3000`.
|
For frontend hot reload, the separate development workflow remains available: run `cargo run --manifest-path backend/Cargo.toml` and `npm --prefix frontend run dev` in separate terminals, then open `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Existing Nginx Proxy Manager on another machine
|
||||||
|
|
||||||
|
Set just the address you want to open in a browser (the address served by NPM):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev:all -- --homepage https://camera.example.com --port 3002
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `https://camera.example.com` with your actual HTTPS domain or IP, including a public port if needed. A bare hostname/IP is treated as HTTPS. A trailing slash is accepted; hosting under a subpath is not supported.
|
||||||
|
|
||||||
|
This mode automatically:
|
||||||
|
|
||||||
|
- Listens on `0.0.0.0` so the other machine can reach Webka (unless you explicitly set `--host`).
|
||||||
|
- Serves the frontend and API over **HTTP** on the chosen internal port; your existing NPM handles HTTPS.
|
||||||
|
- Uses the homepage for QR/share links, secure cookies, and allowed request origins.
|
||||||
|
- Disables mDNS, even if `WEBKA_MDNS=1` was previously set. No Caddy, `.local` hostname, or certificates on the Webka computer are needed.
|
||||||
|
|
||||||
|
In **Nginx Proxy Manager → Proxy Hosts**, configure:
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---|---|
|
||||||
|
| Domain Names | Your homepage hostname, e.g. `camera.example.com` |
|
||||||
|
| Scheme | `http` |
|
||||||
|
| Forward Hostname / IP | The LAN IP of the **Webka computer**, e.g. `192.168.1.50` |
|
||||||
|
| Forward Port | `3002` (or your `--port`) |
|
||||||
|
| Websockets Support | Enabled |
|
||||||
|
| SSL | Select your existing trusted certificate; enable Force SSL |
|
||||||
|
|
||||||
|
Open the homepage HTTPS address on both the host and viewer. Its hostname must resolve to the **NPM machine**, and NPM must be able to reach the Webka machine's LAN IP/port. The homepage is the browser-facing address, not the internal upstream address. For an HTTPS IP address, the certificate must cover that IP and NPM must route it to Webka. See the [official NPM guide](https://nginxproxymanager.com/guide/) and [WebSocket proxy support](https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/backend/templates/proxy_host.conf).
|
||||||
|
|
||||||
|
To save the address, create `webka.config.json` in the repository root (or copy `webka.config.example.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"homepage": "https://camera.example.com",
|
||||||
|
"port": 3002
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start with only `npm run dev:all`. `homepage` alone is sufficient if the internal port is the default `3000`. Optional `host` and `port` may also be saved; command-line options override the file. Restart to apply changes. This machine-specific file is ignored by Git. Homepage mode takes priority over legacy `WEBKA_ORIGIN`, `WEBKA_ALLOWED_ORIGINS`, and `WEBKA_MDNS` values.
|
||||||
|
|
||||||
|
Legacy manual proxy setup via `WEBKA_ORIGIN` and `WEBKA_ALLOWED_ORIGINS` remains supported when no homepage is configured. The launcher does not configure NPM or provision its certificates. The proxy carries page/API/signaling traffic; WebRTC media remains peer-to-peer on the LAN.
|
||||||
|
|
||||||
## Use on your LAN
|
## Use on your LAN
|
||||||
|
|
||||||
A webpage **cannot launch a Rust server** on the device running its browser. Start the Rust service once on a computer on your LAN. A phone, tablet, or computer can then sign in as the administrator and act as the capturing host. This repository does not include a native iOS/Android service wrapper. Each service instance owns its account database.
|
A webpage **cannot launch a Rust server** on the device running its browser. Start the Rust service once on a computer on your LAN. A phone, tablet, or computer can then sign in as the administrator and act as the capturing host. This repository does not include a native iOS/Android service wrapper. Each service instance owns its account database.
|
||||||
|
|
||||||
Browser camera/screen APIs require a **secure context**: localhost on the service machine, or trusted HTTPS on a LAN device. LAN HTTP IP addresses cannot capture camera/screen. For LAN use, use the included `Caddyfile.example` with a trusted local certificate:
|
Browser camera/screen APIs require a **secure context**: localhost on the service machine, or trusted HTTPS on a LAN device. LAN HTTP IP addresses cannot capture camera/screen. If you already have an HTTPS proxy, use the homepage mode above. If you do not, the included `Caddyfile.example` is an optional alternative with a trusted local certificate:
|
||||||
|
|
||||||
1. Point `webka.local` at the service computer, through mDNS (`WEBKA_MDNS=1`), your router, or a hosts entry. `.local` resolution and multicast support vary by network/device; a local DNS name or IP with a matching trusted certificate also works.
|
1. Point `webka.local` at the service computer, through mDNS (`WEBKA_MDNS=1`), your router, or a hosts entry. `.local` resolution and multicast support vary by network/device; a local DNS name or IP with a matching trusted certificate also works.
|
||||||
2. Configure Caddy with the example below, and install/trust Caddy’s local CA on **every** client device. Keep this CA private.
|
2. Configure Caddy with the example below, and install/trust Caddy’s local CA on **every** client device. Keep this CA private.
|
||||||
@@ -81,6 +124,7 @@ Nuxt 3 is retained to match the requested stack. Its upstream documentation now
|
|||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
npm run test:install
|
||||||
npm run test:launcher
|
npm run test:launcher
|
||||||
cargo fmt --manifest-path backend/Cargo.toml --check
|
cargo fmt --manifest-path backend/Cargo.toml --check
|
||||||
cargo test --manifest-path backend/Cargo.toml
|
cargo test --manifest-path backend/Cargo.toml
|
||||||
@@ -92,6 +136,10 @@ cargo build --manifest-path backend/Cargo.toml
|
|||||||
python3 scripts/smoke.py
|
python3 scripts/smoke.py
|
||||||
# Optional POSIX integration test: reserves port 3002, tests startup/restart/stop.
|
# Optional POSIX integration test: reserves port 3002, tests startup/restart/stop.
|
||||||
python3 scripts/test-launcher.py
|
python3 scripts/test-launcher.py
|
||||||
|
# Optional POSIX HTTPS proxy integration test (requires OpenSSL).
|
||||||
|
python3 scripts/test-proxy.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`test:install` checks the frontend lockfile from a temporary folder without `node_modules`. When updating dependencies, run it with both npm 10 (bundled with Node 22) and npm 11; root `npm install` only installs the launcher package and does not repair the frontend lockfile.
|
||||||
|
|
||||||
See `docs/API.md` for REST/signaling contracts and `docs/MANUAL_TEST_PLAN.md` for real-device checks. Design references: [Apple’s introduction](https://www.apple.com/uk/newsroom/2025/06/apple-introduces-a-delightful-and-elegant-new-software-design/) and [Meet Liquid Glass](https://developer.apple.com/videos/play/wwdc2025/219/). No Apple assets are included. Nothing is published by the build commands.
|
See `docs/API.md` for REST/signaling contracts and `docs/MANUAL_TEST_PLAN.md` for real-device checks. Design references: [Apple’s introduction](https://www.apple.com/uk/newsroom/2025/06/apple-introduces-a-delightful-and-elegant-new-software-design/) and [Meet Liquid Glass](https://developer.apple.com/videos/play/wwdc2025/219/). No Apple assets are included. Nothing is published by the build commands.
|
||||||
|
|||||||
585
frontend/package-lock.json
generated
585
frontend/package-lock.json
generated
@@ -1737,9 +1737,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1928,6 +1925,293 @@
|
|||||||
"node": ">=18.12.0"
|
"node": ">=18.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-android-arm64": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-sg/6Ez0KdAygsu0POELux9wN1Po2CP93WY8eNl4DBKIGprsd4QSHBXOb471Pu9i2OCD5sLkISSb2agZEhVn2Zw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-darwin-arm64": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-yiSJmzGUvCUaJT8X3j40gVcX+ckuHQMuiOtF8DvzTs5+JtB/7XuHFPp4M+vv5u+HlBtDUd4Ks5pyHpWz8mfnkg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-darwin-x64": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-6ZeklaamrMy4H2JmhvcJg6iip59tYILtuLaILxyAHT3l5FDxnI5ihVievAft5ZmAbqtlWHErOi1OpJK8gy1wcA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-freebsd-x64": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-vFsPx+a/qFECPnz/H8nC6x6MDvnWscLTCo/5muojEF54ERUq1kdgbvnWo95YnkhjF9sTIcG/uDxQBh1gffaufQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-eOr3M+6iGbbxNL4PSS0VtsyQ2eOUxSBh00BqO22SbolDimPSYsBuLr/LCrZBkiqW2BoabhR6V4R8jrRAay7hjg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-58ZKDw0mQRbCNfrd2IDyV4o8T7enzGERJn41BH2tjrZVGyiKiFzcfDicuB7Zcpb/1xIOrObovr8Dja6lZi8dLw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-linux-arm64-gnu": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-Fnu95O4eZ5i++GPvIzBEZ8y4ddTLR+D9paYa8JRaRk6ZK7nHQiWP5xtrhcPQsXqgat1d7sU/d5rbbI0p1FTHSQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-linux-arm64-musl": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-3CQy/BMdx7N7H3qrcPxUL+a2CwUZodUcf6oq8iJuNZ9C6Ol1aq3mcWzsgySJ7CHFLvpX21ZDPp1r1X0QLbu/AQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-2GBiM9h26dR4WJfhoMvnFMnFLf7m/kYs4UMqjvrOfQG4BV1nuTJDH22Zc2MQr3INZF7nSKYQ6xlhD3hQ7A6gug==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-linux-s390x-gnu": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-9oUHvnTbp7ZraFsTC8PN6XhdhPSSxZumYvixWl7Smi353gEULvK6yV0sXNVrdFMHQeaDKFCi8TgDhNK7/A+Y+Q==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-linux-x64-gnu": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-2qhDSJwKzbSZzF7lDqqk8sr/yXsmwr3PeUa4/nazIF+zFAYz1gVPEfC34GQtGxzJUUmklaYAL63368LEfrMeyw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-linux-x64-musl": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-qQoPDZUFV0bh9xA09XydmkjMBpgc1ukJuhMvzQ9QeVmFaHTS9W5TE5CoLmSl3QQyUP9OuHO3x/WPZTIIZPWR3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-win32-arm64-msvc": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-pWKdzRDNG2+NK4h/V6U/CYERcfYD6u28h5IB/VJVsrZaD3muvE58tUj22lieL5vLZ+XFi1GPv9YXckZbJZ9BLA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-parser/binding-win32-x64-msvc": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-Ye6vB7VQulghWYkYkECOBYFRVEizz4XyRTUAv+t8BuyurhKU7uD0P9eowL+mKG5Mf8MSYx+DI3Cm8SKZvYG7bQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/@oxc-project/types": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/oxc-project"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/oxc-parser": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-syxUKHeUll89RIABQADcI7sikYrwyssvA6gj4phSSIPezKVM8yMaLAiLLSc7fmzVvrwybfFGFbW5zme9sX87rg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@oxc-project/types": "^0.148.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/oxc-project"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@oxc-parser/binding-android-arm-eabi": "0.148.0",
|
||||||
|
"@oxc-parser/binding-android-arm64": "0.148.0",
|
||||||
|
"@oxc-parser/binding-darwin-arm64": "0.148.0",
|
||||||
|
"@oxc-parser/binding-darwin-x64": "0.148.0",
|
||||||
|
"@oxc-parser/binding-freebsd-x64": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-arm-gnueabihf": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-arm-musleabihf": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-arm64-gnu": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-arm64-musl": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-ppc64-gnu": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-riscv64-gnu": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-riscv64-musl": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-s390x-gnu": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-x64-gnu": "0.148.0",
|
||||||
|
"@oxc-parser/binding-linux-x64-musl": "0.148.0",
|
||||||
|
"@oxc-parser/binding-openharmony-arm64": "0.148.0",
|
||||||
|
"@oxc-parser/binding-win32-arm64-msvc": "0.148.0",
|
||||||
|
"@oxc-parser/binding-win32-ia32-msvc": "0.148.0",
|
||||||
|
"@oxc-parser/binding-win32-x64-msvc": "0.148.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nuxt/kit/node_modules/unctx": {
|
"node_modules/@nuxt/kit/node_modules/unctx": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz",
|
||||||
@@ -1954,6 +2238,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nuxt/kit/node_modules/unplugin": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
|
"picomatch": "^4.0.4",
|
||||||
|
"webpack-virtual-modules": "^0.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@farmfe/core": "*",
|
||||||
|
"@rspack/core": "*",
|
||||||
|
"bun-types-no-globals": "*",
|
||||||
|
"esbuild": "*",
|
||||||
|
"rolldown": "*",
|
||||||
|
"rollup": "*",
|
||||||
|
"unloader": "*",
|
||||||
|
"vite": "*",
|
||||||
|
"webpack": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@farmfe/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@rspack/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bun-types-no-globals": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rolldown": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"unloader": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nuxt/nitro-server": {
|
"node_modules/@nuxt/nitro-server": {
|
||||||
"version": "3.21.11",
|
"version": "3.21.11",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/nitro-server/-/nitro-server-3.21.11.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/nitro-server/-/nitro-server-3.21.11.tgz",
|
||||||
@@ -2323,9 +2663,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2342,9 +2679,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2361,9 +2695,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2380,9 +2711,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2399,9 +2727,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2418,9 +2743,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2437,9 +2759,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2456,9 +2775,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2532,6 +2848,23 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@oxc-parser/binding-android-arm-eabi": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-pHASv9g5pASxb7akHERZNSkrEqPhFaUix98o7d9hbTpolnnFWl7UiRrcMhCsV1+iVO4/cJwKsbKRJTFNs2tdBQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@oxc-parser/binding-android-arm64": {
|
"node_modules/@oxc-parser/binding-android-arm64": {
|
||||||
"version": "0.93.0",
|
"version": "0.93.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.93.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.93.0.tgz",
|
||||||
@@ -2635,9 +2968,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2654,14 +2984,28 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
"license": "MIT",
|
||||||
"musl"
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-9LkaYvfiF8hMOw900csAvkf1oxE8XlmMeGowu5BcastSSwV8mKvKRMNU7HsV+ycyj1dQD8pX5qgOw8ja6SJacg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@@ -2673,14 +3017,28 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
"license": "MIT",
|
||||||
"glibc"
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxc-parser/binding-linux-riscv64-musl": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-uPqZexvKJmEgq4mAu36qe2xTfXZE7oyik1R7KtZ5tl8qKlq1U1fIqTFRUEBZqRGvforoTrGIpatRzcoPKO66RA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@@ -2692,9 +3050,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2711,9 +3066,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2730,9 +3082,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2742,6 +3091,23 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@oxc-parser/binding-openharmony-arm64": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-1UGbaQWEXUCLqAmaR5kwRDjx/R4S5LQKZkM9CHmaHkuKhriOF32aRLfS0jCRNE2yGQJLMEA1z9UucbBVqjXnDw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@oxc-parser/binding-wasm32-wasi": {
|
"node_modules/@oxc-parser/binding-wasm32-wasi": {
|
||||||
"version": "0.93.0",
|
"version": "0.93.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.93.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.93.0.tgz",
|
||||||
@@ -2774,6 +3140,23 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@oxc-parser/binding-win32-ia32-msvc": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-i3p4x+mvwtjcE1J5HM6V7ggsbXiznExN/4MkNyOy3dfXrVV3bnkSfmZxvo6/84qCVX4ShkpNE1SKt9biIF31GQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@oxc-parser/binding-win32-x64-msvc": {
|
"node_modules/@oxc-parser/binding-win32-x64-msvc": {
|
||||||
"version": "0.93.0",
|
"version": "0.93.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.93.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.93.0.tgz",
|
||||||
@@ -2918,9 +3301,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2937,9 +3317,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2956,9 +3333,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2975,9 +3349,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2994,9 +3365,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3013,9 +3381,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3032,9 +3397,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3051,9 +3413,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3513,9 +3872,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3529,9 +3885,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3545,9 +3898,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3561,9 +3911,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3577,9 +3924,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3593,9 +3937,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3609,9 +3950,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3625,9 +3963,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3641,9 +3976,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3657,9 +3989,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3673,9 +4002,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3689,9 +4015,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3705,9 +4028,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8861,9 +9181,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8880,9 +9197,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8899,9 +9213,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8918,9 +9229,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8937,9 +9245,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8956,9 +9261,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -9016,6 +9318,17 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nuxt/node_modules/@oxc-project/types": {
|
||||||
|
"version": "0.148.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz",
|
||||||
|
"integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/oxc-project"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nuxt/node_modules/@oxc-transform/binding-android-arm64": {
|
"node_modules/nuxt/node_modules/@oxc-transform/binding-android-arm64": {
|
||||||
"version": "0.143.0",
|
"version": "0.143.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm64/-/binding-android-arm64-0.143.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm64/-/binding-android-arm64-0.143.0.tgz",
|
||||||
@@ -9119,9 +9432,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -9138,9 +9448,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -9157,9 +9464,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -9176,9 +9480,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -9195,9 +9496,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -9214,9 +9512,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -9408,9 +9703,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -9427,9 +9719,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:all": "node scripts/dev-all.mjs",
|
"dev:all": "node scripts/dev-all.mjs",
|
||||||
"start": "node scripts/dev-all.mjs",
|
"start": "node scripts/dev-all.mjs",
|
||||||
"test:launcher": "node --test scripts/dev-all.test.mjs"
|
"test:launcher": "node --test scripts/dev-all.test.mjs",
|
||||||
|
"test:install": "node scripts/test-install.mjs"
|
||||||
},
|
},
|
||||||
"engines": { "node": ">=22.12.0" }
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,22 +10,42 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|||||||
|
|
||||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
||||||
export function parseArgs(args) {
|
export function readSettings(path = join(root, 'webka.config.json')) {
|
||||||
let host = '127.0.0.1', port = '3000';
|
if (!existsSync(path)) return {};
|
||||||
|
let settings;
|
||||||
|
try { settings = JSON.parse(readFileSync(path, 'utf8')); }
|
||||||
|
catch { throw new Error('webka.config.json must contain valid JSON.'); }
|
||||||
|
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) throw new Error('webka.config.json must contain an object.');
|
||||||
|
for (const key of Object.keys(settings)) if (!['homepage', 'host', 'port'].includes(key)) throw new Error(`Unknown webka.config.json setting: ${key}`);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
export function normalizeHomepage(value) {
|
||||||
|
if (typeof value !== 'string' || !value.trim()) throw new Error('Homepage must be an HTTPS address, for example https://camera.example.com.');
|
||||||
|
let parsed;
|
||||||
|
try { parsed = new URL(value.includes('://') ? value.trim() : `https://${value.trim()}`); }
|
||||||
|
catch { throw new Error('Homepage must be a valid HTTPS hostname or IP address.'); }
|
||||||
|
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.pathname !== '/' || parsed.search || parsed.hash || ['0.0.0.0', '[::]'].includes(parsed.hostname)) throw new Error('Homepage must be an HTTPS address without a path, credentials, or query (use the address served by your proxy).');
|
||||||
|
return parsed.origin;
|
||||||
|
}
|
||||||
|
export function parseArgs(args, settings = {}) {
|
||||||
|
let host = settings.host, port = String(settings.port ?? '3000'), homepage = settings.homepage;
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
if (arg === '--help' || arg === '-h') return { help: true };
|
if (arg === '--help' || arg === '-h') return { help: true };
|
||||||
if (arg === '--host' || arg === '--port') {
|
if (arg === '--host' || arg === '--port' || arg === '--homepage') {
|
||||||
if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error(`${arg} needs a value.`);
|
if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error(`${arg} needs a value.`);
|
||||||
if (arg === '--host') host = args[++i]; else port = args[++i];
|
if (arg === '--host') host = args[++i]; else if (arg === '--port') port = args[++i]; else homepage = args[++i];
|
||||||
} else if (arg.startsWith('--host=')) host = arg.slice(7);
|
} else if (arg.startsWith('--host=')) host = arg.slice(7);
|
||||||
else if (arg.startsWith('--port=')) port = arg.slice(7);
|
else if (arg.startsWith('--port=')) port = arg.slice(7);
|
||||||
else throw new Error(`Unknown argument: ${arg}. Use --host and --port (two ordinary hyphens).`);
|
else if (arg.startsWith('--homepage=')) homepage = arg.slice(11);
|
||||||
|
else throw new Error(`Unknown argument: ${arg}. Use --host, --port, or --homepage (two ordinary hyphens).`);
|
||||||
}
|
}
|
||||||
|
if (homepage !== undefined) homepage = normalizeHomepage(homepage);
|
||||||
|
host ??= homepage ? '0.0.0.0' : '127.0.0.1';
|
||||||
if (host === 'localhost') host = '127.0.0.1';
|
if (host === 'localhost') host = '127.0.0.1';
|
||||||
if (!isIP(host)) throw new Error('--host must be an IPv4/IPv6 address or localhost.');
|
if (typeof host !== 'string' || !isIP(host)) throw new Error('--host must be an IPv4/IPv6 address or localhost.');
|
||||||
if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) throw new Error('--port must be between 1 and 65535.');
|
if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) throw new Error('--port must be between 1 and 65535.');
|
||||||
return { host, port: Number(port) };
|
return { host, port: Number(port), ...(homepage ? { homepage } : {}) };
|
||||||
}
|
}
|
||||||
const hostname = host => isIP(host) === 6 ? `[${host}]` : host;
|
const hostname = host => isIP(host) === 6 ? `[${host}]` : host;
|
||||||
const url = (host, port) => `http://${hostname(host)}:${port}`;
|
const url = (host, port) => `http://${hostname(host)}:${port}`;
|
||||||
@@ -33,20 +53,30 @@ function privateIPv4(ip) {
|
|||||||
const parts = ip.split('.').map(Number);
|
const parts = ip.split('.').map(Number);
|
||||||
return isIP(ip) === 4 && (parts[0] === 10 || (parts[0] === 192 && parts[1] === 168) || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31));
|
return isIP(ip) === 4 && (parts[0] === 10 || (parts[0] === 192 && parts[1] === 168) || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31));
|
||||||
}
|
}
|
||||||
export function addresses({ host, port }, interfaces = networkInterfaces(), env = process.env) {
|
export function addresses({ host, port, homepage }, interfaces = networkInterfaces(), env = process.env) {
|
||||||
const wildcard = host === '0.0.0.0' || host === '::';
|
const wildcard = host === '0.0.0.0' || host === '::';
|
||||||
const local = url(wildcard ? (host === '::' ? '::1' : '127.0.0.1') : host, port);
|
const local = url(wildcard ? (host === '::' ? '::1' : '127.0.0.1') : host, port);
|
||||||
const loopback = host === '127.0.0.1' || host === '::1' || wildcard;
|
const loopback = host === '127.0.0.1' || host === '::1' || wildcard;
|
||||||
const lan = wildcard ? [...new Set(Object.values(interfaces).flat().filter(item => item && !item.internal && item.family === 'IPv4' && privateIPv4(item.address)).map(item => url(item.address, port)))] : [];
|
const lan = wildcard ? [...new Set(Object.values(interfaces).flat().filter(item => item && !item.internal && item.family === 'IPv4' && privateIPv4(item.address)).map(item => url(item.address, port)))] : [];
|
||||||
const origin = env.WEBKA_ORIGIN || lan[0] || (loopback ? `http://localhost:${port}` : local);
|
const origin = homepage || env.WEBKA_ORIGIN || lan[0] || (loopback ? `http://localhost:${port}` : local);
|
||||||
const automatic = [origin, local, ...(loopback ? [`http://localhost:${port}`] : []), ...lan];
|
const automatic = [origin, local, ...(loopback ? [`http://localhost:${port}`] : []), ...lan];
|
||||||
// Explicit HTTPS/reverse-proxy setups retain their exact configured origin.
|
// Explicit HTTPS/reverse-proxy setups retain their exact configured origin.
|
||||||
const allowed = env.WEBKA_ALLOWED_ORIGINS !== undefined ? env.WEBKA_ALLOWED_ORIGINS.split(',').filter(Boolean) : env.WEBKA_ORIGIN ? [origin] : automatic;
|
const allowed = homepage ? [origin] : env.WEBKA_ALLOWED_ORIGINS !== undefined ? env.WEBKA_ALLOWED_ORIGINS.split(',').filter(Boolean) : env.WEBKA_ORIGIN ? [origin] : automatic;
|
||||||
for (const value of [origin, ...allowed]) {
|
for (const value of [origin, ...allowed]) {
|
||||||
const parsed = new URL(value);
|
const parsed = new URL(value);
|
||||||
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin !== value || parsed.username || parsed.password) throw new Error(`Use an exact http(s) origin without a path: ${value}`);
|
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin !== value || parsed.username || parsed.password) throw new Error(`Use an exact http(s) origin without a path: ${value}`);
|
||||||
}
|
}
|
||||||
return { local, lan, origin, allowed: [...new Set(allowed)] };
|
return { local, lan, origin, allowed: [...new Set(allowed)], proxy: !!homepage };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connectionEnv(config, urls, env = process.env) {
|
||||||
|
return {
|
||||||
|
WEBKA_BIND: `${hostname(config.host)}:${config.port}`,
|
||||||
|
WEBKA_ORIGIN: urls.origin,
|
||||||
|
WEBKA_ALLOWED_ORIGINS: urls.allowed.join(','),
|
||||||
|
WEBKA_PUBLIC_PORT: String(new URL(urls.origin).port || (urls.origin.startsWith('https:') ? 443 : 80)),
|
||||||
|
WEBKA_MDNS: urls.proxy ? '0' : (env.WEBKA_MDNS || '0'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fingerprint(paths) {
|
function fingerprint(paths) {
|
||||||
@@ -72,9 +102,9 @@ function assertPort(host, port) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function main(args = process.argv.slice(2)) {
|
export async function main(args = process.argv.slice(2)) {
|
||||||
const config = parseArgs(args);
|
const config = parseArgs(args, args.includes('--help') || args.includes('-h') ? {} : readSettings());
|
||||||
if (config.help) {
|
if (config.help) {
|
||||||
console.log('Usage: npm run dev:all -- [--host 127.0.0.1] [--port 3000]\nBuilds and runs the frontend + backend together. Ctrl+C stops the app.');
|
console.log('Usage: npm run dev:all -- [--host 127.0.0.1] [--port 3000] [--homepage https://camera.example.com]\nA homepage enables external HTTPS proxy mode, defaults --host to 0.0.0.0, and disables mDNS.\nOptional webka.config.json saves homepage, host, and port. CLI options override saved settings.\nBuilds and runs the frontend + backend together. Ctrl+C stops the app.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [major, minor] = process.versions.node.split('.').map(Number);
|
const [major, minor] = process.versions.node.split('.').map(Number);
|
||||||
@@ -137,7 +167,7 @@ export async function main(args = process.argv.slice(2)) {
|
|||||||
if (metadata.status !== 0) throw new Error('Could not locate the Rust executable.');
|
if (metadata.status !== 0) throw new Error('Could not locate the Rust executable.');
|
||||||
const executable = join(JSON.parse(metadata.stdout).target_directory, 'debug', process.platform === 'win32' ? 'webka.exe' : 'webka');
|
const executable = join(JSON.parse(metadata.stdout).target_directory, 'debug', process.platform === 'win32' ? 'webka.exe' : 'webka');
|
||||||
const data = resolve(root, process.env.WEBKA_DATA || 'data');
|
const data = resolve(root, process.env.WEBKA_DATA || 'data');
|
||||||
const { proc, completion } = run(executable, [], { ...process.env, WEBKA_BIND: `${hostname(config.host)}:${config.port}`, WEBKA_ORIGIN: urls.origin, WEBKA_ALLOWED_ORIGINS: urls.allowed.join(','), WEBKA_PUBLIC_PORT: String(new URL(urls.origin).port || (urls.origin.startsWith('https:') ? 443 : 80)), WEBKA_ASSETS: join(root, 'frontend/.output/public'), WEBKA_DATA: data });
|
const { proc, completion } = run(executable, [], { ...process.env, ...connectionEnv(config, urls), WEBKA_ASSETS: join(root, 'frontend/.output/public'), WEBKA_DATA: data });
|
||||||
// Observe exit failures immediately while checking readiness.
|
// Observe exit failures immediately while checking readiness.
|
||||||
let failure;
|
let failure;
|
||||||
completion.catch(e => { failure = e; });
|
completion.catch(e => { failure = e; });
|
||||||
@@ -153,11 +183,15 @@ export async function main(args = process.argv.slice(2)) {
|
|||||||
}
|
}
|
||||||
if (!ready && !stopping) { stop(); await completion; throw new Error('The server did not become ready. Check the error above.'); }
|
if (!ready && !stopping) { stop(); await completion; throw new Error('The server did not become ready. Check the error above.'); }
|
||||||
if (stopping) { await completion; return; }
|
if (stopping) { await completion; return; }
|
||||||
|
if (urls.proxy) {
|
||||||
|
console.log(`\nWebka is ready!\n Open in browser: ${urls.origin}\n Proxy upstream: HTTP to this Webka computer's LAN IP, port ${config.port}\n HTTPS: handled by your existing proxy\n mDNS: disabled`);
|
||||||
|
} else {
|
||||||
console.log(`\nWebka is ready!\n This computer: ${urls.local}`);
|
console.log(`\nWebka is ready!\n This computer: ${urls.local}`);
|
||||||
for (const address of urls.lan) console.log(` LAN: ${address}`);
|
for (const address of urls.lan) console.log(` LAN: ${address}`);
|
||||||
if (process.env.WEBKA_ORIGIN) console.log(` Public URL: ${urls.origin}`);
|
if (process.env.WEBKA_ORIGIN) console.log(` Public URL: ${urls.origin}`);
|
||||||
|
if (urls.lan.length) console.log('Use the localhost/127.0.0.1 URL to capture on this computer. Capturing on a phone requires trusted HTTPS (see README).');
|
||||||
|
}
|
||||||
console.log(` Setup token: ${join(data, 'setup-token')}\n Stop: Ctrl+C\n`);
|
console.log(` Setup token: ${join(data, 'setup-token')}\n Stop: Ctrl+C\n`);
|
||||||
if (urls.lan.length) console.log('Use the localhost/127.0.0.1 URL to capture on this computer. Capturing on a phone requires trusted HTTPS (see README).\n');
|
|
||||||
await completion;
|
await completion;
|
||||||
} finally {
|
} finally {
|
||||||
if (child && child.exitCode === null) stop();
|
if (child && child.exitCode === null) stop();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { parseArgs, addresses } from './dev-all.mjs';
|
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { parseArgs, addresses, connectionEnv, readSettings } from './dev-all.mjs';
|
||||||
|
|
||||||
test('default and named host/port options', () => {
|
test('default and named host/port options', () => {
|
||||||
assert.deepEqual(parseArgs([]), { host: '127.0.0.1', port: 3000 });
|
assert.deepEqual(parseArgs([]), { host: '127.0.0.1', port: 3000 });
|
||||||
@@ -26,3 +29,38 @@ test('explicit HTTPS origin does not silently authorize HTTP aliases', () => {
|
|||||||
assert.throws(() => addresses(parseArgs([]), {}, { WEBKA_ORIGIN: 'https://webka.local/login' }));
|
assert.throws(() => addresses(parseArgs([]), {}, { WEBKA_ORIGIN: 'https://webka.local/login' }));
|
||||||
assert.throws(() => addresses(parseArgs([]), {}, { WEBKA_ALLOWED_ORIGINS: '*' }));
|
assert.throws(() => addresses(parseArgs([]), {}, { WEBKA_ALLOWED_ORIGINS: '*' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('homepage mode accepts proxy domain/IP, normalizes HTTPS and exposes the upstream', () => {
|
||||||
|
assert.deepEqual(parseArgs(['--homepage', 'camera.example.com', '--port', '3002']), { host: '0.0.0.0', port: 3002, homepage: 'https://camera.example.com' });
|
||||||
|
assert.deepEqual(parseArgs(['--homepage=https://192.168.1.10:8443/', '--host=127.0.0.1']), { host: '127.0.0.1', port: 3000, homepage: 'https://192.168.1.10:8443' });
|
||||||
|
for (const address of ['', 'http://camera.example.com', 'https://camera.example.com/watch', 'https://a:b@camera.example.com', 'https://camera.example.com?x=1', 'https://camera.example.com#x', 'https://0.0.0.0', 'https://[::]']) assert.throws(() => parseArgs(['--homepage', address]));
|
||||||
|
});
|
||||||
|
test('saved settings apply, with command-line overrides', () => {
|
||||||
|
const settings = { homepage: 'https://camera.example.com', port: 3002 };
|
||||||
|
assert.deepEqual(parseArgs([], settings), { host: '0.0.0.0', port: 3002, homepage: settings.homepage });
|
||||||
|
assert.deepEqual(parseArgs(['--homepage', 'https://other.example.com', '--port', '3003', '--host', '127.0.0.1'], settings), { host: '127.0.0.1', port: 3003, homepage: 'https://other.example.com' });
|
||||||
|
assert.throws(() => parseArgs([], { port: 'invalid' }));
|
||||||
|
});
|
||||||
|
test('configuration file loads a homepage and rejects malformed or misspelled settings', () => {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), 'webka-settings-'));
|
||||||
|
const path = join(directory, 'webka.config.json');
|
||||||
|
try {
|
||||||
|
assert.deepEqual(readSettings(path), {});
|
||||||
|
writeFileSync(path, JSON.stringify({ homepage: 'camera.example.com', port: 3002 }));
|
||||||
|
assert.deepEqual(parseArgs([], readSettings(path)), { host: '0.0.0.0', port: 3002, homepage: 'https://camera.example.com' });
|
||||||
|
for (const invalid of ['{', 'null', '[]', '{"homePage":"camera.example.com"}']) {
|
||||||
|
writeFileSync(path, invalid);
|
||||||
|
assert.throws(() => readSettings(path), /webka.config.json/);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
rmSync(directory, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('homepage wins over old HTTPS/mDNS environment settings without allowing HTTP aliases', () => {
|
||||||
|
const env = { WEBKA_MDNS: '1', WEBKA_ORIGIN: 'https://old.local', WEBKA_ALLOWED_ORIGINS: 'http://unsafe.example.com' };
|
||||||
|
const config = parseArgs(['--homepage', 'https://camera.example.com:8443', '--port', '3002']);
|
||||||
|
const urls = addresses(config, networks, env);
|
||||||
|
assert.equal(urls.origin, config.homepage);
|
||||||
|
assert.deepEqual(urls.allowed, [config.homepage]);
|
||||||
|
assert.deepEqual(connectionEnv(config, urls, env), { WEBKA_BIND: '0.0.0.0:3002', WEBKA_ORIGIN: config.homepage, WEBKA_ALLOWED_ORIGINS: config.homepage, WEBKA_MDNS: '0', WEBKA_PUBLIC_PORT: '8443' });
|
||||||
|
});
|
||||||
|
|||||||
21
scripts/test-install.mjs
Normal file
21
scripts/test-install.mjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Validate npm's resolution from only the committed manifests, with no node_modules.
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { copyFileSync, mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
if (!process.env.npm_execpath) throw new Error('Run npm run test:install.');
|
||||||
|
const fixture = mkdtempSync(join(tmpdir(), 'webka-install-'));
|
||||||
|
try {
|
||||||
|
for (const file of ['package.json', 'package-lock.json']) {
|
||||||
|
copyFileSync(fileURLToPath(new URL(`../frontend/${file}`, import.meta.url)), join(fixture, file));
|
||||||
|
}
|
||||||
|
const result = spawnSync(process.execPath, [process.env.npm_execpath, 'ci', '--dry-run', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: fixture, encoding: 'utf8', timeout: 180000 });
|
||||||
|
if (result.error || result.status !== 0) {
|
||||||
|
console.error(result.stderr || result.error?.message || 'Clean lockfile validation failed.');
|
||||||
|
process.exitCode = 1;
|
||||||
|
} else console.log('PASS: npm ci accepts the frontend lockfile without an existing node_modules directory.');
|
||||||
|
} finally {
|
||||||
|
rmSync(fixture, { recursive: true, force: true });
|
||||||
|
}
|
||||||
164
scripts/test-proxy.py
Normal file
164
scripts/test-proxy.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""POSIX integration: HTTPS reverse proxy -> Webka HTTP upstream, no mDNS."""
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import http.client
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
def free_port():
|
||||||
|
with socket.socket() as sock:
|
||||||
|
sock.bind(('127.0.0.1', 0))
|
||||||
|
return sock.getsockname()[1]
|
||||||
|
|
||||||
|
upstream_port = free_port()
|
||||||
|
proxy_port = free_port()
|
||||||
|
homepage = f'https://127.0.0.1:{proxy_port}'
|
||||||
|
|
||||||
|
class Proxy(BaseHTTPRequestHandler):
|
||||||
|
protocol_version = 'HTTP/1.1'
|
||||||
|
|
||||||
|
def log_message(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def forward(self):
|
||||||
|
body = self.rfile.read(int(self.headers.get('Content-Length', 0)))
|
||||||
|
headers = dict(self.headers)
|
||||||
|
headers['X-Forwarded-Proto'] = 'https'
|
||||||
|
headers['X-Forwarded-For'] = '192.168.1.99'
|
||||||
|
connection = http.client.HTTPConnection('127.0.0.1', upstream_port, timeout=5)
|
||||||
|
try:
|
||||||
|
connection.request(self.command, self.path, body=body, headers=headers)
|
||||||
|
response = connection.getresponse()
|
||||||
|
self.send_response(response.status)
|
||||||
|
for name, value in response.getheaders():
|
||||||
|
if name.lower() not in ['transfer-encoding', 'content-length', 'connection']:
|
||||||
|
self.send_header(name, value)
|
||||||
|
if response.status == 101:
|
||||||
|
self.send_header('Connection', 'Upgrade')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.flush()
|
||||||
|
# Establish the WebSocket upgrade across TLS and forward its first server frame.
|
||||||
|
# Explicit HTTPConnection upgrade retains the underlying socket.
|
||||||
|
head = response.fp.read(2)
|
||||||
|
self.wfile.write(head)
|
||||||
|
size = head[1] & 127
|
||||||
|
if size == 126:
|
||||||
|
extended = response.fp.read(2)
|
||||||
|
self.wfile.write(extended)
|
||||||
|
size = int.from_bytes(extended, 'big')
|
||||||
|
self.wfile.write(response.fp.read(size))
|
||||||
|
self.wfile.flush()
|
||||||
|
self.close_connection = True
|
||||||
|
else:
|
||||||
|
data = response.read()
|
||||||
|
self.send_header('Content-Length', str(len(data)))
|
||||||
|
self.send_header('Connection', 'close')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(data)
|
||||||
|
self.close_connection = True
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
do_GET = forward
|
||||||
|
do_POST = forward
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix='webka-proxy-test-') as temporary:
|
||||||
|
directory = Path(temporary)
|
||||||
|
cert, key = directory / 'cert.pem', directory / 'key.pem'
|
||||||
|
subprocess.run(['openssl', 'req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-keyout', str(key), '-out', str(cert), '-days', '1', '-subj', '/CN=127.0.0.1', '-addext', 'subjectAltName=IP:127.0.0.1'], check=True, capture_output=True)
|
||||||
|
context = ssl.create_default_context(cafile=str(cert))
|
||||||
|
data = directory / 'data'
|
||||||
|
# Deliberately stale values must be overridden by homepage mode.
|
||||||
|
env = dict(os.environ, WEBKA_DATA=str(data), WEBKA_MDNS='1', WEBKA_ORIGIN='https://old.local', WEBKA_ALLOWED_ORIGINS='http://old.example.com')
|
||||||
|
proxy = ThreadingHTTPServer(('127.0.0.1', proxy_port), Proxy)
|
||||||
|
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
server_context.load_cert_chain(cert, key)
|
||||||
|
proxy.socket = server_context.wrap_socket(proxy.socket, server_side=True)
|
||||||
|
thread = threading.Thread(target=proxy.serve_forever, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
logfile = directory / 'launch.log'
|
||||||
|
try:
|
||||||
|
with logfile.open('w') as output:
|
||||||
|
proc = subprocess.Popen(['npm', 'run', 'dev:all', '--', '--homepage', homepage, '--port', str(upstream_port)], cwd=root, env=env, stdout=output, stderr=subprocess.STDOUT, start_new_session=True)
|
||||||
|
try:
|
||||||
|
for _ in range(1200):
|
||||||
|
if proc.poll() is not None:
|
||||||
|
raise AssertionError(logfile.read_text())
|
||||||
|
if 'Webka is ready!' in logfile.read_text():
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
raise AssertionError('Startup timeout')
|
||||||
|
assert 'mDNS: disabled' in logfile.read_text()
|
||||||
|
assert f'Open in browser: {homepage}' in logfile.read_text()
|
||||||
|
def request(path, body=None, origin=homepage, cookie=None):
|
||||||
|
headers = {'Origin': origin, 'Content-Type': 'application/json'}
|
||||||
|
if cookie:
|
||||||
|
headers['Cookie'] = cookie
|
||||||
|
req = urllib.request.Request(homepage + path, data=json.dumps(body).encode() if body is not None else None, headers=headers)
|
||||||
|
return urllib.request.urlopen(req, context=context, timeout=5)
|
||||||
|
with request('/login') as response:
|
||||||
|
assert response.status == 200 and 'text/html' in response.headers['Content-Type']
|
||||||
|
credentials = {'username': 'owner', 'password': 'proxy-test-password123', 'setup_token': (data / 'setup-token').read_text()}
|
||||||
|
with request('/api/bootstrap', credentials) as response:
|
||||||
|
assert response.status == 201
|
||||||
|
with request('/api/login', credentials) as response:
|
||||||
|
assert 'Secure' in response.headers['Set-Cookie']
|
||||||
|
assert 'HttpOnly' in response.headers['Set-Cookie']
|
||||||
|
cookie = response.headers['Set-Cookie'].split(';')[0]
|
||||||
|
with request('/api/status', cookie=cookie) as response:
|
||||||
|
assert json.load(response)['local_url'] == homepage + '/watch'
|
||||||
|
with request('/api/session', cookie=cookie) as response:
|
||||||
|
assert json.load(response)['role'] == 'admin'
|
||||||
|
for wrong_origin in ['https://old.local', f'http://127.0.0.1:{upstream_port}', 'http://old.example.com']:
|
||||||
|
try:
|
||||||
|
request('/api/logout', {}, origin=wrong_origin, cookie=cookie)
|
||||||
|
raise AssertionError('Unexpected origin allowed')
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
assert error.code == 403
|
||||||
|
# A real WSS handshake is forwarded to the HTTP WebSocket upstream.
|
||||||
|
ws_key = base64.b64encode(os.urandom(16)).decode()
|
||||||
|
conn = http.client.HTTPSConnection('127.0.0.1', proxy_port, context=context, timeout=5)
|
||||||
|
conn.request('GET', '/api/signal?mode=host', headers={'Origin': homepage, 'Cookie': cookie, 'Connection': 'Upgrade', 'Upgrade': 'websocket', 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Key': ws_key})
|
||||||
|
response = conn.getresponse()
|
||||||
|
assert response.status == 101
|
||||||
|
expected = base64.b64encode(hashlib.sha1((ws_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').encode()).digest()).decode()
|
||||||
|
assert response.headers['Sec-WebSocket-Accept'] == expected
|
||||||
|
head = response.fp.read(2)
|
||||||
|
size = head[1] & 127
|
||||||
|
if size == 126:
|
||||||
|
size = int.from_bytes(response.fp.read(2), 'big')
|
||||||
|
assert json.loads(response.fp.read(size))['type'] == 'welcome'
|
||||||
|
conn.close()
|
||||||
|
print('PASS: HTTPS proxy login, Secure cookie, public share URL, origin restrictions, WSS upgrade, mDNS disabled.', flush=True)
|
||||||
|
finally:
|
||||||
|
if proc.poll() is None:
|
||||||
|
os.killpg(proc.pid, signal.SIGINT)
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=12)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.killpg(proc.pid, signal.SIGKILL)
|
||||||
|
proc.wait()
|
||||||
|
raise AssertionError('Launcher did not stop')
|
||||||
|
finally:
|
||||||
|
proxy.shutdown()
|
||||||
|
proxy.server_close()
|
||||||
|
thread.join(timeout=5)
|
||||||
|
with socket.socket() as sock:
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.bind(('0.0.0.0', upstream_port))
|
||||||
|
print('PASS: proxy and launcher stopped; HTTP upstream port released.', flush=True)
|
||||||
3
webka.config.example.json
Normal file
3
webka.config.example.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"homepage": "https://camera.example.com"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user