Fixes and improvements
This commit is contained in:
1 parent
1c9540c13c
commit
f24efd1401
33 files changed
+2618
-1093
No files matched your search
@@ -12,16 +12,19 @@ on:
|
||||
default: v0.0.1
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
name: Validate tag
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- id: version
|
||||
name: Validate three-part tag
|
||||
shell: bash
|
||||
@@ -40,6 +43,11 @@ jobs:
|
||||
name: Agent (${{ matrix.platform }})
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
attestations: write
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
CARGO_TARGET_DIR: target/agent
|
||||
NODEFLARE_AGENT_VERSION: ${{ needs.metadata.outputs.version }}
|
||||
@@ -72,8 +80,10 @@ jobs:
|
||||
extension: ""
|
||||
cross: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Install Linux cross compiler
|
||||
@@ -90,7 +100,10 @@ jobs:
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp "target/agent/${{ matrix.target }}/release/nodeflare-agent${{ matrix.extension }}" "release/${{ matrix.asset }}"
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
|
||||
with:
|
||||
subject-path: release/${{ matrix.asset }}
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.asset }}
|
||||
path: release/${{ matrix.asset }}
|
||||
@@ -100,9 +113,16 @@ jobs:
|
||||
name: Frontend
|
||||
needs: metadata
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
attestations: write
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: oven-sh/setup-bun@v2.2.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
- name: Install dependencies
|
||||
@@ -111,7 +131,10 @@ jobs:
|
||||
run: bun run --cwd frontend build:public
|
||||
- name: Package frontend
|
||||
run: tar -czf "nodeflare-frontend-${{ needs.metadata.outputs.version }}.tar.gz" -C frontend/dist .
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
|
||||
with:
|
||||
subject-path: nodeflare-frontend-${{ needs.metadata.outputs.version }}.tar.gz
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: frontend
|
||||
path: nodeflare-frontend-${{ needs.metadata.outputs.version }}.tar.gz
|
||||
@@ -122,10 +145,15 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
needs: [metadata, agent, frontend]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build changelog from commits
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -141,7 +169,7 @@ jobs:
|
||||
git log -1 --pretty=format:'- %s' "$GITHUB_SHA" > CHANGELOG.md
|
||||
fi
|
||||
printf '\n' >> CHANGELOG.md
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: release-assets
|
||||
merge-multiple: true
|
||||
@@ -154,7 +182,7 @@ jobs:
|
||||
cp agent/install.ps1 release-package/install-agent.ps1
|
||||
cp agent/install-macos.sh release-package/install-agent-macos.sh
|
||||
(cd release-package && sha256sum * > SHA256SUMS)
|
||||
- uses: softprops/action-gh-release@v3
|
||||
- uses: softprops/action-gh-release@fe965f7af51af5f2602596916f38a38df2e33de0 # v3.0.2
|
||||
with:
|
||||
body_path: CHANGELOG.md
|
||||
files: release-package/**
|
||||
Generated
+77
-57
@@ -4,9 +4,9 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.91"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -21,17 +21,17 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -64,33 +64,54 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
name = "cmov"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
"ctutils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -173,16 +194,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -191,9 +202,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
@@ -218,6 +229,15 @@ dependencies = [
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.2.0"
|
||||
@@ -329,9 +349,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
@@ -367,7 +387,9 @@ name = "nodeflare-worker"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"futures-util",
|
||||
"hmac",
|
||||
"pbkdf2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -389,6 +411,16 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -528,9 +560,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
@@ -576,12 +608,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
@@ -664,17 +690,11 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -685,9 +705,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.76"
|
||||
version = "0.4.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
|
||||
checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -695,9 +715,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -705,9 +725,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -718,9 +738,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -740,9 +760,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.103"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
||||
checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
||||
+5
-3
@@ -10,11 +10,13 @@ repository = "https://github.com/imengying/NodeFlare"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22"
|
||||
hmac = "0.12"
|
||||
base64 = "0.23"
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
hmac = "0.13"
|
||||
pbkdf2 = { version = "0.13", default-features = false, features = ["hmac"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
sha2 = "0.11"
|
||||
worker = { version = "0.8", features = ["d1"] }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
- 用户名密码登录、Cloudflare Turnstile、节点隐藏和公开备注
|
||||
- 节点管理、批量删除、拖拽排序、Agent 密钥轮换和在线配置
|
||||
- Telegram 通知、资源告警、离线/到期提醒和数据维护
|
||||
- 内置 Komari Glass 主题,支持展示项、背景图和主题选项配置
|
||||
- 内置 Komari Glass 主题,并提供远程主题商店
|
||||
- 远程主题仅支持 GitHub `tree` 地址,Worker 代理 `index.html` 与 `assets/`
|
||||
- Rust Agent 支持 Linux x86_64/ARM64、Windows x86_64 和 macOS ARM64,可按节点自动更新
|
||||
|
||||
## 部署到 Cloudflare
|
||||
@@ -56,6 +57,10 @@
|
||||
| `POST` | `/api/admin/servers/:id/token` | 轮换探针密钥 |
|
||||
| `DELETE` | `/api/admin/servers` | 批量删除节点 |
|
||||
| `GET/PATCH` | `/api/admin/settings` | 读写站点与展示设置 |
|
||||
| `GET/POST` | `/api/admin/themes` | 查询或添加主题 |
|
||||
| `POST` | `/api/admin/themes/:id/activate` | 启用主题 |
|
||||
| `POST` | `/api/admin/themes/:id/preview` | 创建短时主题预览链接 |
|
||||
| `DELETE` | `/api/admin/themes/:id` | 删除主题 |
|
||||
| `GET` | `/api/admin/theme-settings` | 读取当前前端提供的主题设置描述 |
|
||||
| `POST` | `/api/turnstile/verify` | 校验全站 Turnstile 令牌 |
|
||||
| `GET` | `/api/admin/database` | 查询 D1 节点、历史和数据库统计 |
|
||||
@@ -64,6 +69,10 @@
|
||||
| `DELETE` | `/api/admin/history` | 清理历史指标 |
|
||||
| `POST` | `/api/admin/notifications/test` | 发送一条通知测试消息 |
|
||||
|
||||
## 主题
|
||||
|
||||
GitHub `tree` 地址对应的目录需提供 `index.html` 和 `assets/`,并使用 NodeFlare 公开 API 读取数据。启用第三方主题前请确认来源可信。
|
||||
|
||||
## 鸣谢
|
||||
|
||||
感谢 [CF-Server-Monitor](https://github.com/huilang-me/CF-Server-Monitor) 项目提供的思路。
|
||||
|
||||
Generated
+180
-237
@@ -4,9 +4,9 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@@ -16,34 +16,29 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.0"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
|
||||
checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeflare-agent"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sysinfo",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -51,47 +46,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
name = "cookie_store"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"document-features",
|
||||
"idna",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
@@ -107,41 +127,25 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
@@ -152,16 +156,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -174,14 +168,34 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -287,6 +301,16 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -299,18 +323,18 @@ version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
@@ -324,20 +348,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
name = "nodeflare-agent"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sysinfo",
|
||||
"ureq",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -349,6 +368,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
@@ -374,61 +399,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
@@ -438,6 +414,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
@@ -456,12 +438,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -470,23 +446,10 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"getrandom",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -524,38 +487,6 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -601,9 +532,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
@@ -682,16 +613,33 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
name = "time"
|
||||
version = "0.3.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
"deranged",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -724,20 +672,33 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.12.1"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cookie_store",
|
||||
"log",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"webpki-roots 0.26.11",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -752,18 +713,18 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -776,15 +737,6 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.9"
|
||||
@@ -926,15 +878,6 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
|
||||
+3
-5
@@ -9,14 +9,12 @@ repository = "https://github.com/imengying/NodeFlare"
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
ureq = { version = "2.12", default-features = false, features = ["json", "tls"] }
|
||||
sha2 = "0.11"
|
||||
ureq = { version = "3.4", default-features = false, features = ["json", "rustls"] }
|
||||
url = "2.5"
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
|
||||
sysinfo = { version = "0.39.6", default-features = false, features = ["disk", "network", "system"] }
|
||||
ureq = { version = "2.12", default-features = false, features = ["json", "native-tls"] }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
+5
-1
@@ -38,7 +38,11 @@ install_agent() {
|
||||
collect_interval=$(arg --collect-interval "$@" || printf '5')
|
||||
worker_url=${worker_url%/}
|
||||
safe_value "$server_id" && safe_value "$token" && safe_value "$worker_url" || { echo "Invalid install argument" >&2; exit 1; }
|
||||
case "$worker_url" in http://?*|https://?*) ;; *) echo "Worker URL must use HTTP or HTTPS" >&2; exit 1 ;; esac
|
||||
case "$worker_url" in
|
||||
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
|
||||
*) echo "Worker URL must use HTTPS; HTTP is only allowed for loopback development" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$worker_url" in *@*) echo "Worker URL must not contain user information" >&2; exit 1 ;; esac
|
||||
case "$interval" in ''|*[!0-9]*) echo "Invalid interval" >&2; exit 1 ;; esac
|
||||
[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || { echo "Interval must be between 15 and 3600 seconds" >&2; exit 1; }
|
||||
case "$collect_interval" in ''|*[!0-9]*) echo "Invalid collect interval" >&2; exit 1 ;; esac
|
||||
|
||||
@@ -46,7 +46,11 @@ interval=$(arg --interval "$@" || printf '60')
|
||||
collect_interval=$(arg --collect-interval "$@" || printf '5')
|
||||
worker_url=${worker_url%/}
|
||||
safe_value "$server_id" && safe_value "$token" && safe_value "$worker_url" || { echo "Invalid argument" >&2; exit 1; }
|
||||
case "$worker_url" in http://?*|https://?*) ;; *) echo "Worker URL must use HTTP or HTTPS" >&2; exit 1 ;; esac
|
||||
case "$worker_url" in
|
||||
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
|
||||
*) echo "Worker URL must use HTTPS; HTTP is only allowed for loopback development" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$worker_url" in *@*) echo "Worker URL must not contain user information" >&2; exit 1 ;; esac
|
||||
case "$interval" in ''|*[!0-9]*) echo "Invalid interval" >&2; exit 1 ;; esac
|
||||
[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || { echo "Interval must be between 15 and 3600 seconds" >&2; exit 1; }
|
||||
case "$collect_interval" in ''|*[!0-9]*) echo "Invalid collect interval" >&2; exit 1 ;; esac
|
||||
|
||||
+8
-3
@@ -21,17 +21,22 @@ function Assert-Safe([string]$Name, [string]$Value) {
|
||||
|
||||
function Assert-WorkerUrl([string]$Value) {
|
||||
$Parsed = $null
|
||||
$SecureScheme = $false
|
||||
if ([Uri]::TryCreate($Value, [UriKind]::Absolute, [ref]$Parsed)) {
|
||||
$SecureScheme = $Parsed.Scheme -eq "https" -or (
|
||||
$Parsed.Scheme -eq "http" -and $Parsed.Host -in @("localhost", "127.0.0.1", "::1")
|
||||
)
|
||||
}
|
||||
if (
|
||||
$Value.Length -gt 2048 -or
|
||||
$Value -match "\s" -or
|
||||
$Value.Contains("'") -or
|
||||
-not [Uri]::TryCreate($Value, [UriKind]::Absolute, [ref]$Parsed) -or
|
||||
$Parsed.Scheme -notin @("http", "https") -or
|
||||
-not $SecureScheme -or
|
||||
-not [string]::IsNullOrEmpty($Parsed.UserInfo) -or
|
||||
-not [string]::IsNullOrEmpty($Parsed.Query) -or
|
||||
-not [string]::IsNullOrEmpty($Parsed.Fragment)
|
||||
) {
|
||||
throw "Url must be an absolute HTTP or HTTPS URL"
|
||||
throw "Url must use HTTPS; HTTP is only allowed for loopback development"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+216
-106
@@ -2,9 +2,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use std::net::UdpSocket;
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::net::{IpAddr, SocketAddr, TcpStream, ToSocketAddrs};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
#[cfg(unix)]
|
||||
@@ -50,10 +48,12 @@ struct RuntimeConfig {
|
||||
report_interval: u64,
|
||||
collect_interval: u64,
|
||||
network_interface: String,
|
||||
auto_update: bool,
|
||||
latency_tasks: Vec<LatencyTask>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LatencyTask {
|
||||
id: String,
|
||||
name: String,
|
||||
@@ -70,16 +70,13 @@ struct LatencyResult {
|
||||
packet_loss: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RemoteConfig {
|
||||
#[serde(default)]
|
||||
report_interval: u64,
|
||||
#[serde(default)]
|
||||
collect_interval: u64,
|
||||
#[serde(default)]
|
||||
network_interface: String,
|
||||
auto_update: i64,
|
||||
#[serde(default)]
|
||||
latency_tasks: Vec<LatencyTask>,
|
||||
}
|
||||
|
||||
@@ -96,8 +93,15 @@ struct GithubReleaseAsset {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ReportResponse {
|
||||
success: bool,
|
||||
config: RemoteConfig,
|
||||
}
|
||||
|
||||
struct SubmitResult {
|
||||
config: Option<RemoteConfig>,
|
||||
config_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -120,7 +124,7 @@ struct DiskMetric {
|
||||
utilization: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct GpuMetric {
|
||||
model: String,
|
||||
usage: f64,
|
||||
@@ -155,8 +159,6 @@ struct Report {
|
||||
kernel: String,
|
||||
arch: String,
|
||||
virtualization: String,
|
||||
ipv4: String,
|
||||
ipv6: String,
|
||||
gpu_usage: f64,
|
||||
gpu_model: String,
|
||||
agent_version: String,
|
||||
@@ -392,17 +394,6 @@ fn cpu_model() -> String {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn ip_address(family: &str) -> String {
|
||||
command("ip", &["-o", family, "addr", "show", "scope", "global"])
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(|line| line.split_whitespace().find(|value| value.contains('/')))
|
||||
.and_then(|value| value.split('/').next())
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn valid_probe_host(host: &str) -> bool {
|
||||
if host.is_empty() || host.len() > 50 || host.starts_with('.') || host.ends_with('.') {
|
||||
return false;
|
||||
@@ -413,24 +404,76 @@ fn valid_probe_host(host: &str) -> bool {
|
||||
.iter()
|
||||
.all(|label| !label.is_empty() && label.chars().all(|c| c.is_ascii_digit()));
|
||||
if ipv4_like {
|
||||
return labels.iter().all(|label| label.parse::<u8>().is_ok());
|
||||
return host
|
||||
.parse()
|
||||
.is_ok_and(|address| is_public_probe_ip(IpAddr::V4(address)));
|
||||
}
|
||||
let lower = host.to_ascii_lowercase();
|
||||
if labels.len() < 2
|
||||
|| ["local", "localhost", "internal", "lan", "localdomain"]
|
||||
.iter()
|
||||
.any(|suffix| lower == *suffix || lower.ends_with(&format!(".{suffix}")))
|
||||
|| lower == "home.arpa"
|
||||
|| lower.ends_with(".home.arpa")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
labels.iter().all(|label| {
|
||||
!label.is_empty()
|
||||
&& label.chars().all(|character| {
|
||||
character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
|
||||
})
|
||||
&& label
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_alphanumeric() || character == '-')
|
||||
&& label
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|character| character.is_ascii_alphanumeric() || character == '_')
|
||||
.is_some_and(|character| character.is_ascii_alphanumeric())
|
||||
&& label
|
||||
.chars()
|
||||
.last()
|
||||
.is_some_and(|character| character.is_ascii_alphanumeric() || character == '_')
|
||||
.is_some_and(|character| character.is_ascii_alphanumeric())
|
||||
})
|
||||
}
|
||||
|
||||
fn is_public_probe_ip(address: IpAddr) -> bool {
|
||||
match address {
|
||||
IpAddr::V4(address) => {
|
||||
let [a, b, c, _] = address.octets();
|
||||
!(a == 0
|
||||
|| a == 10
|
||||
|| a == 127
|
||||
|| (a == 100 && (64..=127).contains(&b))
|
||||
|| (a == 169 && b == 254)
|
||||
|| (a == 172 && (16..=31).contains(&b))
|
||||
|| (a == 192 && b == 0 && c == 0)
|
||||
|| (a == 192 && b == 0 && c == 2)
|
||||
|| (a == 192 && b == 168)
|
||||
|| (a == 198 && (b == 18 || b == 19))
|
||||
|| (a == 198 && b == 51 && c == 100)
|
||||
|| (a == 203 && b == 0 && c == 113)
|
||||
|| a >= 224)
|
||||
}
|
||||
IpAddr::V6(address) => {
|
||||
if let Some(mapped) = address.to_ipv4_mapped() {
|
||||
return is_public_probe_ip(IpAddr::V4(mapped));
|
||||
}
|
||||
address.segments()[0] & 0xe000 == 0x2000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_public_probe_address(host: &str, port: u16) -> Option<SocketAddr> {
|
||||
let addresses = (host, port)
|
||||
.to_socket_addrs()
|
||||
.ok()?
|
||||
.filter(|address| is_public_probe_ip(address.ip()))
|
||||
.collect::<Vec<_>>();
|
||||
addresses
|
||||
.iter()
|
||||
.find(|address| address.is_ipv4())
|
||||
.or_else(|| addresses.first())
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn parse_probe_target(value: &str) -> Option<(String, u16)> {
|
||||
let raw = value.trim();
|
||||
if raw.is_empty()
|
||||
@@ -466,23 +509,17 @@ fn tcp_latency_probe(target: &str) -> (f64, f64) {
|
||||
let Some((host, port)) = parse_probe_target(target) else {
|
||||
return (-1.0, 100.0);
|
||||
};
|
||||
let Ok(addresses) = (host.as_str(), port).to_socket_addrs() else {
|
||||
return (-1.0, 100.0);
|
||||
};
|
||||
let addresses = addresses.collect::<Vec<_>>();
|
||||
let Some(address) = addresses
|
||||
.iter()
|
||||
.find(|address| address.is_ipv4())
|
||||
.or_else(|| addresses.first())
|
||||
.copied()
|
||||
else {
|
||||
let Some(address) = resolve_public_probe_address(&host, port) else {
|
||||
return (-1.0, 100.0);
|
||||
};
|
||||
tcp_latency_probe_address(&address)
|
||||
}
|
||||
|
||||
fn tcp_latency_probe_address(address: &SocketAddr) -> (f64, f64) {
|
||||
let mut latencies = Vec::with_capacity(PROBE_ATTEMPTS);
|
||||
for _ in 0..PROBE_ATTEMPTS {
|
||||
let started = Instant::now();
|
||||
if TcpStream::connect_timeout(&address, PROBE_TIMEOUT).is_ok() {
|
||||
if TcpStream::connect_timeout(address, PROBE_TIMEOUT).is_ok() {
|
||||
latencies.push(started.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
}
|
||||
@@ -511,19 +548,26 @@ fn ping_latency(output: &str) -> Option<f64> {
|
||||
|
||||
fn icmp_latency_probe(target: &str) -> (f64, f64) {
|
||||
let host = target.trim();
|
||||
if host.contains(':') || !valid_probe_host(host) {
|
||||
if host.contains(':') {
|
||||
return (-1.0, 100.0);
|
||||
}
|
||||
let Some((host, _)) = parse_probe_target(host) else {
|
||||
return (-1.0, 100.0);
|
||||
};
|
||||
let Some(address) = resolve_public_probe_address(&host, 443) else {
|
||||
return (-1.0, 100.0);
|
||||
};
|
||||
let destination = address.ip().to_string();
|
||||
let mut latencies = Vec::with_capacity(PROBE_ATTEMPTS);
|
||||
for _ in 0..PROBE_ATTEMPTS {
|
||||
let started = Instant::now();
|
||||
let mut ping = Command::new("ping");
|
||||
#[cfg(target_os = "linux")]
|
||||
ping.args(["-n", "-c", "1", "-W", "1", host]);
|
||||
ping.args(["-n", "-c", "1", "-W", "1", destination.as_str()]);
|
||||
#[cfg(target_os = "macos")]
|
||||
ping.args(["-n", "-c", "1", "-W", "1000", host]);
|
||||
ping.args(["-n", "-c", "1", "-W", "1000", destination.as_str()]);
|
||||
#[cfg(target_os = "windows")]
|
||||
ping.args(["-n", "1", "-w", "1000", host]);
|
||||
ping.args(["-n", "1", "-w", "1000", destination.as_str()]);
|
||||
let output = ping.output();
|
||||
if let Ok(output) = output {
|
||||
if output.status.success() {
|
||||
@@ -611,17 +655,6 @@ fn gpu_info() -> Vec<GpuMetric> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
fn local_ip(bind: &str, destination: &str) -> String {
|
||||
UdpSocket::bind(bind)
|
||||
.and_then(|socket| {
|
||||
socket.connect(destination)?;
|
||||
socket.local_addr()
|
||||
})
|
||||
.map(|address| address.ip().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
fn u64_to_i64(value: u64) -> i64 {
|
||||
value.min(i64::MAX as u64) as i64
|
||||
@@ -720,8 +753,6 @@ fn collect(config: &RuntimeConfig, latency_results: Vec<LatencyResult>) -> Repor
|
||||
kernel: command("uname", &["-r"]),
|
||||
arch: env::consts::ARCH.to_string(),
|
||||
virtualization: command("systemd-detect-virt", &[]),
|
||||
ipv4: ip_address("-4"),
|
||||
ipv6: ip_address("-6"),
|
||||
gpu_usage,
|
||||
gpu_model,
|
||||
agent_version: VERSION.to_string(),
|
||||
@@ -840,8 +871,6 @@ fn collect(config: &RuntimeConfig, latency_results: Vec<LatencyResult>) -> Repor
|
||||
kernel: System::kernel_version().unwrap_or_default(),
|
||||
arch: env::consts::ARCH.to_string(),
|
||||
virtualization: String::new(),
|
||||
ipv4: local_ip("0.0.0.0:0", "1.1.1.1:80"),
|
||||
ipv6: local_ip("[::]:0", "[2606:4700:4700::1111]:80"),
|
||||
gpu_usage,
|
||||
gpu_model,
|
||||
agent_version: VERSION.to_string(),
|
||||
@@ -886,7 +915,9 @@ fn runtime_config() -> Result<RuntimeConfig> {
|
||||
return Err("SERVER_ID or AGENT_TOKEN is invalid".into());
|
||||
}
|
||||
if !valid_worker_url(&worker_url) {
|
||||
return Err("WORKER_URL must be an http(s) URL without whitespace".into());
|
||||
return Err(
|
||||
"WORKER_URL must use HTTPS; HTTP is only allowed for loopback development".into(),
|
||||
);
|
||||
}
|
||||
Ok(RuntimeConfig {
|
||||
server_id,
|
||||
@@ -895,6 +926,7 @@ fn runtime_config() -> Result<RuntimeConfig> {
|
||||
report_interval: interval,
|
||||
collect_interval,
|
||||
network_interface: env::var("NETWORK_INTERFACE").unwrap_or_default(),
|
||||
auto_update: true,
|
||||
latency_tasks: Vec::new(),
|
||||
})
|
||||
}
|
||||
@@ -903,17 +935,38 @@ fn submit(
|
||||
agent: &ureq::Agent,
|
||||
config: &RuntimeConfig,
|
||||
reports: &[Report],
|
||||
) -> Result<Option<RemoteConfig>> {
|
||||
config_hash: &str,
|
||||
) -> Result<SubmitResult> {
|
||||
let batch = ReportBatch {
|
||||
server_id: &config.server_id,
|
||||
samples: reports,
|
||||
};
|
||||
let response = agent
|
||||
let mut response = agent
|
||||
.post(&format!("{}/api/agent/report", config.worker_url))
|
||||
.set("Authorization", &format!("Bearer {}", config.token))
|
||||
.set("User-Agent", &format!("nodeflare-agent/{VERSION}"))
|
||||
.header("Authorization", format!("Bearer {}", config.token))
|
||||
.header("User-Agent", format!("nodeflare-agent/{VERSION}"))
|
||||
.header("X-Agent-Config-Schema", "1")
|
||||
.header("X-Agent-Config-Sha256", config_hash)
|
||||
.send_json(batch)?;
|
||||
Ok(response.into_json::<ReportResponse>()?.config)
|
||||
let next_hash = response
|
||||
.headers()
|
||||
.get("X-Agent-Config-Sha256")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or(config_hash)
|
||||
.to_string();
|
||||
let remote = if response.status().as_u16() == 204 {
|
||||
None
|
||||
} else {
|
||||
let payload = response.body_mut().read_json::<ReportResponse>()?;
|
||||
if !payload.success {
|
||||
return Err("Worker rejected the report".into());
|
||||
}
|
||||
Some(payload.config)
|
||||
};
|
||||
Ok(SubmitResult {
|
||||
config: remote,
|
||||
config_hash: next_hash,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_remote(config: &mut RuntimeConfig, remote: &RemoteConfig) -> bool {
|
||||
@@ -926,6 +979,7 @@ fn apply_remote(config: &mut RuntimeConfig, remote: &RemoteConfig) -> bool {
|
||||
config
|
||||
.network_interface
|
||||
.clone_from(&remote.network_interface);
|
||||
config.auto_update = remote.auto_update == 1;
|
||||
let tasks = sanitize_latency_tasks(&remote.latency_tasks);
|
||||
let changed = config.latency_tasks != tasks;
|
||||
config.latency_tasks = tasks;
|
||||
@@ -954,15 +1008,32 @@ fn sanitize_latency_tasks(tasks: &[LatencyTask]) -> Vec<LatencyTask> {
|
||||
|
||||
fn valid_worker_url(value: &str) -> bool {
|
||||
let value = value.trim_end_matches('/');
|
||||
let authority = value
|
||||
.strip_prefix("https://")
|
||||
.or_else(|| value.strip_prefix("http://"))
|
||||
.and_then(|remainder| remainder.split('/').next());
|
||||
value.len() <= 2048
|
||||
&& authority.is_some_and(|authority| !authority.is_empty() && !authority.contains('@'))
|
||||
&& !value
|
||||
.chars()
|
||||
.any(|character| character.is_whitespace() || matches!(character, '?' | '#'))
|
||||
if value.is_empty() || value.len() > 2048 || value.chars().any(char::is_whitespace) {
|
||||
return false;
|
||||
}
|
||||
let Ok(parsed) = url::Url::parse(value) else {
|
||||
return false;
|
||||
};
|
||||
if parsed.host().is_none()
|
||||
|| !parsed.username().is_empty()
|
||||
|| parsed.password().is_some()
|
||||
|| parsed.query().is_some()
|
||||
|| parsed.fragment().is_some()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if parsed.scheme() == "https" {
|
||||
return true;
|
||||
}
|
||||
if parsed.scheme() != "http" {
|
||||
return false;
|
||||
}
|
||||
match parsed.host() {
|
||||
Some(url::Host::Domain(host)) => host.eq_ignore_ascii_case("localhost"),
|
||||
Some(url::Host::Ipv4(address)) => address == std::net::Ipv4Addr::LOCALHOST,
|
||||
Some(url::Host::Ipv6(address)) => address.is_loopback(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn report_retry_delay(failures: u32, report_interval: u64) -> Duration {
|
||||
@@ -1037,7 +1108,11 @@ fn sha256_file(path: &Path) -> Result<String> {
|
||||
}
|
||||
digest.update(&buffer[..read]);
|
||||
}
|
||||
Ok(format!("{:x}", digest.finalize()))
|
||||
Ok(digest
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn parse_checksum(contents: &str, artifact: &str) -> Option<String> {
|
||||
@@ -1058,6 +1133,7 @@ fn release_checksum(agent: &ureq::Agent, url: &str, artifact: &str) -> Result<St
|
||||
agent
|
||||
.get(url)
|
||||
.call()?
|
||||
.into_body()
|
||||
.into_reader()
|
||||
.take(1024 * 1024)
|
||||
.read_to_string(&mut contents)?;
|
||||
@@ -1075,7 +1151,7 @@ fn download_agent(
|
||||
let Ok(response) = agent.get(url).call() else {
|
||||
return Ok(false);
|
||||
};
|
||||
let response = response.into_reader();
|
||||
let response = response.into_body().into_reader();
|
||||
let mut file = fs::File::create(destination)?;
|
||||
let copied = io::copy(&mut response.take(MAX_AGENT_BINARY_BYTES + 1), &mut file)?;
|
||||
file.flush()?;
|
||||
@@ -1105,12 +1181,12 @@ fn download_agent(
|
||||
}
|
||||
|
||||
fn update(agent: &ureq::Agent) -> Result<bool> {
|
||||
let release = agent
|
||||
let mut response = agent
|
||||
.get(LATEST_RELEASE_API)
|
||||
.set("Accept", "application/vnd.github+json")
|
||||
.set("User-Agent", &format!("nodeflare-agent/{VERSION}"))
|
||||
.call()?
|
||||
.into_json::<GithubRelease>()?;
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.header("User-Agent", format!("nodeflare-agent/{VERSION}"))
|
||||
.call()?;
|
||||
let release = response.body_mut().read_json::<GithubRelease>()?;
|
||||
let Some(remote_version) = version_triplet(&release.tag_name) else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -1199,9 +1275,10 @@ fn update(agent: &ureq::Agent) -> Result<bool> {
|
||||
|
||||
fn run(once: bool, print_only: bool) -> Result<()> {
|
||||
let mut config = runtime_config()?;
|
||||
let agent = ureq::AgentBuilder::new()
|
||||
.timeout(Duration::from_secs(20))
|
||||
.build();
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.timeout_global(Some(Duration::from_secs(20)))
|
||||
.build()
|
||||
.into();
|
||||
let mut next_report = Instant::now();
|
||||
let mut next_collect = Instant::now();
|
||||
let mut next_latency: HashMap<String, Instant> = HashMap::new();
|
||||
@@ -1209,6 +1286,7 @@ fn run(once: bool, print_only: bool) -> Result<()> {
|
||||
let mut pending_samples = Vec::new();
|
||||
let mut report_failures = 0_u32;
|
||||
let mut next_update_check = Instant::now();
|
||||
let mut config_hash = String::new();
|
||||
loop {
|
||||
let current = Instant::now();
|
||||
let due = config
|
||||
@@ -1252,27 +1330,28 @@ fn run(once: bool, print_only: bool) -> Result<()> {
|
||||
}
|
||||
|
||||
if Instant::now() >= next_report && !pending_samples.is_empty() {
|
||||
match submit(&agent, &config, &pending_samples) {
|
||||
Ok(remote) => {
|
||||
match submit(&agent, &config, &pending_samples, &config_hash) {
|
||||
Ok(result) => {
|
||||
pending_samples.clear();
|
||||
report_failures = 0;
|
||||
if let Some(remote) = remote {
|
||||
if !once && remote.auto_update == 1 && Instant::now() >= next_update_check {
|
||||
match update(&agent) {
|
||||
Ok(true) => return Ok(()),
|
||||
Ok(false) => {
|
||||
next_update_check = Instant::now() + UPDATE_CHECK_INTERVAL;
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("agent update failed: {error}");
|
||||
next_update_check = Instant::now() + UPDATE_RETRY_INTERVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
config_hash = result.config_hash;
|
||||
if let Some(remote) = result.config {
|
||||
if apply_remote(&mut config, &remote) {
|
||||
next_latency.clear();
|
||||
}
|
||||
}
|
||||
if !once && config.auto_update && Instant::now() >= next_update_check {
|
||||
match update(&agent) {
|
||||
Ok(true) => return Ok(()),
|
||||
Ok(false) => {
|
||||
next_update_check = Instant::now() + UPDATE_CHECK_INTERVAL;
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("agent update failed: {error}");
|
||||
next_update_check = Instant::now() + UPDATE_RETRY_INTERVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("report failed: {error}");
|
||||
@@ -1329,10 +1408,10 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{
|
||||
agent_artifact_name, executable_format_valid, median, normalized_version, parse_checksum,
|
||||
parse_probe_target, ping_latency, prune_report_samples, report_retry_delay,
|
||||
sanitize_latency_tasks, selected_interface, tcp_latency_probe, valid_worker_url,
|
||||
version_triplet, LatencyTask, Report, MAX_LATENCY_TASKS, PROBE_ATTEMPTS,
|
||||
agent_artifact_name, executable_format_valid, is_public_probe_ip, median,
|
||||
normalized_version, parse_checksum, parse_probe_target, ping_latency, prune_report_samples,
|
||||
report_retry_delay, sanitize_latency_tasks, selected_interface, tcp_latency_probe_address,
|
||||
valid_worker_url, version_triplet, LatencyTask, Report, MAX_LATENCY_TASKS, PROBE_ATTEMPTS,
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
@@ -1380,6 +1459,10 @@ mod tests {
|
||||
fn validates_worker_urls() {
|
||||
assert!(valid_worker_url("https://monitor.example.com/"));
|
||||
assert!(valid_worker_url("http://127.0.0.1:8787"));
|
||||
assert!(valid_worker_url("http://localhost:8787"));
|
||||
assert!(valid_worker_url("http://[::1]:8787"));
|
||||
assert!(!valid_worker_url("http://monitor.example.com"));
|
||||
assert!(!valid_worker_url("http://127.0.0.2:8787"));
|
||||
assert!(!valid_worker_url("monitor.example.com"));
|
||||
assert!(!valid_worker_url("https://"));
|
||||
assert!(!valid_worker_url("https://user@example.com"));
|
||||
@@ -1475,8 +1558,8 @@ mod tests {
|
||||
Some(("example.com".to_string(), 443))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_probe_target("127.0.0.1:8080"),
|
||||
Some(("127.0.0.1".to_string(), 8080))
|
||||
parse_probe_target("1.1.1.1:8080"),
|
||||
Some(("1.1.1.1".to_string(), 8080))
|
||||
);
|
||||
for target in [
|
||||
"",
|
||||
@@ -1484,6 +1567,11 @@ mod tests {
|
||||
"example.com:0",
|
||||
"example.com:65536",
|
||||
"999.1.1.1",
|
||||
"127.0.0.1:8080",
|
||||
"169.254.169.254",
|
||||
"192.168.1.1",
|
||||
"router.local",
|
||||
"localhost",
|
||||
"[::1]:443",
|
||||
"bad host",
|
||||
] {
|
||||
@@ -1491,6 +1579,29 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_only_public_probe_addresses() {
|
||||
for address in ["1.1.1.1", "8.8.8.8", "2606:4700:4700::1111"] {
|
||||
assert!(is_public_probe_ip(address.parse().expect("IP address")));
|
||||
}
|
||||
for address in [
|
||||
"0.0.0.0",
|
||||
"10.0.0.1",
|
||||
"100.64.0.1",
|
||||
"127.0.0.1",
|
||||
"169.254.169.254",
|
||||
"172.16.0.1",
|
||||
"192.168.1.1",
|
||||
"198.18.0.1",
|
||||
"224.0.0.1",
|
||||
"::1",
|
||||
"fe80::1",
|
||||
"fd00:ec2::254",
|
||||
] {
|
||||
assert!(!is_public_probe_ip(address.parse().expect("IP address")));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculates_median() {
|
||||
assert_eq!(median(&mut [4.0, 1.0, 3.0]), 3.0);
|
||||
@@ -1506,11 +1617,10 @@ mod tests {
|
||||
listener.accept().expect("accept probe connection");
|
||||
}
|
||||
});
|
||||
let (latency, loss) = tcp_latency_probe(&address.to_string());
|
||||
let (latency, loss) = tcp_latency_probe_address(&address);
|
||||
accepter.join().expect("join listener");
|
||||
assert!(latency >= 0.0);
|
||||
assert_eq!(loss, 0.0);
|
||||
assert_eq!(tcp_latency_probe(""), (-1.0, -1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,81 +5,41 @@
|
||||
"": {
|
||||
"name": "nodeflare",
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.120.0",
|
||||
"wrangler": "^4.120.1",
|
||||
},
|
||||
},
|
||||
"frontend": {
|
||||
"name": "nodeflare-ui",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"recharts": "^2.15.4",
|
||||
"lucide-react": "^1.31.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"recharts": "^3.10.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.6",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.2.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@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" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
|
||||
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260801.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-wuJWbXpKvncJi1P0GKS+iYpN5tHdb7JPJJ/+6ZQe8zzovHHVMkLJPNBsgWpqeUhpM3g9qTwEKd2rglNKejuh5A=="],
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260804.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-191/PPEFicRK2wK69eXzSjnLgHL79k7zR2VUpyIr8rhFgGns1b5bTHgnBuUrgU2LPbEtwbE5eL2hJ0uhLAFEow=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260801.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kwoZiTpnhNrF3+APx84Q/oAqvJ3sU9yefGagwm/ASaH/2W19x0vghkW/r4qCoHCK0WW7EPugZ+aXjgPMRtlq1Q=="],
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260804.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aI2cAFLsrNkSz3kLSQrgrO9ICTfY7jb2h0jgaWDE9mQLQQDfpXeYrKWt07tTgMmeNP79o9w3NZe3aqtt8mTGHQ=="],
|
||||
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260801.1", "", { "os": "linux", "cpu": "x64" }, "sha512-r0vAxCZH+Jih9Unm1yoyiByPNWNgawcKciOHDm5Q37ZVGOkKLsT9AtLe3yLSaul76WrKqtf+JP2n0WW32VBLJg=="],
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260804.1", "", { "os": "linux", "cpu": "x64" }, "sha512-KBCjxBIlN2jucfQGaTK4EgmPsWzQgYR/zYhPfi3mkWwdoTyG1dgrt2aizKps/SYse85ci/SOxojKk0/K7vstPw=="],
|
||||
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260801.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-zWgpdZtSozvIgzQNmQiDSF8yEOQJUkRAWNsDXXzAAoy+fCn8YUoSibj3mpFSbZRvbUldeBEaW6SCdC2VEMkhNQ=="],
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260804.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-7lswfarBZ7xkHpTFrNb74ExwOltIalVaASc+GOYfCNtnwFxK/JZSVByfm/hnZEBtrHU7fMY2z7dYT64rwS0pHg=="],
|
||||
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260801.1", "", { "os": "win32", "cpu": "x64" }, "sha512-2oQz+Ksu4ji6e/+ZoYX+tWQEcxAii2p7l+iR8kx48W1llMalaufAsmVxTlk+3/vrM7D3/2c0iK448e0UQTcIMg=="],
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260804.1", "", { "os": "win32", "cpu": "x64" }, "sha512-GOgRWYtxISN5rAWfx3K7zubt3xEn0/ZPFrbcLL+GwT4ouCKyNoHqfT1cPNB6Nx7t8BHEjnuTG7gkHO4Wd5ORdg=="],
|
||||
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
@@ -191,17 +151,13 @@
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||
"@oxc-project/types": ["@oxc-project/types@0.143.0", "", {}, "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA=="],
|
||||
|
||||
"@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],
|
||||
|
||||
@@ -209,69 +165,45 @@
|
||||
|
||||
"@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.3", "", { "os": "android", "cpu": "arm64" }, "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.3", "", { "os": "linux", "cpu": "arm" }, "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="],
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="],
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.3", "", { "os": "none", "cpu": "arm64" }, "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="],
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="],
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
|
||||
|
||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.23", "", {}, "sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
@@ -291,28 +223,58 @@
|
||||
|
||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA=="],
|
||||
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
|
||||
|
||||
"@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
|
||||
|
||||
"@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
|
||||
|
||||
"@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
|
||||
|
||||
"@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
|
||||
|
||||
"@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
|
||||
|
||||
"@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
|
||||
|
||||
"@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
|
||||
|
||||
"@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
|
||||
|
||||
"@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
|
||||
|
||||
"@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
|
||||
|
||||
"@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
|
||||
|
||||
"@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
|
||||
|
||||
"@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
|
||||
|
||||
"@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
|
||||
|
||||
"@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
|
||||
|
||||
"@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
|
||||
|
||||
"@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
|
||||
|
||||
"@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
|
||||
|
||||
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="],
|
||||
|
||||
"nodeflare-ui": ["nodeflare-ui@workspace:frontend"],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
@@ -339,59 +301,59 @@
|
||||
|
||||
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.402", "", {}, "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA=="],
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.50.0", "", {}, "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||
|
||||
"fast-equals": ["fast-equals@5.4.1", "", {}, "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ=="],
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
"immer": ["immer@11.1.16", "", {}, "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="],
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||
|
||||
"miniflare": ["miniflare@5.20260801.1-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260801.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-BHPVzIDA6mbx7LefxpvkXW7DHx9FKB9GorZatbnrrFTt3CVMU8zuUpbgyCuebwDKcTTOZos43ta8GQ0eMVEpxA=="],
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||
|
||||
"lucide-react": ["lucide-react@1.31.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg=="],
|
||||
|
||||
"miniflare": ["miniflare@5.20260804.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260804.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-UDegnTukIpJCI/lNyQXU7B8kYFLcMZihlxqqOocnc62tV8AO6XofmFmZm3ZLDXTWZpkZnLLJ2sKCSlTIEXO3dQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.17", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
"nodeflare-ui": ["nodeflare-ui@workspace:frontend"],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
|
||||
@@ -403,29 +365,27 @@
|
||||
|
||||
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||
|
||||
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
"react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="],
|
||||
|
||||
"react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
|
||||
"recharts": ["recharts@3.10.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^11.1.8", "react-redux": "8.x.x || 9.x.x", "reselect": "5.2.0", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA=="],
|
||||
|
||||
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
|
||||
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
|
||||
|
||||
"recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
|
||||
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
||||
|
||||
"recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
|
||||
"reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
|
||||
|
||||
"rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="],
|
||||
"rolldown": ["rolldown@1.2.3", "", { "dependencies": { "@oxc-project/types": "=0.143.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.3", "@rolldown/binding-darwin-arm64": "1.2.3", "@rolldown/binding-darwin-x64": "1.2.3", "@rolldown/binding-freebsd-x64": "1.2.3", "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", "@rolldown/binding-linux-arm64-gnu": "1.2.3", "@rolldown/binding-linux-arm64-musl": "1.2.3", "@rolldown/binding-linux-ppc64-gnu": "1.2.3", "@rolldown/binding-linux-s390x-gnu": "1.2.3", "@rolldown/binding-linux-x64-gnu": "1.2.3", "@rolldown/binding-linux-x64-musl": "1.2.3", "@rolldown/binding-openharmony-arm64": "1.2.3", "@rolldown/binding-win32-arm64-msvc": "1.2.3", "@rolldown/binding-win32-x64-msvc": "1.2.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="],
|
||||
|
||||
@@ -439,38 +399,26 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
|
||||
|
||||
"undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
|
||||
|
||||
"unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.3.0", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA=="],
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
|
||||
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||
|
||||
"vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
"vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="],
|
||||
|
||||
"workerd": ["workerd@1.20260801.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260801.1", "@cloudflare/workerd-darwin-arm64": "1.20260801.1", "@cloudflare/workerd-linux-64": "1.20260801.1", "@cloudflare/workerd-linux-arm64": "1.20260801.1", "@cloudflare/workerd-windows-64": "1.20260801.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-/g9JGTyqnHtoIscpBHqKD8swE2V4StBs2i69PmLiOhH45OP95jCFICl4F1hKlgN57rqfni5LiCitttoX5OOkVA=="],
|
||||
"workerd": ["workerd@1.20260804.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260804.1", "@cloudflare/workerd-darwin-arm64": "1.20260804.1", "@cloudflare/workerd-linux-64": "1.20260804.1", "@cloudflare/workerd-linux-arm64": "1.20260804.1", "@cloudflare/workerd-windows-64": "1.20260804.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-b0P38g5/ssemwWxd/mafNYggEZ0ere7PCiUH6RCHkgqjRhhXTP45nDiL2L3iIvi8uF2IjNrGpVgMXEunCaeY/w=="],
|
||||
|
||||
"wrangler": ["wrangler@4.120.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260801.1-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260801.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260801.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-cBmu/MeaB/fPacC0JpATs4duTOCagBxrZo+vBzuTX06tLzwSyAHE1drlHUZ8rP0VqVz1fy3ReGYTiHdKkoHltg=="],
|
||||
"wrangler": ["wrangler@4.120.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260804.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260804.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260804.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-rQJ38rY02XiVITbSBWHC7oap3M2evcXgsC4CdlIX4WQENUZMMnue9j2vXO4aIi39KR+jDH+UHK8JNqp1+UjkRQ=="],
|
||||
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
||||
|
||||
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,16 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"recharts": "^2.15.4"
|
||||
"lucide-react": "^1.31.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"recharts": "^3.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.6"
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.2.1"
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,9 @@ export default function App() {
|
||||
if (proof) endpoint.searchParams.set("turnstile_verified", proof);
|
||||
const socket = new WebSocket(endpoint);
|
||||
wsRef.current.push(socket);
|
||||
socket.onopen = () => void load(true);
|
||||
socket.onclose = () => {
|
||||
wsRef.current = wsRef.current.filter((current) => current !== socket);
|
||||
if (!cancelled) reconnects.push(window.setTimeout(connect, 3000));
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
@@ -175,7 +177,7 @@ export default function App() {
|
||||
wsRef.current.forEach((socket) => socket.close());
|
||||
wsRef.current = [];
|
||||
};
|
||||
}, [config.websocket, config.public_dashboard, config.turnstile_enabled, loading, needsVerification, selectedId]);
|
||||
}, [config.websocket, config.public_dashboard, config.turnstile_enabled, load, loading, needsVerification, selectedId]);
|
||||
|
||||
const groups = useMemo(() => ["__all__", ...Array.from(new Set(servers.map((server) => server.group_name || "默认")))], [servers]);
|
||||
const visible = useMemo(() => servers.filter((server) => {
|
||||
|
||||
+14
-5
@@ -1,4 +1,4 @@
|
||||
import type { AlertRule, AlertRuleInput, CloudflareUsage, Config, DatabaseStats, ExchangeRates, HistoryPoint, LatencySample, LatencyTask, LatencyTaskInput, LatencyTestPoint, Server, ServerInput, Settings, ThemeSettingsSchema } from "./types";
|
||||
import type { AdminServer, AlertRule, AlertRuleInput, CloudflareUsage, Config, DatabaseStats, ExchangeRates, HistoryPoint, LatencySample, LatencyTask, LatencyTaskInput, LatencyTestPoint, Server, ServerInput, Settings, Theme, ThemeSettingsSchema } from "./types";
|
||||
|
||||
const TOKEN_KEY = "nodeflare-admin-token";
|
||||
const TURNSTILE_KEY = "nodeflare-turnstile-verified";
|
||||
@@ -45,8 +45,8 @@ export const api = {
|
||||
config: () => request<Config>("/api/config"),
|
||||
exchangeRates: () => request<ExchangeRates>("/api/exchange-rates"),
|
||||
refreshExchangeRates: () => request<ExchangeRates>("/api/admin/exchange-rates/refresh", { method: "POST" }, true),
|
||||
servers: (admin = false) =>
|
||||
request<{ servers: Server[] }>(admin ? "/api/admin/servers" : "/api/servers", {}, admin),
|
||||
servers: () => request<{ servers: Server[] }>("/api/servers"),
|
||||
adminServers: () => request<{ servers: AdminServer[] }>("/api/admin/servers", {}, true),
|
||||
server: (id: string) => request<Server>(`/api/servers/${encodeURIComponent(id)}`),
|
||||
history: (id: string, hours: number) =>
|
||||
request<{ points: HistoryPoint[] }>(`/api/history/${encodeURIComponent(id)}?hours=${hours}`),
|
||||
@@ -54,10 +54,10 @@ export const api = {
|
||||
request<{ tasks: LatencyTestPoint[]; points: LatencySample[] }>(`/api/latency/${encodeURIComponent(id)}?hours=${hours}`),
|
||||
verifyTurnstile: (token: string) =>
|
||||
request<{ verification: string }>("/api/turnstile/verify", { method: "POST", body: JSON.stringify({ token }) }),
|
||||
login: (username: string, password: string, turnstileToken: string) =>
|
||||
login: (username: string, password: string, passwordDerived: string, turnstileToken: string) =>
|
||||
request<{ token: string }>("/api/admin/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password, turnstile_token: turnstileToken }),
|
||||
body: JSON.stringify({ username, password, password_derived: passwordDerived, turnstile_token: turnstileToken }),
|
||||
}),
|
||||
settings: () => request<Settings>("/api/admin/settings", {}, true),
|
||||
latencyTasks: () => request<{ tasks: LatencyTask[] }>("/api/admin/latency-tasks", {}, true),
|
||||
@@ -75,6 +75,15 @@ export const api = {
|
||||
deleteAlertRule: (id: string) =>
|
||||
request<{ success: boolean }>(`/api/admin/alert-rules/${encodeURIComponent(id)}`, { method: "DELETE" }, true),
|
||||
themeSettings: () => request<ThemeSettingsSchema>("/api/admin/theme-settings", {}, true),
|
||||
themes: () => request<{ themes: Theme[] }>("/api/admin/themes", {}, true),
|
||||
addTheme: (input: Pick<Theme, "name" | "description" | "url">) =>
|
||||
request<{ id: string }>("/api/admin/themes", { method: "POST", body: JSON.stringify(input) }, true),
|
||||
activateTheme: (id: string) =>
|
||||
request<{ success: boolean }>(`/api/admin/themes/${encodeURIComponent(id)}/activate`, { method: "POST" }, true),
|
||||
previewTheme: (id: string) =>
|
||||
request<{ preview_url: string }>(`/api/admin/themes/${encodeURIComponent(id)}/preview`, { method: "POST" }, true),
|
||||
deleteTheme: (id: string) =>
|
||||
request<{ success: boolean }>(`/api/admin/themes/${encodeURIComponent(id)}`, { method: "DELETE" }, true),
|
||||
saveSettings: (input: Partial<Settings>) =>
|
||||
request<{ settings: Settings; token: string | null }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(input) }, true),
|
||||
createServer: (input: ServerInput) =>
|
||||
|
||||
@@ -23,11 +23,14 @@ import {
|
||||
SlidersHorizontal,
|
||||
Sun,
|
||||
Trash2,
|
||||
Check,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import { DragEvent, FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { api, ApiError, getToken, setToken } from "../api";
|
||||
import { formatBytes, isOnline } from "../format";
|
||||
import { ASSET_CURRENCIES, type AgentInstallTarget, type CloudflareUsage, type Config, type DatabaseStats, type ExchangeRates, type Server, type ServerInput, type Settings, type ThemeSettingField, type ThemeSettingsSchema, type ThemeSettingValue } from "../types";
|
||||
import { derivePassword } from "../password";
|
||||
import { ASSET_CURRENCIES, type AdminServer, type AgentInstallTarget, type CloudflareUsage, type Config, type DatabaseStats, type ExchangeRates, type ServerInput, type Settings, type Theme, type ThemeSettingField, type ThemeSettingsSchema, type ThemeSettingValue } from "../types";
|
||||
import { Checkbox } from "./Checkbox";
|
||||
import { TurnstileWidget } from "./TurnstileWidget";
|
||||
import { LatencyManager } from "./LatencyManager";
|
||||
@@ -40,7 +43,7 @@ const adminPages: Record<AdminTab, { title: string; description: string }> = {
|
||||
servers: { title: "服务器", description: "管理监控节点、Agent 密钥和运行参数" },
|
||||
latency: { title: "延迟监测", description: "配置分配给各服务器的 TCP 与 ICMP 延迟任务" },
|
||||
appearance: { title: "站点设置", description: "调整站点信息、公开内容和前台显示项目" },
|
||||
themes: { title: "主题管理", description: "管理内置前端主题和公开访问" },
|
||||
themes: { title: "主题商店", description: "选择内置主题或安装远程主题" },
|
||||
themeSettings: { title: "主题设置", description: "调整当前前端主题提供的显示选项" },
|
||||
alerts: { title: "通知", description: "配置 Telegram 通知和资源告警阈值" },
|
||||
security: { title: "登录与安全", description: "管理管理员账号和 Cloudflare Turnstile 防护" },
|
||||
@@ -73,7 +76,7 @@ const emptyServer: ServerInput = {
|
||||
auto_update: true,
|
||||
};
|
||||
|
||||
function toInput(server: Server): ServerInput {
|
||||
function toInput(server: AdminServer): ServerInput {
|
||||
return {
|
||||
...emptyServer,
|
||||
name: server.name,
|
||||
@@ -139,15 +142,19 @@ export function AdminPanel({
|
||||
const [error, setError] = useState("");
|
||||
const [notice, setNotice] = useState("");
|
||||
const [tab, setTab] = useState<AdminTab>("servers");
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
const [servers, setServers] = useState<AdminServer[]>([]);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [draggingId, setDraggingId] = useState("");
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
const [database, setDatabase] = useState<DatabaseStats | null>(null);
|
||||
const [cloudflareUsage, setCloudflareUsage] = useState<CloudflareUsage | null>(null);
|
||||
const [exchangeRates, setExchangeRates] = useState<ExchangeRates | null>(null);
|
||||
const [themes, setThemes] = useState<Theme[]>([]);
|
||||
const [themeName, setThemeName] = useState("");
|
||||
const [themeDescription, setThemeDescription] = useState("");
|
||||
const [themeUrl, setThemeUrl] = useState("");
|
||||
const [themeSettingsSchema, setThemeSettingsSchema] = useState<ThemeSettingsSchema | null>(null);
|
||||
const [editing, setEditing] = useState<Server | "new" | null>(null);
|
||||
const [editing, setEditing] = useState<AdminServer | "new" | null>(null);
|
||||
const [form, setForm] = useState<ServerInput>(emptyServer);
|
||||
const [install, setInstall] = useState<AgentInstallTarget[]>([]);
|
||||
const [installPlatform, setInstallPlatform] = useState<AgentPlatform>("linux");
|
||||
@@ -157,8 +164,9 @@ export function AdminPanel({
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const [serverResult, settingsResult] = await Promise.all([api.servers(true), api.settings()]);
|
||||
const [serverResult, settingsResult, themesResult] = await Promise.all([api.adminServers(), api.settings(), api.themes()]);
|
||||
setServers(serverResult.servers);
|
||||
setThemes(themesResult.themes);
|
||||
setSettings({
|
||||
...settingsResult,
|
||||
site_announcement: settingsResult.site_announcement || "",
|
||||
@@ -193,7 +201,8 @@ export function AdminPanel({
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const result = await api.login(username.trim(), password, turnstileToken);
|
||||
const passwordDerived = await derivePassword(password, config.password_client_salt);
|
||||
const result = await api.login(username.trim(), password, passwordDerived, turnstileToken);
|
||||
setToken(result.token);
|
||||
setPassword("");
|
||||
setTurnstileToken("");
|
||||
@@ -206,7 +215,7 @@ export function AdminPanel({
|
||||
} finally { setBusy(false); }
|
||||
}
|
||||
|
||||
function openEditor(server?: Server) {
|
||||
function openEditor(server?: AdminServer) {
|
||||
setEditing(server ?? "new");
|
||||
setForm(server ? toInput(server) : { ...emptyServer });
|
||||
setError("");
|
||||
@@ -243,13 +252,13 @@ export function AdminPanel({
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
async function remove(server: Server) {
|
||||
async function remove(server: AdminServer) {
|
||||
if (!window.confirm(`确认删除“${server.name}”及其全部历史数据?`)) return;
|
||||
try { await api.deleteServer(server.id); await load(); onChanged(); }
|
||||
catch (reason) { setError(reason instanceof Error ? reason.message : "删除失败"); }
|
||||
}
|
||||
|
||||
async function rotate(server: Server) {
|
||||
async function rotate(server: AdminServer) {
|
||||
if (!window.confirm(`重置“${server.name}”的 Agent 密钥?旧 Agent 将立即失效。`)) return;
|
||||
try {
|
||||
const result = await api.rotateToken(server.id);
|
||||
@@ -296,9 +305,15 @@ export function AdminPanel({
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setNotice("");
|
||||
const payload = { ...settings };
|
||||
if (!payload.new_password) delete payload.new_password;
|
||||
const payload: Partial<Settings> = { ...settings };
|
||||
delete payload.admin_password_configured;
|
||||
try {
|
||||
if (payload.new_password) {
|
||||
payload.new_password_derived = await derivePassword(payload.new_password, config.password_client_salt);
|
||||
} else {
|
||||
delete payload.new_password;
|
||||
delete payload.new_password_derived;
|
||||
}
|
||||
const result = await api.saveSettings(payload);
|
||||
if (result.token) setToken(result.token);
|
||||
setSettings(result.settings);
|
||||
@@ -350,6 +365,59 @@ export function AdminPanel({
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
async function addTheme(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
setBusy(true); setError(""); setNotice("");
|
||||
try {
|
||||
await api.addTheme({ name: themeName.trim(), description: themeDescription.trim(), url: themeUrl.trim() });
|
||||
setThemeName(""); setThemeDescription(""); setThemeUrl("");
|
||||
await load();
|
||||
setNotice("主题已添加");
|
||||
} catch (reason) { setError(reason instanceof Error ? reason.message : "添加主题失败"); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
async function activateTheme(theme: Theme) {
|
||||
if (theme.active) return;
|
||||
setBusy(true); setError(""); setNotice("");
|
||||
try {
|
||||
await api.activateTheme(theme.id);
|
||||
await load();
|
||||
setNotice(`已启用主题:${theme.name}`);
|
||||
onChanged();
|
||||
} catch (reason) { setError(reason instanceof Error ? reason.message : "启用主题失败"); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
async function previewTheme(theme: Theme) {
|
||||
if (theme.builtin) return;
|
||||
const previewWindow = window.open("", "_blank");
|
||||
setBusy(true); setError("");
|
||||
try {
|
||||
const { preview_url } = await api.previewTheme(theme.id);
|
||||
if (previewWindow) {
|
||||
previewWindow.opener = null;
|
||||
previewWindow.location.replace(preview_url);
|
||||
} else {
|
||||
window.open(preview_url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
} catch (reason) {
|
||||
previewWindow?.close();
|
||||
setError(reason instanceof Error ? reason.message : "创建主题预览失败");
|
||||
} finally { setBusy(false); }
|
||||
}
|
||||
|
||||
async function removeTheme(theme: Theme) {
|
||||
if (theme.builtin || !window.confirm(`确认删除主题“${theme.name}”?`)) return;
|
||||
setBusy(true); setError(""); setNotice("");
|
||||
try {
|
||||
await api.deleteTheme(theme.id);
|
||||
await load();
|
||||
setNotice("主题已删除");
|
||||
} catch (reason) { setError(reason instanceof Error ? reason.message : "删除主题失败"); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
async function clearHistory() {
|
||||
if (!window.confirm("确认清空全部历史指标?节点配置和最新状态不会删除。")) return;
|
||||
setBusy(true); setError("");
|
||||
@@ -431,7 +499,7 @@ export function AdminPanel({
|
||||
<button className={tab === "servers" ? "active" : ""} onClick={() => selectTab("servers")}><ServerCog size={19} />服务器</button>
|
||||
<button className={tab === "latency" ? "active" : ""} onClick={() => selectTab("latency")}><RadioTower size={19} />延迟监测</button>
|
||||
<button className={tab === "appearance" ? "active" : ""} onClick={() => selectTab("appearance")}><Eye size={19} />站点设置</button>
|
||||
<button className={tab === "themes" ? "active" : ""} onClick={() => selectTab("themes")}><Palette size={19} />主题管理</button>
|
||||
<button className={tab === "themes" ? "active" : ""} onClick={() => selectTab("themes")}><Palette size={19} />主题商店</button>
|
||||
<button className={tab === "themeSettings" ? "active" : ""} onClick={() => { selectTab("themeSettings"); void loadThemeSettings(); }}><SlidersHorizontal size={19} />主题设置</button>
|
||||
<button className={tab === "alerts" ? "active" : ""} onClick={() => selectTab("alerts")}><AlertTriangle size={19} />通知</button>
|
||||
<button className={tab === "security" ? "active" : ""} onClick={() => selectTab("security")}><ShieldCheck size={19} />登录与安全</button>
|
||||
@@ -463,14 +531,40 @@ export function AdminPanel({
|
||||
) : tab === "latency" ? (
|
||||
<LatencyManager servers={servers} onError={setError} onNotice={setNotice} />
|
||||
) : tab === "themes" && settings ? (
|
||||
<form className="settings-form" onSubmit={saveSite}>
|
||||
<div className="section-title"><Palette size={15} />内置主题</div>
|
||||
<div className="builtin-theme-row"><div><strong>Komari Glass</strong><span>内置前端主题</span></div><b>使用中</b></div>
|
||||
<div className="section-subtitle">公开访问</div>
|
||||
<Toggle label="公开仪表盘" checked={settings.public_dashboard} onChange={(value) => updateSettings("public_dashboard", value)} />
|
||||
<p className="settings-hint">关闭后,未登录访客无法读取节点和历史数据。</p>
|
||||
<div className="form-actions"><button className="primary-btn" disabled={busy}><Save size={16} />保存主题管理设置</button></div>
|
||||
</form>
|
||||
<div className="theme-store-page">
|
||||
<section className="admin-section">
|
||||
<div className="section-head"><div><h3>主题列表</h3><span>内置主题始终可用,远程主题由 Worker 代理前台页面和资源。</span></div></div>
|
||||
<div className="theme-store-grid">
|
||||
{themes.map((theme) => <article className={`theme-store-card ${theme.active ? "active" : ""}`} key={theme.id}>
|
||||
<div className="theme-card-icon"><Palette size={20} /></div>
|
||||
<div className="theme-card-main">
|
||||
<div className="theme-card-title"><strong>{theme.name}</strong><span className={`theme-badge ${theme.builtin ? "builtin" : "remote"}`}>{theme.builtin ? "默认主题" : "远程主题"}</span></div>
|
||||
<p>{theme.description || "暂无主题说明"}</p>
|
||||
{!theme.builtin ? <a href={theme.url} target="_blank" rel="noreferrer"><span>{theme.url}</span><ExternalLink size={13} /></a> : <small>NodeFlare 内置 Komari Glass</small>}
|
||||
</div>
|
||||
<div className="theme-card-actions">
|
||||
{!theme.builtin ? <button type="button" className="secondary-btn compact" disabled={busy} onClick={() => void previewTheme(theme)}><Eye size={14} />预览</button> : null}
|
||||
<button type="button" className={theme.active ? "theme-active-btn" : "primary-btn compact"} disabled={busy || theme.active} onClick={() => void activateTheme(theme)}>{theme.active ? <><Check size={14} />使用中</> : "启用"}</button>
|
||||
{!theme.builtin ? <button type="button" className="icon-btn danger" disabled={busy} title="删除主题" onClick={() => void removeTheme(theme)}><Trash2 size={15} /></button> : null}
|
||||
</div>
|
||||
</article>)}
|
||||
</div>
|
||||
</section>
|
||||
<form className="admin-section theme-add-form" onSubmit={addTheme}>
|
||||
<div className="section-title"><Plus size={15} />添加远程主题</div>
|
||||
<p className="settings-hint">填写 GitHub <code>tree</code> 地址,对应目录需包含 <code>index.html</code> 和 <code>assets/</code> 目录。</p>
|
||||
<div className="form-grid"><label><span>主题名称</span><input required maxLength={80} value={themeName} onChange={(event) => setThemeName(event.target.value)} placeholder="例如:Ocean" /></label><label><span>主题 URL</span><input required type="url" maxLength={2048} value={themeUrl} onChange={(event) => setThemeUrl(event.target.value)} placeholder="https://github.com/user/theme/tree/main" /></label></div>
|
||||
<label><span>主题说明(可选)</span><textarea rows={2} maxLength={300} value={themeDescription} onChange={(event) => setThemeDescription(event.target.value)} placeholder="简短描述主题风格和来源" /></label>
|
||||
<div className="theme-risk-note">远程主题包含第三方 HTML、JavaScript 和 CSS。仅添加你信任的主题仓库。</div>
|
||||
<div className="form-actions"><button className="primary-btn" disabled={busy}><Plus size={16} />添加主题</button></div>
|
||||
</form>
|
||||
<form className="admin-section" onSubmit={saveSite}>
|
||||
<div className="section-title"><Eye size={15} />公开访问</div>
|
||||
<Toggle label="公开仪表盘" checked={settings.public_dashboard} onChange={(value) => updateSettings("public_dashboard", value)} />
|
||||
<p className="settings-hint">关闭后,未登录访客无法读取节点和历史数据。</p>
|
||||
<div className="form-actions"><button className="primary-btn" disabled={busy}><Save size={16} />保存公开设置</button></div>
|
||||
</form>
|
||||
</div>
|
||||
) : settings ? (
|
||||
<form className="settings-form" onSubmit={saveSite}>
|
||||
{tab === "appearance" ? <>
|
||||
@@ -492,7 +586,7 @@ export function AdminPanel({
|
||||
|
||||
{tab === "alerts" ? <>
|
||||
<div className="section-title"><AlertTriangle size={15} />通知与告警</div>
|
||||
<div className="form-grid"><label><span>Telegram Bot Token</span><input autoComplete="off" value={settings.notification_endpoint} onChange={(event) => updateSettings("notification_endpoint", event.target.value)} placeholder="123456789:AA..." /></label><label><span>Telegram Chat ID</span><input value={settings.notification_target} onChange={(event) => updateSettings("notification_target", event.target.value)} placeholder="个人、群组或频道 ID" /></label></div>
|
||||
<div className="form-grid"><label><span>Telegram Bot Token</span><input autoComplete="off" type="password" value={settings.notification_endpoint} onChange={(event) => updateSettings("notification_endpoint", event.target.value)} placeholder="123456789:AA..." /></label><label><span>Telegram Chat ID</span><input value={settings.notification_target} onChange={(event) => updateSettings("notification_target", event.target.value)} placeholder="个人、群组或频道 ID" /></label></div>
|
||||
<Toggle label="启用通知与告警" checked={settings.notification_enabled} onChange={(value) => updateSettings("notification_enabled", value)} />
|
||||
<div className="form-grid three"><label><span>离线告警延迟(分钟)</span><input type="number" min="2" max="1440" value={settings.offline_alert_minutes} onChange={(event) => updateSettings("offline_alert_minutes", Number(event.target.value))} /></label><label><span>到期提醒(天)</span><input type="number" min="0" max="365" value={settings.expiry_alert_days} onChange={(event) => updateSettings("expiry_alert_days", Number(event.target.value))} /></label><label><span>通知测试</span><button type="button" className="secondary-btn" disabled={busy} onClick={() => void testNotification()}>发送测试通知</button></label></div>
|
||||
<p className="settings-hint">通过 Telegram Bot 发送告警;告警状态会记录在 D1,同一故障和恢复只发送一次。</p>
|
||||
|
||||
@@ -19,7 +19,20 @@ function validHost(value: string) {
|
||||
const [name, port] = host.split(":");
|
||||
if (port && (!/^\d{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535)) return false;
|
||||
if (!name || name.startsWith(".") || name.endsWith(".")) return false;
|
||||
return name.split(".").every((part) => /^[A-Za-z0-9_](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?$/.test(part));
|
||||
const labels = name.split(".");
|
||||
if (labels.length === 4 && labels.every((part) => /^\d+$/.test(part))) {
|
||||
const octets = labels.map(Number);
|
||||
if (octets.some((part) => part < 0 || part > 255)) return false;
|
||||
const [a, b, c] = octets;
|
||||
return !(a === 0 || a === 10 || a === 127 || (a === 100 && b >= 64 && b <= 127)
|
||||
|| (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31)
|
||||
|| (a === 192 && b === 0 && (c === 0 || c === 2)) || (a === 192 && b === 168)
|
||||
|| (a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100)))
|
||||
|| (a === 203 && b === 0 && c === 113) || a >= 224);
|
||||
}
|
||||
const lower = name.toLowerCase();
|
||||
if (labels.length < 2 || ["local", "localhost", "internal", "lan", "localdomain"].some((suffix) => lower === suffix || lower.endsWith(`.${suffix}`)) || lower === "home.arpa" || lower.endsWith(".home.arpa")) return false;
|
||||
return labels.every((part) => /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(part));
|
||||
}
|
||||
|
||||
export function LatencyManager({
|
||||
@@ -83,7 +96,7 @@ export function LatencyManager({
|
||||
async function save(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (!validHost(form.target) || (form.task_type === "icmp" && form.target.includes(":"))) {
|
||||
onError(form.task_type === "icmp" ? "ICMP 目标应为域名或 IPv4,不能包含端口" : "TCP 目标应为域名、IPv4 或 host:port");
|
||||
onError(form.task_type === "icmp" ? "ICMP 目标应为公网域名或公网 IPv4,不能包含端口" : "TCP 目标应为公网域名、公网 IPv4 或 host:port");
|
||||
return;
|
||||
}
|
||||
if (!form.default_enabled && !form.server_ids.length) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MonitorCog } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const systems = [
|
||||
["alibaba", "os-alibaba.svg", "Alibaba Cloud Linux"],
|
||||
["alma", "os-alma.svg", "AlmaLinux"], ["alpine", "os-alpine.webp", "Alpine Linux"],
|
||||
["arch", "os-arch.svg", "Arch Linux"], ["armbian", "os-armbian.png", "Armbian"],
|
||||
["centos", "os-centos.svg", "CentOS"], ["debian", "os-debian.svg", "Debian"],
|
||||
|
||||
+12
-24
@@ -10,6 +10,7 @@ export const demoConfig: Config = {
|
||||
offline_threshold_seconds: 180,
|
||||
history_retention_days: 30,
|
||||
default_theme: "system",
|
||||
active_theme_id: "builtin-komari-glass",
|
||||
background_url: "",
|
||||
theme_options: {},
|
||||
show_search: true,
|
||||
@@ -25,6 +26,7 @@ export const demoConfig: Config = {
|
||||
turnstile_enabled: false,
|
||||
turnstile_login_enabled: true,
|
||||
turnstile_site_key: "",
|
||||
password_client_salt: "nodeflare-demo-password-kdf",
|
||||
websocket: true,
|
||||
};
|
||||
|
||||
@@ -77,9 +79,6 @@ const baseServer: Server = {
|
||||
region: "",
|
||||
group_name: "默认",
|
||||
tags: "",
|
||||
note: "",
|
||||
hidden: 0,
|
||||
sort_order: 0,
|
||||
expires_at: null,
|
||||
traffic_limit: 0,
|
||||
traffic_limit_type: "sum",
|
||||
@@ -88,16 +87,7 @@ const baseServer: Server = {
|
||||
currency: "CNY",
|
||||
auto_renewal: 0,
|
||||
public_remark: "",
|
||||
network_interface: "",
|
||||
reset_day: 1,
|
||||
report_interval: 60,
|
||||
collect_interval: 5,
|
||||
rx_correction: 0,
|
||||
tx_correction: 0,
|
||||
offline_notify_disabled: 0,
|
||||
auto_update: 1,
|
||||
created_at: now - 365 * 86400,
|
||||
updated_at: now,
|
||||
timestamp: now - 12,
|
||||
cpu: 24,
|
||||
load1: 0.42,
|
||||
@@ -123,8 +113,6 @@ const baseServer: Server = {
|
||||
kernel: "6.1.0",
|
||||
arch: "x86_64",
|
||||
virtualization: "KVM",
|
||||
ipv4: "203.0.113.8",
|
||||
ipv6: "2001:db8::8",
|
||||
gpu_usage: 0,
|
||||
gpu_model: "",
|
||||
agent_version: "0.0.1",
|
||||
@@ -140,21 +128,21 @@ const baseServer: Server = {
|
||||
latency: [],
|
||||
};
|
||||
|
||||
function node(sort_order: number, input: Partial<Server> & Pick<Server, "id" | "name">): Server {
|
||||
const server = { ...baseServer, ...input, sort_order };
|
||||
function node(input: Partial<Server> & Pick<Server, "id" | "name">): Server {
|
||||
const server = { ...baseServer, ...input };
|
||||
server.latency = input.latency ?? demoLatestLatency(server.id);
|
||||
return server;
|
||||
}
|
||||
|
||||
export const demoServers: Server[] = [
|
||||
node(0, { id: "hongkong-edge", name: "香港 Edge 01", region: "HK", group_name: "边缘网络", tags: "主力,线路:BGP,用途:网站", price: 128, expires_at: now + 46 * 86400, traffic_limit: 2 * 1024 ** 4 }),
|
||||
node(1, { id: "tokyo-core", name: "东京 Core", region: "JP", group_name: "核心服务", tags: "主力,线路:Premium", price: 9.9, currency: "USD", expires_at: now + 46 * 86400, mem_total: 16 * 1024 ** 3, mem_used: 6.72 * 1024 ** 3, disk_total: 224 * 1024 ** 3, disk_used: 69.44 * 1024 ** 3, traffic_limit: 2 * 1024 ** 4, net_rx_total: 810 * 1024 ** 3, net_tx_total: 310 * 1024 ** 3, cpu: 31 }),
|
||||
node(2, { id: "singapore-data", name: "新加坡 Data", region: "SG", group_name: "数据服务", tags: "数据库,NVMe", price: 88, expires_at: now + 46 * 86400, mem_total: 24 * 1024 ** 3, mem_used: 10.08 * 1024 ** 3, disk_total: 288 * 1024 ** 3, disk_used: 89.28 * 1024 ** 3, traffic_limit: 2 * 1024 ** 4, net_rx_total: 980 * 1024 ** 3, net_tx_total: 400 * 1024 ** 3, cpu: 38 }),
|
||||
node(3, { id: "los-angeles-west", name: "洛杉矶 West", region: "US", group_name: "边缘网络", tags: "备用,线路:CN2", price: 24, currency: "USD", expires_at: now + 46 * 86400, disk_total: 352 * 1024 ** 3, disk_used: 109.12 * 1024 ** 3, traffic_limit: 2 * 1024 ** 4, net_rx_total: 1.12 * 1024 ** 4, net_tx_total: 490 * 1024 ** 3, cpu: 45 }),
|
||||
node(4, { id: "frankfurt-lab", name: "法兰克福 Lab", region: "DE", group_name: "实验服务", tags: "Lab,IPv6", price: -1, expires_at: null, mem_total: 16 * 1024 ** 3, mem_used: 6.72 * 1024 ** 3, cpu: 52, uptime: 134 * 86400 }),
|
||||
node(5, { id: "taipei-homelab", name: "台北 HomeLab", region: "TW", group_name: "家庭网络", tags: "HomeLab,自建", price: -1, expires_at: null, mem_total: 24 * 1024 ** 3, mem_used: 10.08 * 1024 ** 3, cpu: 59, uptime: 122 * 86400 }),
|
||||
node(6, { id: "london-archive", name: "伦敦 Archive", region: "GB", group_name: "存储服务", tags: "Archive,HDD", price: 18, currency: "EUR", billing_cycle: 365, expires_at: now + 110 * 86400, disk_total: 4 * 1024 ** 4, disk_used: 2.7 * 1024 ** 4, cpu: 66, latency: [] }),
|
||||
node(7, { id: "toronto-standby", name: "多伦多 Standby", region: "CA", group_name: "备用节点", tags: "Standby", price: 16, currency: "CAD", expires_at: now + 18 * 86400, timestamp: now - 640, cpu: 0, net_in: 0, net_out: 0, message: "节点维护中" }),
|
||||
node({ id: "hongkong-edge", name: "香港 Edge 01", region: "HK", group_name: "边缘网络", tags: "主力,线路:BGP,用途:网站", price: 128, expires_at: now + 46 * 86400, traffic_limit: 2 * 1024 ** 4 }),
|
||||
node({ id: "tokyo-core", name: "东京 Core", region: "JP", group_name: "核心服务", tags: "主力,线路:Premium", price: 9.9, currency: "USD", expires_at: now + 46 * 86400, mem_total: 16 * 1024 ** 3, mem_used: 6.72 * 1024 ** 3, disk_total: 224 * 1024 ** 3, disk_used: 69.44 * 1024 ** 3, traffic_limit: 2 * 1024 ** 4, net_rx_total: 810 * 1024 ** 3, net_tx_total: 310 * 1024 ** 3, cpu: 31 }),
|
||||
node({ id: "singapore-data", name: "新加坡 Data", region: "SG", group_name: "数据服务", tags: "数据库,NVMe", price: 88, expires_at: now + 46 * 86400, mem_total: 24 * 1024 ** 3, mem_used: 10.08 * 1024 ** 3, disk_total: 288 * 1024 ** 3, disk_used: 89.28 * 1024 ** 3, traffic_limit: 2 * 1024 ** 4, net_rx_total: 980 * 1024 ** 3, net_tx_total: 400 * 1024 ** 3, cpu: 38 }),
|
||||
node({ id: "los-angeles-west", name: "洛杉矶 West", region: "US", group_name: "边缘网络", tags: "备用,线路:CN2", price: 24, currency: "USD", expires_at: now + 46 * 86400, disk_total: 352 * 1024 ** 3, disk_used: 109.12 * 1024 ** 3, traffic_limit: 2 * 1024 ** 4, net_rx_total: 1.12 * 1024 ** 4, net_tx_total: 490 * 1024 ** 3, cpu: 45 }),
|
||||
node({ id: "frankfurt-lab", name: "法兰克福 Lab", region: "DE", group_name: "实验服务", tags: "Lab,IPv6", price: -1, expires_at: null, mem_total: 16 * 1024 ** 3, mem_used: 6.72 * 1024 ** 3, cpu: 52, uptime: 134 * 86400 }),
|
||||
node({ id: "taipei-homelab", name: "台北 HomeLab", region: "TW", group_name: "家庭网络", tags: "HomeLab,自建", price: -1, expires_at: null, mem_total: 24 * 1024 ** 3, mem_used: 10.08 * 1024 ** 3, cpu: 59, uptime: 122 * 86400 }),
|
||||
node({ id: "london-archive", name: "伦敦 Archive", region: "GB", group_name: "存储服务", tags: "Archive,HDD", price: 18, currency: "EUR", billing_cycle: 365, expires_at: now + 110 * 86400, disk_total: 4 * 1024 ** 4, disk_used: 2.7 * 1024 ** 4, cpu: 66, latency: [] }),
|
||||
node({ id: "toronto-standby", name: "多伦多 Standby", region: "CA", group_name: "备用节点", tags: "Standby", price: 16, currency: "CAD", expires_at: now + 18 * 86400, timestamp: now - 640, cpu: 0, net_in: 0, net_out: 0, message: "节点维护中" }),
|
||||
];
|
||||
|
||||
export function demoHistory(serverId: string, hours: number): HistoryPoint[] {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
const CLIENT_PASSWORD_ROUNDS = 600_000;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
function toHex(bytes: Uint8Array) {
|
||||
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
export async function derivePassword(password: string, deploymentSalt: string) {
|
||||
if (!deploymentSalt) throw new Error("登录密码派生参数缺失");
|
||||
if (!globalThis.crypto?.subtle) throw new Error("当前浏览器不支持安全密码派生");
|
||||
const key = await globalThis.crypto.subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(password),
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveBits"],
|
||||
);
|
||||
const bits = await globalThis.crypto.subtle.deriveBits(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-256",
|
||||
iterations: CLIENT_PASSWORD_ROUNDS,
|
||||
salt: encoder.encode(`nodeflare:${deploymentSalt}`),
|
||||
},
|
||||
key,
|
||||
256,
|
||||
);
|
||||
return toHex(new Uint8Array(bits));
|
||||
}
|
||||
+28
-9
@@ -333,11 +333,24 @@ label > span { color: var(--muted-foreground); font-size: 9px; }
|
||||
.usage-table tbody tr:last-child > * { border-bottom: 0; }
|
||||
.usage-empty { display: flex; min-height: 48px; align-items: center; justify-content: center; border: 1px dashed var(--border); border-radius: 7px; color: var(--muted-foreground); font-size: 9px; }
|
||||
.data-actions { border-top: 1px solid var(--border); padding-top: 14px; }
|
||||
.builtin-theme-row { display: flex; min-height: 64px; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid var(--border); border-radius: 7px; padding: 11px 13px; }
|
||||
.builtin-theme-row > div { display: grid; gap: 4px; }
|
||||
.builtin-theme-row strong { font-size: 12px; }
|
||||
.builtin-theme-row span { color: var(--muted-foreground); font-size: 9px; }
|
||||
.builtin-theme-row b { border: 1px solid color-mix(in srgb, var(--primary) 35%, var(--border)); border-radius: 7px; padding: 3px 7px; color: var(--primary); font-size: 9px; }
|
||||
.theme-store-page { display: grid; gap: 16px; }
|
||||
.theme-store-grid { display: grid; gap: 9px; }
|
||||
.theme-store-card { display: grid; min-width: 0; align-items: center; gap: 12px; border: 1px solid var(--border); border-radius: 7px; padding: 13px; grid-template-columns: 38px minmax(0, 1fr) auto; }
|
||||
.theme-store-card.active { border-color: color-mix(in srgb, var(--primary) 55%, var(--border)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 18%, transparent); }
|
||||
.theme-card-icon { display: inline-flex; width: 38px; height: 38px; align-items: center; justify-content: center; border-radius: 8px; color: var(--primary); background: color-mix(in srgb, var(--primary) 10%, transparent); }
|
||||
.theme-card-main { display: grid; min-width: 0; gap: 5px; }
|
||||
.theme-card-title { display: flex; min-width: 0; align-items: center; gap: 7px; }
|
||||
.theme-card-title strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.theme-card-title .theme-badge { border-radius: 999px; padding: 2px 6px; font-size: 9px; white-space: nowrap; }
|
||||
.theme-badge.builtin { color: var(--primary); background: color-mix(in srgb, var(--primary) 10%, transparent); }
|
||||
.theme-badge.remote { color: var(--info); background: color-mix(in srgb, var(--info) 10%, transparent); }
|
||||
.theme-card-main p, .theme-card-main small { margin: 0; overflow: hidden; color: var(--muted-foreground); font-size: 10px; line-height: 1.4; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.theme-card-main a { display: inline-flex; min-width: 0; align-items: center; gap: 4px; overflow: hidden; color: var(--info); font-size: 10px; text-decoration: none; }
|
||||
.theme-card-main a span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.theme-card-actions { display: flex; align-items: center; gap: 5px; }
|
||||
.theme-active-btn { display: inline-flex; min-height: 30px; align-items: center; gap: 4px; border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border)); border-radius: 6px; padding: 0 9px; color: var(--primary); background: color-mix(in srgb, var(--primary) 8%, transparent); font-size: 11px; font-weight: 600; }
|
||||
.theme-risk-note { border-left: 3px solid var(--warning); padding: 7px 10px; color: var(--muted-foreground); background: color-mix(in srgb, var(--warning) 7%, transparent); font-size: 11px; line-height: 1.5; }
|
||||
.theme-add-form code { border-radius: 3px; padding: 1px 3px; color: var(--foreground); background: color-mix(in srgb, var(--foreground) 8%, transparent); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92em; }
|
||||
.theme-option-grid { display: grid; align-items: start; gap: 12px; grid-template-columns: 1fr 1fr; }
|
||||
.theme-option-empty { display: grid; min-height: 74px; place-items: center; border: 1px dashed var(--border); border-radius: 7px; color: var(--muted-foreground); font-size: 10px; }
|
||||
.theme-color-option input { width: 100%; min-height: 40px; padding: 4px; cursor: pointer; }
|
||||
@@ -450,6 +463,9 @@ label > span { color: var(--muted-foreground); font-size: 9px; }
|
||||
.server-usage { display: none; }
|
||||
.server-row .row-actions { grid-column: 4 / 6; justify-self: end; }
|
||||
.form-grid, .form-grid.three, .settings-toggles, .theme-option-grid { grid-template-columns: 1fr; }
|
||||
.theme-store-card { grid-template-columns: 32px minmax(0, 1fr); }
|
||||
.theme-card-icon { width: 32px; height: 32px; }
|
||||
.theme-card-actions { grid-column: 2; justify-content: flex-start; }
|
||||
.data-stat-grid { grid-template-columns: 1fr 1fr 1fr; }
|
||||
.usage-table th, .usage-table td { padding-inline: 5px; font-size: 8px; }
|
||||
.editor-modal, .install-modal { padding: 15px 13px; }
|
||||
@@ -564,10 +580,13 @@ label > span { color: var(--muted-foreground); font-size: 9px; }
|
||||
.usage-table th, .usage-table td { height: 40px; color: var(--admin-text); font-size: 12px; }
|
||||
.usage-table thead th { color: var(--admin-muted); background: var(--admin-bg); }
|
||||
.usage-empty { color: var(--admin-muted); font-size: 13px; }
|
||||
.builtin-theme-row { border-color: var(--admin-border); }
|
||||
.builtin-theme-row strong { color: var(--admin-text); font-size: 14px; }
|
||||
.builtin-theme-row span { color: var(--admin-muted); font-size: 12px; }
|
||||
.builtin-theme-row b { font-size: 11px; }
|
||||
.admin-page .theme-store-card { border-color: var(--admin-border); background: var(--admin-surface); }
|
||||
.admin-page .theme-store-card.active { border-color: var(--admin-accent-border); box-shadow: 0 0 0 1px color-mix(in srgb, var(--admin-accent) 18%, transparent); }
|
||||
.admin-page .theme-card-title strong, .admin-page .theme-card-main small { color: var(--admin-text); }
|
||||
.admin-page .theme-card-main p { color: var(--admin-muted); }
|
||||
.admin-page .theme-card-main a { color: var(--admin-accent); }
|
||||
.admin-page .theme-active-btn { border-color: var(--admin-accent-border); color: var(--admin-accent); background: var(--admin-active-bg); }
|
||||
.admin-page .theme-risk-note { color: var(--admin-muted); }
|
||||
.admin-page .theme-option-grid > .toggle-row { min-height: 40px; align-self: end; padding-block: 7px; }
|
||||
.theme-option-empty { color: var(--admin-muted); font-size: 13px; }
|
||||
.editor-modal, .install-modal, .latency-editor { border: 1px solid var(--admin-border); color: var(--admin-text); background: var(--admin-surface); }
|
||||
|
||||
+26
-21
@@ -8,6 +8,7 @@ export interface Config {
|
||||
offline_threshold_seconds: number;
|
||||
history_retention_days: number;
|
||||
default_theme: "system" | "light" | "dark";
|
||||
active_theme_id: string;
|
||||
background_url: string;
|
||||
theme_options: Record<string, ThemeSettingValue>;
|
||||
show_search: boolean;
|
||||
@@ -23,6 +24,7 @@ export interface Config {
|
||||
turnstile_enabled: boolean;
|
||||
turnstile_login_enabled: boolean;
|
||||
turnstile_site_key: string;
|
||||
password_client_salt: string;
|
||||
websocket: boolean;
|
||||
}
|
||||
|
||||
@@ -56,6 +58,15 @@ export interface ThemeSettingsSchema {
|
||||
settings: ThemeSettingField[];
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
builtin: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface ExchangeRates {
|
||||
base: "CNY";
|
||||
rates: Record<string, number>;
|
||||
@@ -113,10 +124,7 @@ export interface LatencyTask {
|
||||
target: string;
|
||||
interval_seconds: number;
|
||||
default_enabled: boolean;
|
||||
sort_order: number;
|
||||
server_ids: string[];
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export type LatencyTaskInput = Pick<LatencyTask, "name" | "task_type" | "target" | "interval_seconds" | "default_enabled" | "server_ids">;
|
||||
@@ -127,9 +135,6 @@ export interface Server {
|
||||
region: string;
|
||||
group_name: string;
|
||||
tags: string;
|
||||
note: string;
|
||||
hidden: number;
|
||||
sort_order: number;
|
||||
expires_at: number | null;
|
||||
traffic_limit: number;
|
||||
traffic_limit_type: TrafficLimitType;
|
||||
@@ -138,16 +143,7 @@ export interface Server {
|
||||
currency: string;
|
||||
auto_renewal: number;
|
||||
public_remark: string;
|
||||
network_interface: string;
|
||||
reset_day: number;
|
||||
report_interval: number;
|
||||
collect_interval: number;
|
||||
rx_correction: number;
|
||||
tx_correction: number;
|
||||
offline_notify_disabled: number;
|
||||
auto_update: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
timestamp: number | null;
|
||||
cpu: number | null;
|
||||
load1: number | null;
|
||||
@@ -173,8 +169,6 @@ export interface Server {
|
||||
kernel: string | null;
|
||||
arch: string | null;
|
||||
virtualization: string | null;
|
||||
ipv4: string | null;
|
||||
ipv6: string | null;
|
||||
gpu_usage: number | null;
|
||||
gpu_model: string | null;
|
||||
agent_version: string | null;
|
||||
@@ -190,6 +184,18 @@ export interface Server {
|
||||
latency?: LatencySample[];
|
||||
}
|
||||
|
||||
export interface AdminServer extends Server {
|
||||
note: string;
|
||||
hidden: number;
|
||||
network_interface: string;
|
||||
report_interval: number;
|
||||
collect_interval: number;
|
||||
rx_correction: number;
|
||||
tx_correction: number;
|
||||
offline_notify_disabled: number;
|
||||
auto_update: number;
|
||||
}
|
||||
|
||||
export interface HistoryPoint {
|
||||
timestamp: number;
|
||||
cpu: number;
|
||||
@@ -243,10 +249,11 @@ export interface ServerInput {
|
||||
auto_update: boolean;
|
||||
}
|
||||
|
||||
export interface Settings extends Omit<Config, "websocket"> {
|
||||
export interface Settings extends Omit<Config, "websocket" | "password_client_salt"> {
|
||||
admin_username: string;
|
||||
admin_password_configured: boolean;
|
||||
new_password?: string;
|
||||
new_password_derived?: string;
|
||||
turnstile_secret_key: string;
|
||||
notification_enabled: boolean;
|
||||
notification_endpoint: string;
|
||||
@@ -269,11 +276,9 @@ export interface AlertRule {
|
||||
aggregation: AlertAggregation;
|
||||
enabled: number;
|
||||
server_ids: string[];
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface AlertRuleInput extends Omit<AlertRule, "id" | "enabled" | "created_at" | "updated_at"> {
|
||||
export interface AlertRuleInput extends Omit<AlertRule, "id" | "enabled"> {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,8 +63,6 @@ CREATE TABLE IF NOT EXISTS latest_metrics (
|
||||
kernel TEXT NOT NULL DEFAULT '',
|
||||
arch TEXT NOT NULL DEFAULT '',
|
||||
virtualization TEXT NOT NULL DEFAULT '',
|
||||
ipv4 TEXT NOT NULL DEFAULT '',
|
||||
ipv6 TEXT NOT NULL DEFAULT '',
|
||||
gpu_usage REAL NOT NULL DEFAULT 0,
|
||||
gpu_model TEXT NOT NULL DEFAULT '',
|
||||
agent_version TEXT NOT NULL DEFAULT '',
|
||||
@@ -152,7 +150,9 @@ INSERT OR IGNORE INTO settings(key, value, updated_at) VALUES
|
||||
('favicon_url', '', unixepoch()),
|
||||
('locale', 'zh-CN', unixepoch()),
|
||||
('public_dashboard', 'true', unixepoch()),
|
||||
('history_cache_version', '0', unixepoch()),
|
||||
('default_theme', 'system', unixepoch()),
|
||||
('active_theme_id', 'builtin-komari-glass', unixepoch()),
|
||||
('background_url', '', unixepoch()),
|
||||
('theme_options', '{}', unixepoch()),
|
||||
('show_search', 'true', unixepoch()),
|
||||
@@ -167,6 +167,7 @@ INSERT OR IGNORE INTO settings(key, value, updated_at) VALUES
|
||||
('show_uptime', 'true', unixepoch()),
|
||||
('admin_username', '', unixepoch()),
|
||||
('admin_password_hash', '', unixepoch()),
|
||||
('password_client_salt', lower(hex(randomblob(16))), unixepoch()),
|
||||
('turnstile_enabled', 'false', unixepoch()),
|
||||
('turnstile_login_enabled', 'true', unixepoch()),
|
||||
('turnstile_site_key', '', unixepoch()),
|
||||
@@ -188,6 +189,17 @@ CREATE TABLE IF NOT EXISTS exchange_rate_snapshots (
|
||||
attempted_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS themes (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
url TEXT NOT NULL UNIQUE,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_themes_created
|
||||
ON themes(created_at DESC);
|
||||
|
||||
INSERT OR IGNORE INTO exchange_rate_snapshots (
|
||||
base_currency, rates_json, source, rate_date, fetched_at, attempted_at
|
||||
) VALUES (
|
||||
|
||||
+1
-1
@@ -19,6 +19,6 @@
|
||||
"test:smoke": "sh scripts/smoke-test.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.120.0"
|
||||
"wrangler": "^4.120.1"
|
||||
}
|
||||
}
|
||||
+44
-8
@@ -17,7 +17,18 @@ request() {
|
||||
monitor_curl --fail --location --silent --show-error "$@"
|
||||
}
|
||||
|
||||
request "$MONITOR_BASE_URL/api/config" | jq -e '.site_name | length > 0' >/dev/null
|
||||
config_json=$(request "$MONITOR_BASE_URL/api/config")
|
||||
printf '%s' "$config_json" | jq -e '.site_name | length > 0' >/dev/null
|
||||
password_client_salt=$(printf '%s' "$config_json" | jq -er '.password_client_salt | select(length > 0)')
|
||||
password_derived=$(NODEFLARE_PASSWORD="$MONITOR_ADMIN_PASSWORD" NODEFLARE_SALT="$password_client_salt" bun -e '
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey("raw", encoder.encode(process.env.NODEFLARE_PASSWORD), "PBKDF2", false, ["deriveBits"]);
|
||||
const bits = await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", iterations: 600000, salt: encoder.encode(`nodeflare:${process.env.NODEFLARE_SALT}`) }, key, 256);
|
||||
console.log(Array.from(new Uint8Array(bits), byte => byte.toString(16).padStart(2, "0")).join(""));
|
||||
')
|
||||
cross_origin_status=$(monitor_curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
-H 'Origin: https://not-nodeflare.invalid' "$MONITOR_BASE_URL/api/config")
|
||||
[ "$cross_origin_status" = "403" ]
|
||||
|
||||
admin_html=$(request "$MONITOR_BASE_URL/admin")
|
||||
case "$admin_html" in
|
||||
@@ -38,13 +49,28 @@ request "$MONITOR_BASE_URL/admin-assets/admin.js" >/dev/null
|
||||
request "$MONITOR_BASE_URL/admin-assets/admin.css" >/dev/null
|
||||
|
||||
login_json=$(request -H 'Content-Type: application/json' \
|
||||
--data "$(jq -nc --arg username "$MONITOR_ADMIN_USERNAME" --arg password "$MONITOR_ADMIN_PASSWORD" --arg turnstile_token "$MONITOR_TURNSTILE_TOKEN" '{username:$username,password:$password,turnstile_token:$turnstile_token}')" \
|
||||
--data "$(jq -nc --arg username "$MONITOR_ADMIN_USERNAME" --arg password "$MONITOR_ADMIN_PASSWORD" --arg password_derived "$password_derived" --arg turnstile_token "$MONITOR_TURNSTILE_TOKEN" '{username:$username,password:$password,password_derived:$password_derived,turnstile_token:$turnstile_token}')" \
|
||||
"$MONITOR_BASE_URL/api/admin/login")
|
||||
admin_token=$(printf '%s' "$login_json" | jq -er '.token')
|
||||
|
||||
incomplete_server_status=$(monitor_curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
-H "Authorization: Bearer $admin_token" -H 'Content-Type: application/json' \
|
||||
--data '{"name":"incomplete"}' "$MONITOR_BASE_URL/api/admin/servers")
|
||||
[ "$incomplete_server_status" = "400" ]
|
||||
unknown_setting_status=$(monitor_curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
-X PATCH -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/json' \
|
||||
--data '{"unexpected":true}' "$MONITOR_BASE_URL/api/admin/settings")
|
||||
[ "$unknown_setting_status" = "400" ]
|
||||
|
||||
request -H "Authorization: Bearer $admin_token" \
|
||||
"$MONITOR_BASE_URL/api/exchange-rates" | \
|
||||
jq -e '.base == "CNY" and .rates.CNY == 1 and .rates.USD > 0 and .rates.CAD > 0 and .cny.USD == .rates.USD' >/dev/null
|
||||
jq -e '.base == "CNY" and .rates.CNY == 1 and .rates.USD > 0 and .rates.CAD > 0 and (has("cny") | not)' >/dev/null
|
||||
|
||||
request -H "Authorization: Bearer $admin_token" \
|
||||
"$MONITOR_BASE_URL/api/admin/themes" | \
|
||||
jq -e '.themes | any(.builtin == true and .id == "builtin-komari-glass" and .active == true)' >/dev/null
|
||||
|
||||
server_input='{"name":"Smoke Test Node","region":"JP","group_name":"Test","tags":"smoke","note":"private","hidden":false,"expires_at":1893456000,"traffic_limit":107374182400,"traffic_limit_type":"max","price":9.9,"billing_cycle":30,"currency":"USD","auto_renewal":true,"public_remark":"public","network_interface":"","reset_day":1,"report_interval":60,"collect_interval":5,"rx_correction":0,"tx_correction":0,"offline_notify_disabled":false,"auto_update":true}'
|
||||
|
||||
server_id=
|
||||
latency_task_id=
|
||||
@@ -73,15 +99,21 @@ latency_task_id=$(printf '%s' "$latency_task_json" | jq -er '.id')
|
||||
|
||||
server_json=$(request -H "Authorization: Bearer $admin_token" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data '{"name":"Smoke Test Node","region":"JP","group_name":"Test","tags":"smoke","note":"private","public_remark":"public","hidden":false,"expires_at":1893456000,"traffic_limit":107374182400,"traffic_limit_type":"max","price":9.9,"billing_cycle":30,"currency":"USD","auto_renewal":true}' \
|
||||
--data "$server_input" \
|
||||
"$MONITOR_BASE_URL/api/admin/servers")
|
||||
server_id=$(printf '%s' "$server_json" | jq -er '.id')
|
||||
agent_token=$(printf '%s' "$server_json" | jq -er '.agent_token')
|
||||
agent_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' agent/Cargo.toml | head -1)
|
||||
|
||||
incomplete_report_status=$(monitor_curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
-H "Authorization: Bearer $agent_token" -H 'Content-Type: application/json' \
|
||||
--data "$(jq -nc --arg server_id "$server_id" '{server_id:$server_id,samples:[{timestamp:(now|floor),cpu:1}]}')" \
|
||||
"$MONITOR_BASE_URL/api/agent/report")
|
||||
[ "$incomplete_report_status" = "400" ]
|
||||
|
||||
sample=$(jq -nc --arg agent_version "$agent_version" '{
|
||||
timestamp:(now|floor),cpu:18.5,load1:0.42,load5:0.36,load15:0.31,
|
||||
mem_used:2147483648,mem_total:4294967296,disk_used:21474836480,disk_total:53687091200,
|
||||
mem_used:2147483648,mem_total:4294967296,swap_used:0,swap_total:0,disk_used:21474836480,disk_total:53687091200,
|
||||
net_in:4096,net_out:2048,net_rx_total:1073741824,net_tx_total:536870912,
|
||||
uptime:86400,processes:90,tcp_connections:18,udp_connections:4,cpu_cores:2,
|
||||
cpu_model:"Smoke CPU",os:"Debian 12",kernel:"6.1",arch:"x86_64",virtualization:"kvm",
|
||||
@@ -89,7 +121,7 @@ sample=$(jq -nc --arg agent_version "$agent_version" '{
|
||||
disk_read_iops:120,disk_write_iops:48,disk_await_ms:1.4,disk_utilization:8.2,
|
||||
disks:[{name:"/dev/vda1",mount_point:"/",used:21474836480,total:53687091200,read_bps:4194304,write_bps:2097152,read_iops:120,write_iops:48,await_ms:1.4,utilization:8.2}],
|
||||
gpus:[{model:"NVIDIA T4",usage:32.5,memory_used:1073741824,memory_total:17179869184}],
|
||||
agent_version:$agent_version,message:"smoke"
|
||||
agent_version:$agent_version,message:"smoke",latency_results:[]
|
||||
}')
|
||||
report=$(jq -nc --arg server_id "$server_id" --argjson sample "$sample" '{server_id:$server_id,samples:[$sample]}')
|
||||
report_response=$(request -H "Authorization: Bearer $agent_token" -H 'Content-Type: application/json' \
|
||||
@@ -102,8 +134,12 @@ latency_report=$(printf '%s' "$report" | jq --arg task_id "$latency_task_id" '.s
|
||||
request -H "Authorization: Bearer $agent_token" -H 'Content-Type: application/json' \
|
||||
--data "$latency_report" "$MONITOR_BASE_URL/api/agent/report" | jq -e '.success == true' >/dev/null
|
||||
|
||||
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/servers/$server_id" | jq -e --arg task_id "$latency_task_id" '.cpu == 18.5 and .gpu_usage == 32.5 and .disk_await_ms == 1.4 and (.gpus | length) == 1 and (.disks | length) == 1 and .disk_used == 21474836480 and .traffic_limit == 107374182400 and .price == 9.9 and .note == "" and .public_remark == "public" and (.latency | any(.task_id == $task_id and .latency_ms == 28.4 and .packet_loss == 25))' >/dev/null
|
||||
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/servers/$server_id" | jq -e --arg task_id "$latency_task_id" '.cpu == 18.5 and .gpu_usage == 32.5 and .disk_await_ms == 1.4 and (.gpus | length) == 1 and (.disks | length) == 1 and .disk_used == 21474836480 and .traffic_limit == 107374182400 and .price == 9.9 and (has("note") | not) and .public_remark == "public" and (.latency | any(.task_id == $task_id and .latency_ms == 28.4 and .packet_loss == 25))' >/dev/null
|
||||
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/history/$server_id?hours=1" | jq -e '.points | length >= 1 and any(.gpu_usage == 32.5)' >/dev/null
|
||||
history_cache_header=$(monitor_curl --silent --dump-header - --output /dev/null \
|
||||
-H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/history/$server_id?hours=1" | \
|
||||
tr -d '\r' | awk -F ': ' 'tolower($1) == "x-cache" { print $2 }' | tail -1)
|
||||
[ "$history_cache_header" = "HIT" ]
|
||||
request -H "Authorization: Bearer $admin_token" "$MONITOR_BASE_URL/api/latency/$server_id?hours=1" | jq -e --arg task_id "$latency_task_id" '(.tasks | any(.id == $task_id)) and (.points | any(.task_id == $task_id and .latency_ms == 28.4))' >/dev/null
|
||||
alert_rule_json=$(request -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/json' \
|
||||
--data "$(jq -nc --arg server_id "$server_id" '{name:"Smoke CPU",metric:"cpu",threshold:80,duration_minutes:5,aggregation:"average",enabled:true,server_ids:[$server_id]}')" \
|
||||
@@ -122,7 +158,7 @@ request -H "Authorization: Bearer $rotated_token" -H 'Content-Type: application/
|
||||
--data "$latency_report" "$MONITOR_BASE_URL/api/agent/report" | jq -e '.success == true' >/dev/null
|
||||
|
||||
request -H "Authorization: Bearer $admin_token" -H 'Content-Type: application/json' -X PATCH \
|
||||
--data '{"name":"Smoke Test Node","region":"JP","group_name":"Test","tags":"smoke","note":"private","public_remark":"public","hidden":true,"expires_at":1893456000,"traffic_limit":107374182400,"traffic_limit_type":"max","price":9.9,"billing_cycle":30,"currency":"USD","auto_renewal":true}' \
|
||||
--data "$(printf '%s' "$server_input" | jq '.hidden=true')" \
|
||||
"$MONITOR_BASE_URL/api/admin/servers/$server_id" | jq -e '.success == true' >/dev/null
|
||||
hidden_detail_status=$(monitor_curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
-H "Authorization: Bearer $admin_token" \
|
||||
|
||||
+110
-18
@@ -1,12 +1,18 @@
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use hmac::{Hmac, Mac};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use pbkdf2::pbkdf2_hmac;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use worker::{Date, Env, Request};
|
||||
use worker::{
|
||||
js_sys::{Function, Reflect, Uint8Array},
|
||||
wasm_bindgen::{JsCast, JsValue},
|
||||
Date, Env, Request,
|
||||
};
|
||||
|
||||
const SESSION_SECONDS: i64 = 7 * 24 * 60 * 60;
|
||||
const TURNSTILE_PROOF_SECONDS: i64 = 60 * 60;
|
||||
const PASSWORD_ROUNDS: usize = 10_000;
|
||||
const THEME_PREVIEW_SECONDS: i64 = 10 * 60;
|
||||
const SERVER_PASSWORD_ROUNDS: u32 = 10_000;
|
||||
const JWT_HEADER: &str = r#"{"alg":"HS256","typ":"JWT"}"#;
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
@@ -47,36 +53,63 @@ pub fn admin_username(env: &Env, configured: &str) -> String {
|
||||
.unwrap_or_else(|_| "admin".to_string())
|
||||
}
|
||||
|
||||
fn password_digest(password: &str, salt: &str) -> String {
|
||||
let mut digest = Sha256::digest(format!("nodeflare:{salt}:{password}").as_bytes()).to_vec();
|
||||
for _ in 1..PASSWORD_ROUNDS {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&digest);
|
||||
hasher.update(salt.as_bytes());
|
||||
digest = hasher.finalize().to_vec();
|
||||
}
|
||||
fn password_digest(password_derived: &str, salt: &str, rounds: u32) -> String {
|
||||
let mut digest = [0_u8; 32];
|
||||
pbkdf2_hmac::<Sha256>(
|
||||
password_derived.as_bytes(),
|
||||
salt.as_bytes(),
|
||||
rounds,
|
||||
&mut digest,
|
||||
);
|
||||
digest.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
pub fn hash_password(password: &str, salt: &str) -> String {
|
||||
format!("sha256${salt}${}", password_digest(password, salt))
|
||||
pub fn hash_password(password_derived: &str, salt: &str) -> String {
|
||||
format!(
|
||||
"pbkdf2_sha256_client_v1${SERVER_PASSWORD_ROUNDS}${salt}${}",
|
||||
password_digest(password_derived, salt, SERVER_PASSWORD_ROUNDS)
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_password_hash(password: &str, encoded: &str) -> bool {
|
||||
pub fn random_salt() -> Option<String> {
|
||||
let global = worker::js_sys::global();
|
||||
let crypto = Reflect::get(&global, &JsValue::from_str("crypto")).ok()?;
|
||||
let get_random_values = Reflect::get(&crypto, &JsValue::from_str("getRandomValues"))
|
||||
.ok()?
|
||||
.dyn_into::<Function>()
|
||||
.ok()?;
|
||||
let salt = Uint8Array::new_with_length(16);
|
||||
get_random_values.call1(&crypto, &salt).ok()?;
|
||||
Some(
|
||||
salt.to_vec()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_password_hash(password_derived: &str, encoded: &str) -> bool {
|
||||
let mut parts = encoded.split('$');
|
||||
let Some(algorithm) = parts.next() else {
|
||||
return false;
|
||||
};
|
||||
let Some(rounds) = parts.next().and_then(|value| value.parse::<u32>().ok()) else {
|
||||
return false;
|
||||
};
|
||||
let Some(salt) = parts.next() else {
|
||||
return false;
|
||||
};
|
||||
let Some(expected) = parts.next() else {
|
||||
return false;
|
||||
};
|
||||
if algorithm != "sha256" || salt.is_empty() || parts.next().is_some() {
|
||||
if algorithm != "pbkdf2_sha256_client_v1"
|
||||
|| rounds != SERVER_PASSWORD_ROUNDS
|
||||
|| salt.len() < 32
|
||||
|| parts.next().is_some()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
secure_eq(&password_digest(password, salt), expected)
|
||||
secure_eq(&password_digest(password_derived, salt, rounds), expected)
|
||||
}
|
||||
|
||||
pub fn verify_credentials(
|
||||
@@ -85,6 +118,7 @@ pub fn verify_credentials(
|
||||
password_hash: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
password_derived: &str,
|
||||
) -> bool {
|
||||
let expected_username = admin_username(env, configured_username);
|
||||
let username_valid = secure_eq(&expected_username, username.trim());
|
||||
@@ -93,7 +127,7 @@ pub fn verify_credentials(
|
||||
.map(|expected| secure_eq(&expected, password))
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
verify_password_hash(password, password_hash)
|
||||
verify_password_hash(password_derived, password_hash)
|
||||
};
|
||||
username_valid && password_valid
|
||||
}
|
||||
@@ -198,6 +232,43 @@ pub fn verify_turnstile_proof(value: &str, env: &Env, password_hash: &str) -> bo
|
||||
secure_eq(&expected, signature)
|
||||
}
|
||||
|
||||
pub fn create_theme_preview_proof(
|
||||
env: &Env,
|
||||
password_hash: &str,
|
||||
theme_id: &str,
|
||||
) -> Option<String> {
|
||||
let secret = signing_secret(env, password_hash)?;
|
||||
let expires = Date::now().as_millis() as i64 / 1000 + THEME_PREVIEW_SECONDS;
|
||||
let payload = format!("{theme_id}.{expires}");
|
||||
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).ok()?;
|
||||
mac.update(format!("nodeflare-theme-preview:{payload}").as_bytes());
|
||||
let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
|
||||
Some(format!("{payload}.{signature}"))
|
||||
}
|
||||
|
||||
pub fn verify_theme_preview_proof(value: &str, env: &Env, password_hash: &str) -> Option<String> {
|
||||
let secret = signing_secret(env, password_hash)?;
|
||||
let mut parts = value.split('.');
|
||||
let (Some(theme_id), Some(expires), Some(signature), None) =
|
||||
(parts.next(), parts.next(), parts.next(), parts.next())
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if theme_id.is_empty() || theme_id.len() > 80 || theme_id.contains('/') {
|
||||
return None;
|
||||
}
|
||||
let expires = expires.parse::<i64>().ok()?;
|
||||
let current = Date::now().as_millis() as i64 / 1000;
|
||||
if expires < current || expires > current + THEME_PREVIEW_SECONDS + 60 {
|
||||
return None;
|
||||
}
|
||||
let signature = URL_SAFE_NO_PAD.decode(signature).ok()?;
|
||||
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).ok()?;
|
||||
mac.update(format!("nodeflare-theme-preview:{theme_id}.{expires}").as_bytes());
|
||||
mac.verify_slice(&signature).ok()?;
|
||||
Some(theme_id.to_string())
|
||||
}
|
||||
|
||||
pub fn bearer_token(req: &Request) -> Option<String> {
|
||||
req.headers()
|
||||
.get("Authorization")
|
||||
@@ -208,7 +279,28 @@ pub fn bearer_token(req: &Request) -> Option<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{create_admin_jwt_with_secret, verify_admin_jwt, SESSION_SECONDS};
|
||||
use super::{
|
||||
create_admin_jwt_with_secret, hash_password, verify_admin_jwt, verify_password_hash,
|
||||
SESSION_SECONDS,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn hashes_passwords_with_pbkdf2() {
|
||||
let encoded = hash_password(
|
||||
"correct horse battery staple",
|
||||
"0123456789abcdef0123456789abcdef",
|
||||
);
|
||||
assert!(encoded.starts_with("pbkdf2_sha256_client_v1$10000$"));
|
||||
assert!(verify_password_hash(
|
||||
"correct horse battery staple",
|
||||
&encoded
|
||||
));
|
||||
assert!(!verify_password_hash("wrong", &encoded));
|
||||
assert!(!verify_password_hash(
|
||||
"correct horse battery staple",
|
||||
"sha256$0123456789abcdef0123456789abcdef$invalid"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signs_and_verifies_admin_jwt() {
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use worker::{wasm_bindgen::JsValue, D1Database, Date, Result};
|
||||
|
||||
use crate::models::{
|
||||
AgentConfigView, AgentReport, AlertRuleInput, AlertRuleView, HistoryPoint, ServerInput,
|
||||
ServerView, SettingsInput, TokenHashRow,
|
||||
ServerView, SettingsInput, ThemeInput, ThemeView, TokenHashRow,
|
||||
};
|
||||
|
||||
const SERVER_SELECT: &str = r#"
|
||||
SELECT
|
||||
s.id, s.name, s.region, s.group_name, s.tags, s.note, s.hidden,
|
||||
s.sort_order, s.expires_at, s.traffic_limit, s.traffic_limit_type,
|
||||
s.expires_at, s.traffic_limit, s.traffic_limit_type,
|
||||
s.price, s.billing_cycle, s.currency, s.auto_renewal, s.public_remark,
|
||||
s.network_interface, s.reset_day, s.report_interval, s.collect_interval,
|
||||
s.rx_correction, s.tx_correction, s.offline_notify_disabled, s.auto_update,
|
||||
s.created_at, s.updated_at,
|
||||
s.created_at,
|
||||
m.timestamp, m.cpu, m.load1, m.load5, m.load15, m.mem_used, m.mem_total,
|
||||
m.swap_used, m.swap_total, m.disk_used, m.disk_total, m.net_in, m.net_out,
|
||||
CASE WHEN m.net_rx_total IS NULL THEN NULL ELSE m.net_rx_total + s.rx_correction END AS net_rx_total,
|
||||
CASE WHEN m.net_tx_total IS NULL THEN NULL ELSE m.net_tx_total + s.tx_correction END AS net_tx_total,
|
||||
m.uptime, m.processes, m.tcp_connections,
|
||||
m.udp_connections, m.cpu_cores, m.cpu_model, m.os, m.kernel, m.arch,
|
||||
m.virtualization, m.ipv4, m.ipv6, m.gpu_usage, m.gpu_model, m.agent_version,
|
||||
m.virtualization, m.gpu_usage, m.gpu_model, m.agent_version,
|
||||
m.disk_read_bps, m.disk_write_bps, m.disk_read_iops, m.disk_write_iops,
|
||||
m.disk_await_ms, m.disk_utilization, m.disk_info, m.gpu_info,
|
||||
m.message
|
||||
@@ -30,12 +30,45 @@ FROM servers s
|
||||
LEFT JOIN latest_metrics m ON m.server_id = s.id
|
||||
"#;
|
||||
|
||||
pub const SECRET_MASK: &str = "********";
|
||||
|
||||
fn secret_for_api(value: &str) -> &str {
|
||||
if value.is_empty() {
|
||||
""
|
||||
} else {
|
||||
SECRET_MASK
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_secret<S>(value: &str, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(secret_for_api(value))
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct SettingRow {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ThemeRow {
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AgentConfigRow {
|
||||
report_interval: i64,
|
||||
collect_interval: i64,
|
||||
network_interface: String,
|
||||
auto_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SettingsView {
|
||||
pub site_name: String,
|
||||
@@ -46,7 +79,10 @@ pub struct SettingsView {
|
||||
pub public_dashboard: bool,
|
||||
pub offline_threshold_seconds: i64,
|
||||
pub history_retention_days: i64,
|
||||
#[serde(skip_serializing)]
|
||||
pub history_cache_version: i64,
|
||||
pub default_theme: String,
|
||||
pub active_theme_id: String,
|
||||
pub background_url: String,
|
||||
pub theme_options: serde_json::Value,
|
||||
pub show_search: bool,
|
||||
@@ -63,16 +99,21 @@ pub struct SettingsView {
|
||||
pub admin_password_configured: bool,
|
||||
#[serde(skip_serializing)]
|
||||
pub admin_password_hash: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub password_client_salt: String,
|
||||
pub turnstile_enabled: bool,
|
||||
pub turnstile_login_enabled: bool,
|
||||
pub turnstile_site_key: String,
|
||||
#[serde(serialize_with = "serialize_secret")]
|
||||
pub turnstile_secret_key: String,
|
||||
pub notification_enabled: bool,
|
||||
#[serde(serialize_with = "serialize_secret")]
|
||||
pub notification_endpoint: String,
|
||||
pub notification_target: String,
|
||||
pub offline_alert_minutes: i64,
|
||||
pub expiry_alert_days: i64,
|
||||
pub cloudflare_account_id: String,
|
||||
#[serde(serialize_with = "serialize_secret")]
|
||||
pub cloudflare_api_token: String,
|
||||
}
|
||||
|
||||
@@ -104,8 +145,6 @@ struct AlertRuleRow {
|
||||
duration_minutes: i64,
|
||||
aggregation: String,
|
||||
enabled: i64,
|
||||
created_at: i64,
|
||||
updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -118,6 +157,10 @@ pub struct AlertMetricRow {
|
||||
pub server_id: String,
|
||||
pub name: String,
|
||||
pub value: f64,
|
||||
pub sample_count: i64,
|
||||
pub first_timestamp: i64,
|
||||
pub last_timestamp: i64,
|
||||
pub report_interval: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -170,7 +213,7 @@ pub async fn get_token_hash(db: &D1Database, id: &str) -> Result<Option<TokenHas
|
||||
}
|
||||
|
||||
pub async fn agent_config(db: &D1Database, id: &str) -> Result<Option<AgentConfigView>> {
|
||||
let mut config: Option<AgentConfigView> = db
|
||||
let row: Option<AgentConfigRow> = db
|
||||
.prepare(
|
||||
"SELECT report_interval, collect_interval, network_interface, auto_update \
|
||||
FROM servers WHERE id = ?1",
|
||||
@@ -178,11 +221,16 @@ pub async fn agent_config(db: &D1Database, id: &str) -> Result<Option<AgentConfi
|
||||
.bind(&[text(id)])?
|
||||
.first(None)
|
||||
.await?;
|
||||
if let Some(config) = config.as_mut() {
|
||||
config.network_interface = config.network_interface.trim().to_string();
|
||||
config.latency_tasks = crate::latency::tasks_for_server(db, id).await?;
|
||||
}
|
||||
Ok(config)
|
||||
let Some(row) = row else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(AgentConfigView {
|
||||
report_interval: row.report_interval,
|
||||
collect_interval: row.collect_interval,
|
||||
network_interface: row.network_interface.trim().to_string(),
|
||||
auto_update: row.auto_update,
|
||||
latency_tasks: crate::latency::tasks_for_server(db, id).await?,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn create_server(
|
||||
@@ -329,8 +377,8 @@ pub async fn reorder_servers(db: &D1Database, ids: &[String]) -> Result<()> {
|
||||
pub async fn list_alert_rules(db: &D1Database) -> Result<Vec<AlertRuleView>> {
|
||||
let rows: Vec<AlertRuleRow> = db
|
||||
.prepare(
|
||||
"SELECT id, name, metric, threshold, duration_minutes, aggregation, enabled, \
|
||||
created_at, updated_at FROM alert_rules ORDER BY created_at ASC",
|
||||
"SELECT id, name, metric, threshold, duration_minutes, aggregation, enabled \
|
||||
FROM alert_rules ORDER BY created_at ASC",
|
||||
)
|
||||
.all()
|
||||
.await?
|
||||
@@ -355,8 +403,6 @@ pub async fn list_alert_rules(db: &D1Database) -> Result<Vec<AlertRuleView>> {
|
||||
aggregation: row.aggregation,
|
||||
enabled: row.enabled,
|
||||
server_ids: servers.into_iter().map(|row| row.server_id).collect(),
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
});
|
||||
}
|
||||
Ok(rules)
|
||||
@@ -453,20 +499,41 @@ pub async fn alert_metric_values(
|
||||
"AVG"
|
||||
};
|
||||
let since = current_time - rule.duration_minutes.clamp(1, 1440) * 60;
|
||||
let minimum_samples = rule.duration_minutes.clamp(1, 1440);
|
||||
let query = format!(
|
||||
"SELECT s.id AS server_id, s.name, {aggregate}({metric}) AS value \
|
||||
"SELECT s.id AS server_id, s.name, {aggregate}({metric}) AS value, \
|
||||
COUNT(*) AS sample_count, MIN(h.timestamp) AS first_timestamp, \
|
||||
MAX(h.timestamp) AS last_timestamp, s.report_interval \
|
||||
FROM servers s JOIN metric_history h ON h.server_id=s.id \
|
||||
WHERE h.timestamp >= ?1 AND s.hidden=0 AND (\
|
||||
NOT EXISTS(SELECT 1 FROM alert_rule_servers ars WHERE ars.rule_id=?2) OR \
|
||||
EXISTS(SELECT 1 FROM alert_rule_servers ars WHERE ars.rule_id=?2 AND ars.server_id=s.id)\
|
||||
) GROUP BY s.id, s.name HAVING COUNT(*) >= ?3"
|
||||
) GROUP BY s.id, s.name, s.report_interval"
|
||||
);
|
||||
db.prepare(query)
|
||||
.bind(&[number(since), text(&rule.id), number(minimum_samples)])?
|
||||
let rows: Vec<AlertMetricRow> = db
|
||||
.prepare(query)
|
||||
.bind(&[number(since), text(&rule.id)])?
|
||||
.all()
|
||||
.await?
|
||||
.results()
|
||||
.results()?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.filter(|row| alert_window_covered(row, rule.duration_minutes, current_time))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn alert_window_covered(row: &AlertMetricRow, duration_minutes: i64, current_time: i64) -> bool {
|
||||
let window_seconds = duration_minutes.clamp(1, 1440) * 60;
|
||||
let report_interval = row.report_interval.clamp(15, 3600).max(60);
|
||||
let expected_samples = ((window_seconds + report_interval - 1) / report_interval).max(1);
|
||||
let required_samples = if expected_samples == 1 {
|
||||
1
|
||||
} else {
|
||||
((expected_samples * 3 + 4) / 5).max(2)
|
||||
};
|
||||
let fresh_within = (report_interval * 2).max(120);
|
||||
row.sample_count >= required_samples
|
||||
&& row.first_timestamp <= row.last_timestamp
|
||||
&& current_time.saturating_sub(row.last_timestamp) <= fresh_within
|
||||
}
|
||||
|
||||
pub async fn active_alert_states(db: &D1Database) -> Result<std::collections::HashSet<String>> {
|
||||
@@ -481,16 +548,25 @@ pub async fn active_alert_states(db: &D1Database) -> Result<std::collections::Ha
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn replace_active_alert_states(
|
||||
pub async fn sync_active_alert_states(
|
||||
db: &D1Database,
|
||||
keys: &std::collections::HashSet<String>,
|
||||
previous: &std::collections::HashSet<String>,
|
||||
current: &std::collections::HashSet<String>,
|
||||
) -> Result<()> {
|
||||
let mut statements = vec![db.prepare("DELETE FROM alert_states")];
|
||||
let mut statements = Vec::new();
|
||||
let delete = db.prepare("DELETE FROM alert_states WHERE rule_id=?1 AND server_id=?2");
|
||||
let insert = db.prepare(
|
||||
"INSERT INTO alert_states(rule_id, server_id, active, updated_at) VALUES (?1, ?2, 1, ?3)",
|
||||
"INSERT INTO alert_states(rule_id, server_id, active, updated_at) VALUES (?1, ?2, 1, ?3) \
|
||||
ON CONFLICT(rule_id, server_id) DO UPDATE SET active=1, updated_at=excluded.updated_at",
|
||||
);
|
||||
let timestamp = now();
|
||||
for key in keys {
|
||||
for key in previous.difference(current) {
|
||||
let Some((rule_id, server_id)) = key.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
statements.push(delete.clone().bind(&[text(rule_id), text(server_id)])?);
|
||||
}
|
||||
for key in current.difference(previous) {
|
||||
let Some((rule_id, server_id)) = key.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
@@ -500,7 +576,9 @@ pub async fn replace_active_alert_states(
|
||||
number(timestamp),
|
||||
])?);
|
||||
}
|
||||
db.batch(statements).await?;
|
||||
if !statements.is_empty() {
|
||||
db.batch(statements).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -568,14 +646,13 @@ pub async fn save_reports(db: &D1Database, server_id: &str, reports: &[AgentRepo
|
||||
swap_used, swap_total, disk_used, disk_total, net_in, net_out,
|
||||
net_rx_total, net_tx_total, uptime, processes, tcp_connections,
|
||||
udp_connections, cpu_cores, cpu_model, os, kernel, arch,
|
||||
virtualization, ipv4, ipv6, gpu_usage, gpu_model, agent_version,
|
||||
virtualization, gpu_usage, gpu_model, agent_version,
|
||||
disk_read_bps, disk_write_bps, disk_read_iops, disk_write_iops,
|
||||
disk_await_ms, disk_utilization, disk_info, gpu_info, message
|
||||
) VALUES (
|
||||
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14,
|
||||
?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26,
|
||||
?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35, ?36, ?37, ?38,
|
||||
?39, ?40
|
||||
?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35, ?36, ?37, ?38
|
||||
) ON CONFLICT(server_id) DO UPDATE SET
|
||||
timestamp=excluded.timestamp, cpu=excluded.cpu, load1=excluded.load1,
|
||||
load5=excluded.load5, load15=excluded.load15, mem_used=excluded.mem_used,
|
||||
@@ -587,8 +664,7 @@ pub async fn save_reports(db: &D1Database, server_id: &str, reports: &[AgentRepo
|
||||
tcp_connections=excluded.tcp_connections, udp_connections=excluded.udp_connections,
|
||||
cpu_cores=excluded.cpu_cores, cpu_model=excluded.cpu_model, os=excluded.os,
|
||||
kernel=excluded.kernel, arch=excluded.arch,
|
||||
virtualization=excluded.virtualization, ipv4=excluded.ipv4, ipv6=excluded.ipv6,
|
||||
gpu_usage=excluded.gpu_usage, gpu_model=excluded.gpu_model,
|
||||
virtualization=excluded.virtualization, gpu_usage=excluded.gpu_usage, gpu_model=excluded.gpu_model,
|
||||
agent_version=excluded.agent_version,
|
||||
disk_read_bps=excluded.disk_read_bps, disk_write_bps=excluded.disk_write_bps,
|
||||
disk_read_iops=excluded.disk_read_iops, disk_write_iops=excluded.disk_write_iops,
|
||||
@@ -676,8 +752,6 @@ fn report_values(server_id: &str, report: &AgentReport, timestamp: i64) -> Vec<J
|
||||
text(&report.kernel),
|
||||
text(&report.arch),
|
||||
text(&report.virtualization),
|
||||
text(&report.ipv4),
|
||||
text(&report.ipv6),
|
||||
number(report.gpu_usage),
|
||||
text(&report.gpu_model),
|
||||
text(&report.agent_version),
|
||||
@@ -778,7 +852,9 @@ pub async fn settings(
|
||||
"history_retention_days",
|
||||
default_retention.clamp(1, 365),
|
||||
),
|
||||
history_cache_version: integer_setting(&values, "history_cache_version", 0),
|
||||
default_theme: string_setting(&values, "default_theme", "system"),
|
||||
active_theme_id: string_setting(&values, "active_theme_id", crate::theme::BUILTIN_THEME_ID),
|
||||
background_url: string_setting(&values, "background_url", ""),
|
||||
theme_options: values
|
||||
.get("theme_options")
|
||||
@@ -800,6 +876,7 @@ pub async fn settings(
|
||||
.get("admin_password_hash")
|
||||
.is_some_and(|value| !value.is_empty()),
|
||||
admin_password_hash: string_setting(&values, "admin_password_hash", ""),
|
||||
password_client_salt: string_setting(&values, "password_client_salt", ""),
|
||||
turnstile_enabled: bool_setting(&values, "turnstile_enabled", false),
|
||||
turnstile_login_enabled: bool_setting(&values, "turnstile_login_enabled", true),
|
||||
turnstile_site_key: string_setting(&values, "turnstile_site_key", ""),
|
||||
@@ -863,6 +940,9 @@ pub async fn update_settings(
|
||||
if let Some(value) = input.default_theme.as_deref() {
|
||||
push_setting!("default_theme", value);
|
||||
}
|
||||
if let Some(value) = input.active_theme_id.as_deref() {
|
||||
push_setting!("active_theme_id", value.trim());
|
||||
}
|
||||
if let Some(value) = input.background_url.as_deref() {
|
||||
push_setting!("background_url", value.trim());
|
||||
}
|
||||
@@ -918,13 +998,21 @@ pub async fn update_settings(
|
||||
if let Some(value) = input.turnstile_site_key.as_deref() {
|
||||
push_setting!("turnstile_site_key", value.trim());
|
||||
}
|
||||
if let Some(value) = input.turnstile_secret_key.as_deref() {
|
||||
if let Some(value) = input
|
||||
.turnstile_secret_key
|
||||
.as_deref()
|
||||
.filter(|value| value.trim() != SECRET_MASK)
|
||||
{
|
||||
push_setting!("turnstile_secret_key", value.trim());
|
||||
}
|
||||
if let Some(value) = input.notification_enabled {
|
||||
push_setting!("notification_enabled", if value { "true" } else { "false" });
|
||||
}
|
||||
if let Some(value) = input.notification_endpoint.as_deref() {
|
||||
if let Some(value) = input
|
||||
.notification_endpoint
|
||||
.as_deref()
|
||||
.filter(|value| value.trim() != SECRET_MASK)
|
||||
{
|
||||
push_setting!("notification_endpoint", value.trim());
|
||||
}
|
||||
if let Some(value) = input.notification_target.as_deref() {
|
||||
@@ -946,13 +1034,104 @@ pub async fn update_settings(
|
||||
};
|
||||
}
|
||||
push_trimmed!(input.cloudflare_account_id, "cloudflare_account_id");
|
||||
push_trimmed!(input.cloudflare_api_token, "cloudflare_api_token");
|
||||
if let Some(value) = input
|
||||
.cloudflare_api_token
|
||||
.as_deref()
|
||||
.filter(|value| value.trim() != SECRET_MASK)
|
||||
{
|
||||
push_setting!("cloudflare_api_token", value.trim());
|
||||
}
|
||||
if !statements.is_empty() {
|
||||
db.batch(statements).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_themes(db: &D1Database, active_id: &str) -> Result<Vec<ThemeView>> {
|
||||
let rows: Vec<ThemeRow> = db
|
||||
.prepare("SELECT id, name, description, url FROM themes ORDER BY created_at DESC")
|
||||
.all()
|
||||
.await?
|
||||
.results()?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let active = row.id == active_id;
|
||||
ThemeView {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
url: row.url,
|
||||
builtin: false,
|
||||
active,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn create_theme(
|
||||
db: &D1Database,
|
||||
id: &str,
|
||||
input: &ThemeInput,
|
||||
source_url: &str,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
db.prepare(
|
||||
"INSERT INTO themes(id, name, description, url, created_at) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
)
|
||||
.bind(&[
|
||||
text(id),
|
||||
text(input.name.trim()),
|
||||
text(input.description.trim()),
|
||||
text(source_url),
|
||||
number(timestamp),
|
||||
])?
|
||||
.run()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn theme_exists(db: &D1Database, id: &str) -> Result<bool> {
|
||||
let row: Option<ThemeRow> = db
|
||||
.prepare("SELECT id, name, description, url FROM themes WHERE id = ?1")
|
||||
.bind(&[text(id)])?
|
||||
.first(None)
|
||||
.await?;
|
||||
Ok(row.is_some())
|
||||
}
|
||||
|
||||
pub async fn theme_url(db: &D1Database, id: &str) -> Result<Option<String>> {
|
||||
let row: Option<ThemeRow> = db
|
||||
.prepare("SELECT id, name, description, url FROM themes WHERE id = ?1")
|
||||
.bind(&[text(id)])?
|
||||
.first(None)
|
||||
.await?;
|
||||
row.map(|theme| crate::theme::resolve_url(&theme.url).map(|resolved| resolved.resolved_url))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub async fn set_active_theme(db: &D1Database, id: &str) -> Result<bool> {
|
||||
if id != crate::theme::BUILTIN_THEME_ID && !theme_exists(db, id).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
save_setting(db, "active_theme_id", id).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn delete_theme(db: &D1Database, id: &str) -> Result<bool> {
|
||||
let result = db
|
||||
.prepare("DELETE FROM themes WHERE id = ?1")
|
||||
.bind(&[text(id)])?
|
||||
.run()
|
||||
.await?;
|
||||
let deleted = result.meta()?.and_then(|meta| meta.changes).unwrap_or(0) > 0;
|
||||
if deleted && get_setting(db, "active_theme_id").await?.as_deref() == Some(id) {
|
||||
save_setting(db, "active_theme_id", crate::theme::BUILTIN_THEME_ID).await?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
pub async fn cleanup_history(db: &D1Database, retention_days: i64) -> Result<()> {
|
||||
let cutoff = now() - retention_days.clamp(1, 365) * 86_400;
|
||||
let recent_cutoff = now() - 7 * 86_400;
|
||||
@@ -1120,3 +1299,47 @@ pub async fn update_expiry(db: &D1Database, id: &str, expires_at: i64) -> Result
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{alert_window_covered, secret_for_api, AlertMetricRow, SECRET_MASK};
|
||||
|
||||
fn row(samples: i64, last_timestamp: i64, report_interval: i64) -> AlertMetricRow {
|
||||
AlertMetricRow {
|
||||
server_id: "server".to_string(),
|
||||
name: "Server".to_string(),
|
||||
value: 90.0,
|
||||
sample_count: samples,
|
||||
first_timestamp: last_timestamp - samples.saturating_sub(1) * 60,
|
||||
last_timestamp,
|
||||
report_interval,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alert_coverage_tracks_report_interval_and_freshness() {
|
||||
let current = 10_000;
|
||||
assert!(alert_window_covered(&row(6, current - 30, 60), 10, current));
|
||||
assert!(!alert_window_covered(
|
||||
&row(5, current - 30, 60),
|
||||
10,
|
||||
current
|
||||
));
|
||||
assert!(alert_window_covered(
|
||||
&row(2, current - 60, 300),
|
||||
10,
|
||||
current
|
||||
));
|
||||
assert!(!alert_window_covered(
|
||||
&row(2, current - 700, 300),
|
||||
10,
|
||||
current
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masks_stored_secrets_in_api_responses() {
|
||||
assert_eq!(secret_for_api(""), "");
|
||||
assert_eq!(secret_for_api("stored-secret"), SECRET_MASK);
|
||||
}
|
||||
}
|
||||
+4
-13
@@ -13,9 +13,6 @@ struct TaskRow {
|
||||
target: String,
|
||||
interval_seconds: i64,
|
||||
default_enabled: i64,
|
||||
sort_order: i64,
|
||||
created_at: i64,
|
||||
updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -32,10 +29,7 @@ pub struct LatencyTaskView {
|
||||
pub target: String,
|
||||
pub interval_seconds: i64,
|
||||
pub default_enabled: bool,
|
||||
pub sort_order: i64,
|
||||
pub server_ids: Vec<String>,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -61,8 +55,8 @@ fn number(value: impl ToString) -> JsValue {
|
||||
pub async fn list_tasks(db: &D1Database) -> Result<Vec<LatencyTaskView>> {
|
||||
let rows: Vec<TaskRow> = db
|
||||
.prepare(
|
||||
"SELECT id, name, task_type, target, interval_seconds, default_enabled, \
|
||||
sort_order, created_at, updated_at FROM latency_tasks \
|
||||
"SELECT id, name, task_type, target, interval_seconds, default_enabled \
|
||||
FROM latency_tasks \
|
||||
ORDER BY sort_order ASC, created_at ASC",
|
||||
)
|
||||
.all()
|
||||
@@ -93,9 +87,6 @@ pub async fn list_tasks(db: &D1Database) -> Result<Vec<LatencyTaskView>> {
|
||||
target: row.target,
|
||||
interval_seconds: row.interval_seconds,
|
||||
default_enabled: row.default_enabled != 0,
|
||||
sort_order: row.sort_order,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -153,8 +144,8 @@ pub async fn update_task(
|
||||
) -> Result<bool> {
|
||||
let exists: Option<TaskRow> = db
|
||||
.prepare(
|
||||
"SELECT id, name, task_type, target, interval_seconds, default_enabled, \
|
||||
sort_order, created_at, updated_at FROM latency_tasks WHERE id = ?1",
|
||||
"SELECT id, name, task_type, target, interval_seconds, default_enabled \
|
||||
FROM latency_tasks WHERE id = ?1",
|
||||
)
|
||||
.bind(&[text(id)])?
|
||||
.first(None)
|
||||
|
||||
+945
-194
File diff suppressed because it is too large.
Load diff
+5
-5
@@ -7,6 +7,9 @@ pub struct LiveHub {
|
||||
|
||||
impl DurableObject for LiveHub {
|
||||
fn new(state: State, _env: Env) -> Self {
|
||||
if let Ok(pair) = WebSocketRequestResponsePair::new("ping", "pong") {
|
||||
state.set_websocket_auto_response(&pair);
|
||||
}
|
||||
Self { state }
|
||||
}
|
||||
|
||||
@@ -51,12 +54,9 @@ impl DurableObject for LiveHub {
|
||||
|
||||
async fn websocket_message(
|
||||
&self,
|
||||
ws: WebSocket,
|
||||
message: WebSocketIncomingMessage,
|
||||
_ws: WebSocket,
|
||||
_message: WebSocketIncomingMessage,
|
||||
) -> Result<()> {
|
||||
if matches!(message, WebSocketIncomingMessage::String(ref value) if value == "ping") {
|
||||
ws.send_with_str("pong")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+45
-120
@@ -1,102 +1,49 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn default_group() -> String {
|
||||
"默认".to_string()
|
||||
}
|
||||
|
||||
fn default_price() -> f64 {
|
||||
0.0
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_billing_cycle() -> i64 {
|
||||
30
|
||||
}
|
||||
|
||||
fn default_currency() -> String {
|
||||
"CNY".to_string()
|
||||
}
|
||||
|
||||
fn default_traffic_limit_type() -> String {
|
||||
"sum".to_string()
|
||||
}
|
||||
|
||||
fn default_reset_day() -> i64 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_report_interval() -> i64 {
|
||||
60
|
||||
}
|
||||
|
||||
fn default_collect_interval() -> i64 {
|
||||
5
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub password_derived: String,
|
||||
pub turnstile_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TurnstileVerifyRequest {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ServerInput {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub region: String,
|
||||
#[serde(default = "default_group")]
|
||||
pub group_name: String,
|
||||
#[serde(default)]
|
||||
pub tags: String,
|
||||
#[serde(default)]
|
||||
pub note: String,
|
||||
#[serde(default)]
|
||||
pub hidden: bool,
|
||||
#[serde(default)]
|
||||
pub expires_at: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub traffic_limit: i64,
|
||||
#[serde(default = "default_traffic_limit_type")]
|
||||
pub traffic_limit_type: String,
|
||||
#[serde(default = "default_price")]
|
||||
pub price: f64,
|
||||
#[serde(default = "default_billing_cycle")]
|
||||
pub billing_cycle: i64,
|
||||
#[serde(default = "default_currency")]
|
||||
pub currency: String,
|
||||
#[serde(default)]
|
||||
pub auto_renewal: bool,
|
||||
#[serde(default)]
|
||||
pub public_remark: String,
|
||||
#[serde(default)]
|
||||
pub network_interface: String,
|
||||
#[serde(default = "default_reset_day")]
|
||||
pub reset_day: i64,
|
||||
#[serde(default = "default_report_interval")]
|
||||
pub report_interval: i64,
|
||||
#[serde(default = "default_collect_interval")]
|
||||
pub collect_interval: i64,
|
||||
#[serde(default)]
|
||||
pub rx_correction: i64,
|
||||
#[serde(default)]
|
||||
pub tx_correction: i64,
|
||||
#[serde(default)]
|
||||
pub offline_notify_disabled: bool,
|
||||
#[serde(default = "default_enabled")]
|
||||
pub auto_update: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SettingsInput {
|
||||
pub site_name: Option<String>,
|
||||
pub site_description: Option<String>,
|
||||
@@ -107,6 +54,7 @@ pub struct SettingsInput {
|
||||
pub offline_threshold_seconds: Option<i64>,
|
||||
pub history_retention_days: Option<i64>,
|
||||
pub default_theme: Option<String>,
|
||||
pub active_theme_id: Option<String>,
|
||||
pub background_url: Option<String>,
|
||||
pub theme_options: Option<serde_json::Value>,
|
||||
pub show_search: Option<bool>,
|
||||
@@ -121,6 +69,7 @@ pub struct SettingsInput {
|
||||
pub show_uptime: Option<bool>,
|
||||
pub admin_username: Option<String>,
|
||||
pub new_password: Option<String>,
|
||||
pub new_password_derived: Option<String>,
|
||||
pub turnstile_enabled: Option<bool>,
|
||||
pub turnstile_login_enabled: Option<bool>,
|
||||
pub turnstile_site_key: Option<String>,
|
||||
@@ -135,24 +84,43 @@ pub struct SettingsInput {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ThemeInput {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ThemeView {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub url: String,
|
||||
pub builtin: bool,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ServerOrderInput {
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ServerBatchInput {
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LatencyTaskInput {
|
||||
pub name: String,
|
||||
pub task_type: String,
|
||||
pub target: String,
|
||||
pub interval_seconds: i64,
|
||||
#[serde(default)]
|
||||
pub default_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub server_ids: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -166,6 +134,7 @@ pub struct AgentLatencyTask {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AgentLatencyResult {
|
||||
pub task_id: String,
|
||||
pub timestamp: i64,
|
||||
@@ -173,7 +142,8 @@ pub struct AgentLatencyResult {
|
||||
pub packet_loss: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AgentDiskMetric {
|
||||
pub name: String,
|
||||
pub mount_point: String,
|
||||
@@ -187,7 +157,8 @@ pub struct AgentDiskMetric {
|
||||
pub utilization: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AgentGpuMetric {
|
||||
pub model: String,
|
||||
pub usage: f64,
|
||||
@@ -195,93 +166,53 @@ pub struct AgentGpuMetric {
|
||||
pub memory_total: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AgentReportBatch {
|
||||
pub server_id: String,
|
||||
pub samples: Vec<AgentReport>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AgentReport {
|
||||
#[serde(default)]
|
||||
pub timestamp: i64,
|
||||
#[serde(default)]
|
||||
pub cpu: f64,
|
||||
#[serde(default)]
|
||||
pub load1: f64,
|
||||
#[serde(default)]
|
||||
pub load5: f64,
|
||||
#[serde(default)]
|
||||
pub load15: f64,
|
||||
#[serde(default)]
|
||||
pub mem_used: i64,
|
||||
#[serde(default)]
|
||||
pub mem_total: i64,
|
||||
#[serde(default)]
|
||||
pub swap_used: i64,
|
||||
#[serde(default)]
|
||||
pub swap_total: i64,
|
||||
#[serde(default)]
|
||||
pub disk_used: i64,
|
||||
#[serde(default)]
|
||||
pub disk_total: i64,
|
||||
#[serde(default)]
|
||||
pub net_in: f64,
|
||||
#[serde(default)]
|
||||
pub net_out: f64,
|
||||
#[serde(default)]
|
||||
pub net_rx_total: i64,
|
||||
#[serde(default)]
|
||||
pub net_tx_total: i64,
|
||||
#[serde(default)]
|
||||
pub uptime: i64,
|
||||
#[serde(default)]
|
||||
pub processes: i64,
|
||||
#[serde(default)]
|
||||
pub tcp_connections: i64,
|
||||
#[serde(default)]
|
||||
pub udp_connections: i64,
|
||||
#[serde(default)]
|
||||
pub cpu_cores: i64,
|
||||
#[serde(default)]
|
||||
pub cpu_model: String,
|
||||
#[serde(default)]
|
||||
pub os: String,
|
||||
#[serde(default)]
|
||||
pub kernel: String,
|
||||
#[serde(default)]
|
||||
pub arch: String,
|
||||
#[serde(default)]
|
||||
pub virtualization: String,
|
||||
#[serde(default)]
|
||||
pub ipv4: String,
|
||||
#[serde(default)]
|
||||
pub ipv6: String,
|
||||
#[serde(default)]
|
||||
pub gpu_usage: f64,
|
||||
#[serde(default)]
|
||||
pub gpu_model: String,
|
||||
#[serde(default)]
|
||||
pub agent_version: String,
|
||||
#[serde(default)]
|
||||
pub disk_read_bps: f64,
|
||||
#[serde(default)]
|
||||
pub disk_write_bps: f64,
|
||||
#[serde(default)]
|
||||
pub disk_read_iops: f64,
|
||||
#[serde(default)]
|
||||
pub disk_write_iops: f64,
|
||||
#[serde(default)]
|
||||
pub disk_await_ms: f64,
|
||||
#[serde(default)]
|
||||
pub disk_utilization: f64,
|
||||
#[serde(default)]
|
||||
pub disks: Vec<AgentDiskMetric>,
|
||||
#[serde(default)]
|
||||
pub gpus: Vec<AgentGpuMetric>,
|
||||
#[serde(default)]
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub latency_results: Vec<AgentLatencyResult>,
|
||||
}
|
||||
|
||||
@@ -294,7 +225,6 @@ pub struct ServerView {
|
||||
pub tags: String,
|
||||
pub note: String,
|
||||
pub hidden: i64,
|
||||
pub sort_order: i64,
|
||||
pub expires_at: Option<i64>,
|
||||
pub traffic_limit: i64,
|
||||
pub traffic_limit_type: String,
|
||||
@@ -311,8 +241,8 @@ pub struct ServerView {
|
||||
pub tx_correction: i64,
|
||||
pub offline_notify_disabled: i64,
|
||||
pub auto_update: i64,
|
||||
#[serde(skip_serializing)]
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
pub timestamp: Option<i64>,
|
||||
pub cpu: Option<f64>,
|
||||
pub load1: Option<f64>,
|
||||
@@ -338,8 +268,6 @@ pub struct ServerView {
|
||||
pub kernel: Option<String>,
|
||||
pub arch: Option<String>,
|
||||
pub virtualization: Option<String>,
|
||||
pub ipv4: Option<String>,
|
||||
pub ipv6: Option<String>,
|
||||
pub gpu_usage: Option<f64>,
|
||||
pub gpu_model: Option<String>,
|
||||
pub agent_version: Option<String>,
|
||||
@@ -349,7 +277,9 @@ pub struct ServerView {
|
||||
pub disk_write_iops: Option<f64>,
|
||||
pub disk_await_ms: Option<f64>,
|
||||
pub disk_utilization: Option<f64>,
|
||||
#[serde(skip_serializing)]
|
||||
pub disk_info: Option<String>,
|
||||
#[serde(skip_serializing)]
|
||||
pub gpu_info: Option<String>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
@@ -389,30 +319,28 @@ pub struct TokenHashRow {
|
||||
pub hidden: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AgentConfigView {
|
||||
pub report_interval: i64,
|
||||
pub collect_interval: i64,
|
||||
pub network_interface: String,
|
||||
pub auto_update: i64,
|
||||
#[serde(default)]
|
||||
pub latency_tasks: Vec<AgentLatencyTask>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AlertRuleInput {
|
||||
pub name: String,
|
||||
pub metric: String,
|
||||
pub threshold: f64,
|
||||
pub duration_minutes: i64,
|
||||
pub aggregation: String,
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub server_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AlertRuleView {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
@@ -421,10 +349,7 @@ pub struct AlertRuleView {
|
||||
pub duration_minutes: i64,
|
||||
pub aggregation: String,
|
||||
pub enabled: i64,
|
||||
#[serde(default)]
|
||||
pub server_ids: Vec<String>,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
||||
+29
-6
@@ -5,7 +5,7 @@ use worker::{wasm_bindgen::JsValue, D1Database, Fetch, Method, Request, RequestI
|
||||
|
||||
use crate::db::{self, SettingsView};
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
#[derive(Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
struct AlertState {
|
||||
offline: HashSet<String>,
|
||||
expiry: HashSet<String>,
|
||||
@@ -63,7 +63,7 @@ pub async fn check_alerts(db_conn: &D1Database, settings: &SettingsView) -> Resu
|
||||
|
||||
let servers = db::list_servers(db_conn, true).await?;
|
||||
let stored = db::get_setting(db_conn, "alert_state").await?;
|
||||
let mut previous: AlertState = stored
|
||||
let previous: AlertState = stored
|
||||
.as_deref()
|
||||
.and_then(|value| serde_json::from_str(value).ok())
|
||||
.unwrap_or_default();
|
||||
@@ -72,7 +72,7 @@ pub async fn check_alerts(db_conn: &D1Database, settings: &SettingsView) -> Resu
|
||||
let offline_after = settings.offline_alert_minutes.clamp(2, 1440) * 60;
|
||||
let mut messages = Vec::new();
|
||||
|
||||
for server in servers {
|
||||
for server in &servers {
|
||||
let server_id = server.id.clone();
|
||||
let is_offline = server
|
||||
.timestamp
|
||||
@@ -119,10 +119,26 @@ pub async fn check_alerts(db_conn: &D1Database, settings: &SettingsView) -> Resu
|
||||
|
||||
let previous_resources = db::active_alert_states(db_conn).await?;
|
||||
let mut current_resources = HashSet::new();
|
||||
let mut evaluated_resources = HashSet::new();
|
||||
let mut eligible_resources = HashSet::new();
|
||||
for rule in db::list_alert_rules(db_conn).await? {
|
||||
if rule.enabled == 0 {
|
||||
continue;
|
||||
}
|
||||
if rule.server_ids.is_empty() {
|
||||
eligible_resources.extend(
|
||||
servers
|
||||
.iter()
|
||||
.filter(|server| server.hidden == 0)
|
||||
.map(|server| format!("{}:{}", rule.id, server.id)),
|
||||
);
|
||||
} else {
|
||||
eligible_resources.extend(
|
||||
rule.server_ids
|
||||
.iter()
|
||||
.map(|server_id| format!("{}:{server_id}", rule.id)),
|
||||
);
|
||||
}
|
||||
let metric_label = match rule.metric.as_str() {
|
||||
"cpu" => "CPU",
|
||||
"memory" => "内存",
|
||||
@@ -138,6 +154,7 @@ pub async fn check_alerts(db_conn: &D1Database, settings: &SettingsView) -> Resu
|
||||
};
|
||||
for value in db::alert_metric_values(db_conn, &rule, current_time).await? {
|
||||
let key = format!("{}:{}", rule.id, value.server_id);
|
||||
evaluated_resources.insert(key.clone());
|
||||
if value.value >= rule.threshold {
|
||||
current_resources.insert(key.clone());
|
||||
if !previous_resources.contains(&key) {
|
||||
@@ -163,12 +180,18 @@ pub async fn check_alerts(db_conn: &D1Database, settings: &SettingsView) -> Resu
|
||||
}
|
||||
}
|
||||
}
|
||||
for key in &previous_resources {
|
||||
if eligible_resources.contains(key) && !evaluated_resources.contains(key) {
|
||||
current_resources.insert(key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !messages.is_empty() {
|
||||
let body = format!("NodeFlare 告警\n\n{}", messages.join("\n"));
|
||||
send(settings, &body).await?;
|
||||
}
|
||||
previous = current;
|
||||
db::save_setting(db_conn, "alert_state", &serde_json::to_string(&previous)?).await?;
|
||||
db::replace_active_alert_states(db_conn, ¤t_resources).await
|
||||
if previous != current {
|
||||
db::save_setting(db_conn, "alert_state", &serde_json::to_string(¤t)?).await?;
|
||||
}
|
||||
db::sync_active_alert_states(db_conn, &previous_resources, ¤t_resources).await
|
||||
}
|
||||
+218
-1
@@ -1,4 +1,185 @@
|
||||
use futures_util::TryStreamExt;
|
||||
use serde_json::Value;
|
||||
use worker::{Error, Fetch, Method, Request, Response, Result, Url};
|
||||
|
||||
pub const BUILTIN_THEME_ID: &str = "builtin-komari-glass";
|
||||
pub const BUILTIN_THEME_NAME: &str = "Komari Glass";
|
||||
pub const INDEX_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
pub const ASSET_MAX_BYTES: usize = 16 * 1024 * 1024;
|
||||
|
||||
pub struct ResolvedTheme {
|
||||
pub source_url: String,
|
||||
pub resolved_url: String,
|
||||
}
|
||||
|
||||
pub fn normalize_url(value: &str) -> Option<String> {
|
||||
let raw = value.trim();
|
||||
if !(12..=2048).contains(&raw.len()) || raw.contains('\\') {
|
||||
return None;
|
||||
}
|
||||
let parsed = Url::parse(raw).ok()?;
|
||||
if parsed.scheme() != "https"
|
||||
|| parsed.host_str() != Some("github.com")
|
||||
|| parsed.port().is_some()
|
||||
|| parsed.username() != ""
|
||||
|| parsed.password().is_some()
|
||||
|| parsed.query().is_some()
|
||||
|| parsed.fragment().is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = parsed.path().trim_matches('/');
|
||||
let parts: Vec<_> = path.split('/').filter(|part| !part.is_empty()).collect();
|
||||
if parts.len() < 4
|
||||
|| parts[2] != "tree"
|
||||
|| parts.iter().any(|part| {
|
||||
part.is_empty()
|
||||
|| !part
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_alphanumeric() || ".-_".contains(character))
|
||||
|| matches!(*part, "." | "..")
|
||||
})
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(format!("https://github.com/{}", parts.join("/")))
|
||||
}
|
||||
|
||||
pub fn resolve_url(value: &str) -> Result<ResolvedTheme> {
|
||||
let source_url =
|
||||
normalize_url(value).ok_or_else(|| Error::RustError("主题 URL 格式无效".to_string()))?;
|
||||
let parsed = Url::parse(&source_url)?;
|
||||
|
||||
let parts: Vec<_> = parsed
|
||||
.path()
|
||||
.trim_matches('/')
|
||||
.split('/')
|
||||
.filter(|part| !part.is_empty())
|
||||
.collect();
|
||||
let owner = parts[0];
|
||||
let repository = parts[1];
|
||||
let reference = parts[3];
|
||||
let subdirectory = parts[4..].join("/");
|
||||
let suffix = if subdirectory.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("/{subdirectory}")
|
||||
};
|
||||
Ok(ResolvedTheme {
|
||||
source_url,
|
||||
resolved_url: format!(
|
||||
"https://raw.githubusercontent.com/{owner}/{repository}/{reference}{suffix}"
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn remote_url(base: &str, relative: &str) -> String {
|
||||
format!("{}/{}", base.trim_end_matches('/'), relative)
|
||||
}
|
||||
|
||||
fn raw_github_base(base: &str) -> Option<Url> {
|
||||
let parsed = Url::parse(&format!("{}/", base.trim_end_matches('/'))).ok()?;
|
||||
if parsed.scheme() != "https"
|
||||
|| parsed.host_str() != Some("raw.githubusercontent.com")
|
||||
|| parsed.port().is_some()
|
||||
|| parsed.username() != ""
|
||||
|| parsed.password().is_some()
|
||||
|| parsed.query().is_some()
|
||||
|| parsed.fragment().is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let parts = parsed
|
||||
.path()
|
||||
.trim_matches('/')
|
||||
.split('/')
|
||||
.filter(|part| !part.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
(parts.len() >= 3
|
||||
&& parts.iter().all(|part| {
|
||||
!matches!(*part, "." | "..")
|
||||
&& part
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_alphanumeric() || ".-_".contains(character))
|
||||
}))
|
||||
.then_some(parsed)
|
||||
}
|
||||
|
||||
pub fn asset_url(base: &str, path: &str) -> Option<String> {
|
||||
let relative = path.strip_prefix("/assets/")?;
|
||||
if relative.is_empty()
|
||||
|| relative.len() > 1024
|
||||
|| relative
|
||||
.chars()
|
||||
.any(|character| character.is_control() || matches!(character, '%' | '\\' | '?' | '#'))
|
||||
|| relative
|
||||
.split('/')
|
||||
.any(|part| part.is_empty() || part == "." || part == "..")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let base = raw_github_base(base)?;
|
||||
let remote = base.join(&format!("assets/{relative}")).ok()?;
|
||||
let expected_path = format!("{}assets/{relative}", base.path());
|
||||
if remote.origin() != base.origin() || remote.path() != expected_path {
|
||||
return None;
|
||||
}
|
||||
Some(remote.to_string())
|
||||
}
|
||||
|
||||
pub fn index_url(base: &str) -> Option<String> {
|
||||
raw_github_base(base).map(|base| remote_url(base.as_str(), "index.html"))
|
||||
}
|
||||
|
||||
pub async fn read_response_limited(
|
||||
response: &mut Response,
|
||||
limit: usize,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let declared = response
|
||||
.headers()
|
||||
.get("Content-Length")?
|
||||
.and_then(|value| value.parse::<usize>().ok());
|
||||
if declared.is_some_and(|length| length == 0 || length > limit) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut body = Vec::with_capacity(declared.unwrap_or(0).min(limit));
|
||||
let mut stream = response.stream()?;
|
||||
while let Some(mut chunk) = stream.try_next().await? {
|
||||
let Some(length) = body.len().checked_add(chunk.len()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if length > limit {
|
||||
return Ok(None);
|
||||
}
|
||||
body.append(&mut chunk);
|
||||
}
|
||||
Ok((!body.is_empty()).then_some(body))
|
||||
}
|
||||
|
||||
pub async fn validate_remote(base: &str) -> Result<()> {
|
||||
let index = index_url(base)
|
||||
.ok_or_else(|| Error::RustError("主题资源地址不是 GitHub Raw 地址".to_string()))?;
|
||||
let request = Request::new(&index, Method::Get)?;
|
||||
let mut response = Fetch::Request(request).send().await?;
|
||||
let status = response.status_code();
|
||||
if !(200..300).contains(&status) {
|
||||
return Err(Error::RustError(format!(
|
||||
"主题 index.html 返回 HTTP {status}"
|
||||
)));
|
||||
}
|
||||
let body = read_response_limited(&mut response, INDEX_MAX_BYTES)
|
||||
.await?
|
||||
.ok_or_else(|| Error::RustError("主题 index.html 内容无效".to_string()))?;
|
||||
let body = String::from_utf8(body)
|
||||
.map_err(|_| Error::RustError("主题 index.html 不是 UTF-8 文本".to_string()))?;
|
||||
if body.trim().is_empty() {
|
||||
return Err(Error::RustError("主题 index.html 内容无效".to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn settings_schema() -> Value {
|
||||
let currencies = [
|
||||
@@ -35,7 +216,7 @@ pub fn settings_schema() -> Value {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::settings_schema;
|
||||
use super::{asset_url, normalize_url, resolve_url, settings_schema};
|
||||
|
||||
#[test]
|
||||
fn exposes_builtin_glass_theme_settings() {
|
||||
@@ -45,4 +226,40 @@ mod tests {
|
||||
assert!(settings.iter().any(|field| field["key"] == "enableBlur"));
|
||||
assert_eq!(schema["source"], "builtin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_only_github_tree_urls() {
|
||||
assert_eq!(
|
||||
normalize_url("https://github.com/acme/theme/tree/abc123").as_deref(),
|
||||
Some("https://github.com/acme/theme/tree/abc123")
|
||||
);
|
||||
assert!(normalize_url("https://themes.example.com/nodeflare").is_none());
|
||||
assert!(normalize_url("http://themes.example.com/theme").is_none());
|
||||
assert!(normalize_url("https://user@example.com/theme").is_none());
|
||||
assert!(normalize_url("https://github.com/acme/theme").is_none());
|
||||
assert_eq!(
|
||||
resolve_url("https://github.com/acme/theme/tree/main/dist")
|
||||
.expect("GitHub theme URL")
|
||||
.resolved_url,
|
||||
"https://raw.githubusercontent.com/acme/theme/main/dist"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_proxy_paths_to_theme_assets() {
|
||||
assert_eq!(
|
||||
asset_url(
|
||||
"https://raw.githubusercontent.com/acme/theme/main",
|
||||
"/assets/app.js"
|
||||
)
|
||||
.as_deref(),
|
||||
Some("https://raw.githubusercontent.com/acme/theme/main/assets/app.js")
|
||||
);
|
||||
assert!(asset_url("https://example.com/theme", "/assets/app.js").is_none());
|
||||
assert!(asset_url("https://example.com/theme", "/logo.svg").is_none());
|
||||
assert!(asset_url("https://example.com/theme", "/assets/../secret").is_none());
|
||||
assert!(asset_url("https://example.com/theme", "/assets/%2e%2e/secret").is_none());
|
||||
assert!(asset_url("https://example.com/theme", "/assets/..\\secret").is_none());
|
||||
assert!(asset_url("https://example.com/theme", "/assets/a%2fb.js").is_none());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,30 @@ binding = "DB"
|
||||
# Wrangler 4.45+ provisions and binds this database when database_id is omitted.
|
||||
database_name = "nodeflare"
|
||||
|
||||
[[ratelimits]]
|
||||
name = "API_RATE_LIMITER"
|
||||
namespace_id = "73120401"
|
||||
|
||||
[ratelimits.simple]
|
||||
limit = 300
|
||||
period = 60
|
||||
|
||||
[[ratelimits]]
|
||||
name = "AUTH_RATE_LIMITER"
|
||||
namespace_id = "73120402"
|
||||
|
||||
[ratelimits.simple]
|
||||
limit = 20
|
||||
period = 60
|
||||
|
||||
[[ratelimits]]
|
||||
name = "AGENT_RATE_LIMITER"
|
||||
namespace_id = "73120403"
|
||||
|
||||
[ratelimits.simple]
|
||||
limit = 120
|
||||
period = 60
|
||||
|
||||
[[durable_objects.bindings]]
|
||||
name = "LIVE_HUB"
|
||||
class_name = "LiveHub"
|
||||
|
||||
Reference in new issue
Block a user