diff --git a/.env.example b/.env.example index e6f2403..9aedbf9 100644 --- a/.env.example +++ b/.env.example @@ -22,8 +22,8 @@ # Delay before checking bandwidth downgrade (seconds) #DOWNGRADE_START_CHECK=1800 -# Relay server port -#HBBS_PORT=21117 +# Relay server port (hbbr) +#HBBR_PORT=21117 # ============================ # HBBS (Rendezvous / Signal Server) @@ -85,7 +85,7 @@ # Gin mode (release/debug/test) #RUSTDESK_API_GIN_MODE=release -# Language for API responses (en, ru, fr, es, ko) +# Language for API responses (en, ru, zh-CN) #RUSTDESK_API_LANG=en # Enable web client (1=on, 0=off) @@ -120,16 +120,16 @@ #RUSTDESK_API_POSTGRESQL_DBNAME=postgres #RUSTDESK_API_POSTGRESQL_SSLMODE=disable -# RustDesk server addresses (announced to clients) +# RustDesk server endpoints (announced to clients; keep explicit ports where required) #RUSTDESK_API_RUSTDESK_ID_SERVER= #RUSTDESK_API_RUSTDESK_RELAY_SERVER= #RUSTDESK_API_RUSTDESK_API_SERVER= #RUSTDESK_API_RUSTDESK_WS_HOST= -# Path to the public key file (generated by hbbs on first run) +# Current public-key file contract: use the file generated by hbbs on first run. #RUSTDESK_API_KEY_FILE=/data/id_ed25519.pub -# Encryption key (same as HBBS KEY) +# Legacy raw key compatibility only; new deployments must use RUSTDESK_API_KEY_FILE. #RUSTDESK_API_RUSTDESK_KEY= # Enable personal API (1=on, 0=off) @@ -152,7 +152,7 @@ #RUSTDESK_API_REDIS_ADDR= #RUSTDESK_API_REDIS_PASSWORD= -# Cache type (memory/redis) +# Cache type (memory/file/redis) #RUSTDESK_API_CACHE_TYPE=memory # Proxy for outgoing connections (OAuth etc.) @@ -163,15 +163,20 @@ # Custom Client Builder # ============================ -# Build agents use shared Docker volumes for job tickets. -# Build output is written to rdgen-data:/output/{build_id}/ -# No additional env vars are required for basic operation. +# Manual/historical file-queue material only (not the current provider-dispatch +# build path): old agents used the shared Docker volume for job tickets and wrote +# output below rdgen-data:/output/{build_id}/. No additional env vars are required +# for the current provider-backed operation. # Secret encryption key for data at rest (BUGS.md B-008). # Encrypts the GitHub PAT, the workflow PayloadKey and permanent_password # (stored inside custom_json) in the database with AES-256-GCM. # Read directly from the environment (NOT via the RUSTDESK_API_ prefix). -# Use a long random value; keep it stable (rotating it makes existing -# encrypted rows unreadable). If unset, secrets are stored in plaintext. +# Use a long random value. +# Existing ciphertext requires the same key; key rotation is unsupported. +# Legacy plaintext rows remain readable; saving them again encrypts them when +# the key exists. If unset, new non-empty +# secret writes and secret-bearing Custom Builder operations are rejected rather +# than stored as plaintext. # Must NOT reuse WORKFLOW_PAYLOAD_KEY (that one is shared with GitHub). #SECRET_ENCRYPTION_KEY= diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0532d99 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,133 @@ +name: Build + +on: + push: + branches: [main] + paths: + - "api/**" + - "admin-ui/**" + - "server/**" + - "libs/**" + - "go.mod" + - "go.work" + - "go.work.sum" + - "docker/**" + - ".dockerignore" + - "Dockerfile" + - "docker-compose.yml" + - "docker-compose.yaml" + - "compose.yml" + - "compose.yaml" + - ".github/workflows/**" + pull_request: + paths: + - "api/**" + - "admin-ui/**" + - "server/**" + - "libs/**" + - "go.mod" + - "go.work" + - "go.work.sum" + - "docker/**" + - ".dockerignore" + - "Dockerfile" + - "docker-compose.yml" + - "docker-compose.yaml" + - "compose.yml" + - "compose.yaml" + - ".github/workflows/**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + go: + name: Go checks + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: api + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: api/go.mod + cache: true + cache-dependency-path: | + api/go.sum + go.work.sum + - run: go test ./... + - run: go vet ./... + - run: go build -o /tmp/deskforge-apimain ./cmd/apimain.go + + admin-ui: + name: Admin UI build + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: admin-ui + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + - run: npm ci + - run: npm run build + + rust: + name: Rust checks + runs-on: ubuntu-24.04 + env: + DATABASE_URL: sqlite:///tmp/deskforge-sqlx.sqlite3 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - run: rustup toolchain install 1.92.0 --profile minimal --component rustfmt + - run: cargo +1.92.0 fmt --all -- --check + working-directory: server + - run: cargo +1.92.0 fmt --all -- --check + working-directory: libs/hbb_common + - name: Prepare SQLx SQLite schema + run: | + python3 - <<'PY' + import sqlite3 + + with sqlite3.connect("/tmp/deskforge-sqlx.sqlite3") as connection: + connection.executescript( + """ + create table if not exists peer ( + guid blob primary key not null, + id varchar(100) not null, + uuid blob not null, + pk blob not null, + created_at datetime not null default(current_timestamp), + user blob, + status tinyint, + note varchar(300), + info text not null + ) without rowid; + create unique index if not exists index_peer_id on peer (id); + create index if not exists index_peer_user on peer (user); + create index if not exists index_peer_created_at on peer (created_at); + create index if not exists index_peer_status on peer (status); + """ + ) + - run: cargo +1.92.0 check --locked + working-directory: server + - run: cargo +1.92.0 test --locked + working-directory: server + + docker: + name: Docker build + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - run: docker build -f docker/Dockerfile -t deskforge:ci . diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml new file mode 100644 index 0000000..269bdf6 --- /dev/null +++ b/.github/workflows/build_test.yml @@ -0,0 +1,77 @@ +name: Build Test + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + focused: + name: Focused monorepo validation + runs-on: ubuntu-24.04 + env: + DATABASE_URL: sqlite:///tmp/deskforge-sqlx.sqlite3 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: api/go.mod + cache: true + cache-dependency-path: | + api/go.sum + go.work.sum + - name: Go tests and vet + working-directory: api + run: | + go test ./... + go vet ./... + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + - name: Admin UI build + working-directory: admin-ui + run: | + npm ci + npm run build + - run: rustup toolchain install 1.92.0 --profile minimal --component rustfmt + - name: Rust format check + working-directory: server + run: cargo +1.92.0 fmt --all -- --check + - name: Shared Rust format check + working-directory: libs/hbb_common + run: cargo +1.92.0 fmt --all -- --check + - name: Prepare SQLx SQLite schema + run: | + python3 - <<'PY' + import sqlite3 + + with sqlite3.connect("/tmp/deskforge-sqlx.sqlite3") as connection: + connection.executescript( + """ + create table if not exists peer ( + guid blob primary key not null, + id varchar(100) not null, + uuid blob not null, + pk blob not null, + created_at datetime not null default(current_timestamp), + user blob, + status tinyint, + note varchar(300), + info text not null + ) without rowid; + create unique index if not exists index_peer_id on peer (id); + create index if not exists index_peer_user on peer (user); + create index if not exists index_peer_created_at on peer (created_at); + create index if not exists index_peer_status on peer (status); + """ + ) + PY + - name: Rust check + working-directory: server + run: cargo +1.92.0 check --locked + - name: Rust tests + working-directory: server + run: cargo +1.92.0 test --locked diff --git a/AGENTS.md b/AGENTS.md index 3f8e360..51b2a13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Everything in one Docker image via s6-overlay. - **Rust servers** (`server/`): hbbs (ID/signaling, TCP/UDP 21116) + hbbr (relay, TCP 21117) - **Go API** (`api/`): Gin on port 21114. GORM (SQLite/MySQL/PostgreSQL). JWT, LDAP, OIDC. - **Admin UI** (`admin-ui/`): Vue 3 + Element Plus. Served at `/admin/`. REST + WebSocket. -- **rdgen** (`rdgen/`): vendored reference workflow (not a service). +- **rdgen** (`rdgen/`): vendored historical/reference workflow material (not a service; frozen). - **Shared lib** (`libs/hbb_common`): Rust crate shared between hbbs and hbbr. ## Tech stack @@ -18,7 +18,7 @@ Everything in one Docker image via s6-overlay. | Component | Stack | | ------------- | ----------------------------------------------------------------- | | Rust (server) | 2021 edition, axum 0.5, sqlx 0.6, tokio, sodiumoxide, openssl | -| Go (api) | 1.23, gin 1.9, gorm 1.25, swag, cobra/viper, jwt, ldap, OIDC | +| Go (api) | 1.25, gin 1.9, gorm 1.25, swag, cobra/viper, jwt, ldap, OIDC | | Admin UI | Vue 3.5, Element Plus 2.8, Vite 6, Pinia 2.2, vue-router 4, axios| | Python (rdgen)| Django (vendored reference, not a service) | | Infra | Docker + s6-overlay, docker compose | @@ -49,14 +49,30 @@ admin-ui/ — Vue 3 admin panel ├── src/styles/ — SCSS (design tokens, light/dark) └── src/utils/ — auth, request, export, i18n (en/ru/zh_CN) -rdgen/ — vendored reference workflow (patches, generator-*.yml) -libs/hbb_common/ — shared Rust library (submodule) +rdgen/ — ❄️ frozen vendored historical/reference workflow material (patches, generator-*.yml) +libs/hbb_common/ — tracked shared Rust library in DeskForge (not a submodule here) docker/ — Dockerfile + compose + entrypoint scripts -github-build/ — active CI workflow for client builds -win-builder/ — ❄️ frozen standalone Windows builder -offline-kit/ — ❄️ frozen sovereign build kit +github-build/ — reference/documentation only; no executable workflow copies +win-builder/ — ❄️ frozen manual/historical-only standalone builder +offline-kit/ — ❄️ frozen dependency-freeze tool; verification is incomplete ``` +DeskForge tracks `libs/hbb_common/` directly. The configured RustDesk fork has its +own `libs/hbb_common` git submodule, currently recorded against upstream +`rustdesk/hbb_common`; its local state is dirty and unpublished, so clean fork +provenance is an explicit reproducibility gate. The fork's `.github/workflows/` files +are the sole executable source for active client-build workflows; `github-build/` and +`rdgen/` are reference/frozen material only. No current repository `vendor/` tree, +`rustdesk-deps/` archive, or client-release directory is tracked here. +The published RustDesk client source/ref is 1.4.8 and the published DeskForge API +schema is `DatabaseVersion` 272. The current local corrective candidate targets API +schema 283, which is not published or live-provider evidence. Schema 283 adds +`idx_custom_presets_user_id_name` on `(user_id, name)`; before `AutoMigrate`, its +preflight fails on existing duplicate groups without auto-selecting a row or deleting +data. The earlier schema-282 workflow-approval migration remains historical +context. SQLite is the only recorded migration evidence; MySQL/PostgreSQL migration +and read/write coverage remain unverified. + ## Build / dev commands ### Docker (primary) @@ -65,7 +81,7 @@ offline-kit/ — ❄️ frozen sovereign build kit cd docker docker compose build # full build docker compose up -d # start -docker compose -f docker-compose-dev.yaml up -d # dev +cd ../api && docker compose -f docker-compose-dev.yaml up -d # dev API stack ``` ### Rust @@ -77,7 +93,12 @@ cd server && cargo build --release && cargo clippy && cargo test ### Go ```bash -cd api && go build -o release/apimain cmd/apimain.go && go vet ./... && go test ./... +cd api && go build -o release/apimain cmd/apimain.go +# Full local checks: +GOWORK=off go vet ./... +GOWORK=off go test ./... +# Redis integration tests and benchmarks are opt-in. Configure +# DESKFORGE_TEST_REDIS_ADDR to run them; no live Redis endpoint is assumed. ``` ### Admin UI @@ -91,6 +112,8 @@ cd admin-ui && npm install && npm run dev && npm run build | Variable | Purpose | Used by | |----------|---------|---------| | `RELAY` | Relay server address | Rust hbbr | +| `HBBR_PORT` | Relay server port | Rust hbbr | +| `HBBS_PORT` | ID/Rendezvous server port | Rust hbbs | | `ENCRYPTED_ONLY` | Only encrypted connections | Rust | | `MUST_LOGIN` | Require login before connect | Rust | | `RUSTDESK_API_RUSTDESK_ID_SERVER` | ID server address | Go API | @@ -98,9 +121,13 @@ cd admin-ui && npm install && npm run dev && npm run build | `RUSTDESK_API_RUSTDESK_API_SERVER` | API server URL | Go API | | `RUSTDESK_API_KEY_FILE` | Path to public key file | Go API | | `RUSTDESK_API_JWT_KEY` | JWT secret key | Go + Rust | -| `RUSTDESK_API_GORM_TYPE` | sqlite/mysql/postgres | Go API | +| `RUSTDESK_API_GORM_TYPE` | sqlite/mysql/postgresql | Go API | | `RUSTDESK_API_LANG` | en/ru/zh-CN | Go + UI | -| `SECRET_CRYPT_KEY` | AES-GCM key for secrets at rest | Go API | +| `SECRET_ENCRYPTION_KEY` | AES-GCM key for secrets at rest | Go API | + +New non-empty secret writes and secret-bearing Custom Builder operations require this +key and are rejected rather than stored as plaintext when it is missing. Legacy plaintext +rows remain readable; saving them again encrypts them when the key exists. ## Key integration points @@ -109,7 +136,7 @@ cd admin-ui && npm install && npm run dev && npm run build - Go reads public key from `RUSTDESK_API_KEY_FILE` (`/data/id_ed25519.pub`) - Go connects to hbbs/hbbr via `RUSTDESK_API_RUSTDESK_ID_SERVER`/`RUSTDESK_API_RUSTDESK_RELAY_SERVER` - JWT: Go generates, Rust validates (`jwt.rs`) -- WebSocket bridge: port 21118 +- WebSocket bridge: port 21118; relay WebSocket: port 21119 ### Admin UI ↔ Go API @@ -121,6 +148,12 @@ cd admin-ui && npm install && npm run dev && npm run build ## Agent constraints - Do not modify upstream directly (`rustdesk/rustdesk-server`, `lejianwen/rustdesk-api`) — only forks. +- Active third-party upstream dependencies remain in the Rust/Go manifests; a complete + upstream-independent or offline dependency bundle is not currently verified. +- The combined DeskForge distribution is identified as AGPL-3.0 for the covered work; + separate API/UI/reference components retain their applicable upstream licenses and notices. + The license inventory is incomplete; no signatures or attestations are recorded, and no + full sovereignty claim is made. - Keep Docker entrypoint scripts in sync with the services they supervise. - Never log or commit secrets. - Document env vars in README + docker-compose. @@ -136,7 +169,9 @@ cd admin-ui && npm install && npm run dev && npm run build - **Clean layered (Go):** Controller → Service → Model. Do not mix layers. - **Embedded UI:** Go embeds `admin-ui/dist/` and `web/`. -- **Multi-DB:** GORM, no raw SQL. +- **Configured multi-DB:** GORM with SQLite/MySQL/PostgreSQL drivers, no raw SQL; + SQLite is locally exercised, while MySQL/PostgreSQL migration and read/write + coverage remain unverified. - **OAuth/LDAP:** configured via admin panel → DB. Falls back to local users. - **Server commands:** allowlist in `serverCmd.go`. @@ -151,4 +186,4 @@ cd admin-ui && npm install && npm run dev && npm run build ## New upstream version workflow -See [PLAN.md §7](PLAN.md#7-workflow-new-upstream-rustdesk-client-release). +See [PLAN.md §7](PLAN.md#7-historical-fork-maintenance-notes-for-a-new-upstream-rustdesk-client-release). diff --git a/BUGS.md b/BUGS.md index f2b7ef5..02c986f 100644 --- a/BUGS.md +++ b/BUGS.md @@ -3,54 +3,64 @@ > Tracker for issues found in the build-custom-agent end-to-end flow. > Backend: `api/http/controller/admin/custom_build.go`, `api/service/custom_build.go`, > `api/service/github_build_config.go`. Frontend: `admin-ui/src/views/custom-client/index.vue`, -> `admin-ui/src/views/server/github-build.vue`. Workflow: `github-build/rustqs-windows-min-test.yml`. +> `admin-ui/src/views/server/github-build.vue`. Active Windows workflow source: branch +> `rustqs/workflows`, workflow `.github/workflows/rustqs-windows.yml`, artifact +> `rustqs-windows`; local `github-build/` workflow files are reference material only. > > Status legend: `[ ]` open · `[x]` fixed · `[~]` partial · `[skip]` won't fix (owner decision). > -> Last audit: 2026-06-23 (fixed items removed; tracker lists only open work). +> Last audit: 2026-06-23. Current file contents are a bounded tracker snapshot: +> 1 fixed, 4 partial, and 1 open entry; older aggregate counts remain in dated +> changelog history. +> Current-state reconciliation: 2026-08-10. Historical findings below are labeled +> where the current provider-only path supersedes the old queue behavior; no live +> provider or clean-build evidence is implied. Older branch and workflow names below +> are retained as historical records and do not describe the current executable path. --- -## Architectural mismatch with PLAN.md §3 +## Historical architectural mismatch with PLAN.md §3 -PLAN.md declares the standalone / Docker build agents **frozen as fallback** (§8.3, §8.4). -Reality: `custom_build.go::submitBuild` still routes every non-Windows platform — and even -`windows-x86` and Windows when GitHub config is absent — into the file queue -(`/rdgen-data/jobs/{id}.json`). Owner decision (2026-06-20): +The original audit recorded a mismatch with the standalone / Docker build agents marked +**frozen as fallback** (§8.3, §8.4). The current source now checks provider readiness +before persistence and does not route production submissions into the file queue. The +queue scripts remain frozen historical material. Owner decision (2026-06-20): -1. Treat Docker `build-linux` and `build-win` containers as **frozen manual fallback**, not the +1. Treat Docker `build-linux` and `build-win` containers as **frozen manual/historical-only** material, not the default route. They stay on disk but should not be started by `docker compose up`. 2. Remove `windows-x86` (32-bit) as a build target everywhere — UI option, form defaults, any router branches. 2026; not worth maintaining. -3. Build a **GitHub Actions workflow for Linux + Android** mirroring the windows-min-test - pipeline, and re-route `submitBuild` accordingly. Until those workflows ship, non-Windows - platforms should be hidden in the UI to stop users from creating phantom builds. +3. Keep the fork-owned Linux + Android workflow mappings, but do not re-expose those + platforms until PR11 has real end-to-end evidence. Until then, non-Windows platforms + remain gated in the UI/API to prevent phantom builds. The bugs below are grouped by where they leak into user-visible breakage. --- -## CRITICAL — workflow is silently broken end-to-end - -### [~] B-001 · File-queue jobs never propagate `done` status back to the DB -**Deferred on branch `fix/build-custom-agent` (2026-06-20):** UI now restricts platforms to -Windows-via-GitHub (B-013), so the file-queue path is unreachable from the default flow -even though `submitBuild` still has the branch. `docker/docker-compose.yml` moved -`build-linux` and `build-win` services behind a `fallback` profile so they don't start by -default. Full fix (status mirror) only matters once Linux/Android workflows land (B-012), -at which point we'd rather route them through GitHub too. - -**Where:** `api/http/controller/admin/custom_build.go:172-201` (writes job), -`docker/entrypoint-linux.sh:37,49,76,...` and `docker/entrypoint-win.sh:33,45,210` (write `output_dir/status`). -**Symptom:** Linux/Android (and Windows when GitHub config is missing) builds sit at -`Status=pending` forever. `DownloadByKey` returns HTTP 409. The Download button never appears -in the UI (`v-if="row.status === 'done'"`, `custom-client/index.vue:306`). +## Historical critical finding — workflow was silently broken end-to-end + +### [~] B-001 · Historical file-queue jobs do not propagate `done` status back to the DB +**Current state:** provider readiness is checked before a production build row is persisted, +so the file-queue path is not a production fallback. `docker/docker-compose.yml` keeps +`build-linux` and `build-win` behind a `fallback` profile. The frozen scripts still have +the status-mirror limitation if an operator runs them manually; no live provider evidence +is implied by this closure boundary. + +**Current path:** `api/http/controller/admin/custom_build.go:1246-1261` dispatches +production builds through the configured provider; the old queue references remain +only in the frozen scripts below. **Historical queue locations:** +`docker/entrypoint-linux.sh:37,49,76,...` and `docker/entrypoint-win.sh:33,45,210` +write `output_dir/status`. +**Historical symptom:** Linux/Android (and Windows when GitHub config was missing) builds +sat at `Status=pending` forever. `DownloadByKey` returned HTTP 409. The Download button +did not appear in the UI (`v-if="row.status === 'done'"`, `custom-client/index.vue:306`). **Root cause:** no Go-side watcher reads `/rdgen-data/output/{id}/status`. The build agent's status file is dead-letter. **Fix path (per owner direction):** -- Short term: hide all non-Windows-via-GitHub options in the UI (B-002, B-013). -- Long term: replace the file queue for the supported platforms with GitHub Actions dispatch - (linux/android workflows, B-012). +- Current: keep all non-Windows-via-GitHub options gated in the UI/API (B-002, B-013). +- Future: enable Linux/Android only after fork workflow, artifact, embedding, and download + evidence is recorded under B-012/PR11. ## LOW — dead code / cleanup @@ -80,6 +90,12 @@ Left in place because it's a documented capability URL and may have third-party ## STRUCTURAL — to enable B-001/B-002/B-013 fixes ### [~] B-012 · Build Linux + Android GitHub Actions workflows +**Current state:** the API has fork-owned filename mappings, but its production capability +gate rejects Linux and Android until PR11 validates the complete workflow and artifact path. +No live provider/workflow run or clean build is recorded in the current canonical plan. + +**Historical audit record:** + **Backend:** merged (PR #44 backend routing: `submitBuild` dispatches `platform=linux`/`android` by workflow constant; `tryGithubDispatch` picks `rustqs-linux.yml`/`rustqs-android.yml`; `pollAndDownload` selects artifact by platform). @@ -88,6 +104,8 @@ by workflow constant; `tryGithubDispatch` picks `rustqs-linux.yml`/`rustqs-andro and `rustqs/min-test` (execution) — all three are indexed (HTTP 200): `rustqs-windows-min-test.yml`, `rustqs-linux.yml`, `rustqs-android.yml`. Filenames in Go constants match fork filenames exactly. +These deployment observations are historical; they are not current provider-run, +artifact, package, or support evidence. **Critical dependency:** `bridge.yml` must also exist on both branches — all three `rustqs-*.yml` reference it as a reusable workflow. Without it, dispatch succeeds @@ -96,11 +114,15 @@ but the run fails with a parse error (422). Still open: - validate `rustqs-linux.yml` and `rustqs-android.yml` on real Actions runs (build steps: vcpkg/flutter/build.py/packaging/artifact paths — need CI iteration like windows-min-test did) -- Android `custom_.txt` embedding is best-effort, needs verification +- Android `custom_.txt` runtime-path and fail-closed packaging checks have local static + evidence; no live APK/package/install/runtime evidence exists - re-expose Linux/Android in the UI (B-013) behind a feature flag once runs are green -**Where:** `github-build/rustqs-linux.yml`, `github-build/rustqs-android.yml`. Reference templates: +**Where (active source):** the configured RustDesk fork's +`.github/workflows/rustqs-linux.yml` and `.github/workflows/rustqs-android.yml`. +**Historical/reference templates:** former local `github-build/` workflow references and `rdgen/.github/workflows/generator-linux.yml`, `rdgen/.github/workflows/generator-android.yml`. +The `github-build/` and `rdgen/` workflow material is not the executable source. **Symptoms (historical):** before the push, dispatch returned HTTP 404 because workflow files were not on `master` (default branch); submit went to the deprecated file queue (B-001). Resolved by pushing workflow files to both `master` and `rustqs/min-test`. @@ -119,7 +141,10 @@ can create/alter peers and inject audit entries. `/api/shared-peer` also does an `(*j)["share_token"].(string)` assertion (`webClient.go:57`) → 500 on missing token. **Fix:** needs RustDesk protocol design confirmation (the PC client hits these before auth). -### [ ] AU-L-010 · Hardcoded version list in Custom Client UI +### [x] AU-L-010 · Hardcoded version list in Custom Client UI +Resolved in the current source by the provider-derived version catalog; unavailable +provider catalog data returns an empty/error state rather than an obsolete hardcoded +version fallback. Live provider catalog evidence remains unverified. ## rdgen generator — open findings (consolidated from the removed `AUDIT.md`) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d0ef7..d1e7ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,61 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Local PR #59 review remediation — 2026-08-20 +- The current local corrective candidate is `DatabaseVersion` **283**; the published + DeskForge schema remains **272**. Schema 283 adds + `idx_custom_presets_user_id_name` on `(user_id, name)`. Its migration preflight + stops on existing duplicate groups before `AutoMigrate` without auto-selecting a + row or deleting data. The earlier schema-282 workflow-approval migration + remains historical context. +- DeskForge locally requires the exact provider-owned + `# deskforge-workflow-identity-guard: v1` marker in workflow content resolved at + the immutable workflow SHA before approval, preparation, or secret-bearing + dispatch. Legacy unguarded tags fail closed. This is not an atomic GitHub + selector/SHA binding or live-provider-readiness claim. +- [RustDesk PR #7](https://github.com/bashrusakh/rustdesk/pull/7) is merged into + `rustqs/workflows` at merge commit + `ced31ae07f69c20119b88212b10d2eb2df651c97`, containing prior source commit + `6ef1cd7fe`. It adds the marker, requires outer and inner SHA checks in the bridge, + and gates draft Linux/Android before secret-bearing jobs. +- DeskForge local source remediation also includes `d67b6e7`, which requires a stored + v2 producer-manifest and exact canonical-output proof across publication, recovery, + detail, download, reuse, and handoff boundaries. This is not live provider artifact + or production-output proof. +- DeskForge PR #59 remains open and dirty remotely. The merge conflict is resolved + only in local `2c38c87`; later local `da42521`, `9a1ee5e`, and `d67b6e7` are not + pushed. `workflow-v1.2.0` does not exist, and no newly signed provider-verified + immutable protected workflow tag exists yet; live tag protection verification, + reapproval, and reverification remain required before production dispatch. +- Prior local remediation results record passing focused migration/custom-preset + coverage, a focused race check, `go vet`, `gofmt`, and `git diff --check`. + Migration evidence is SQLite-only; MySQL/PostgreSQL migration and read/write + coverage remain unverified. + +### Current-state reconciliation — 2026-08-10 +- The published RustDesk client source/ref is **1.4.8** and the published DeskForge API + schema is `DatabaseVersion` **272**. At that reconciliation, the then-current + local workflow-approval migration was schema 282; it is not a current + published-schema claim. + Older 1.4.7/272 references in the dated entries below remain historical release/schema notes. +- The current custom-client path is owned-fork dispatch, provider polling, exact artifact + retrieval, and local validation/publication. Runner callbacks, local file-queue fallback, + and sole-artifact selection are not current production behavior. +- Dispatch `return_run_details=true` plus an exact HTTP 200 run-details response is the + project/provider contract under test. Standard 204 is intentionally unsupported because + it does not provide an accepted exact run correlation; normal GitHub operation is not verified. +- Windows is the only production capability admitted by the API gate. Linux/Android + workflow mappings exist but remain gated pending PR11 evidence; no live provider run or + clean-environment build proof is claimed. +- Canonical plan state: PR9 is `verified-with-notes`; PR10 and PR11 remain + `in-progress`, while PR12 is `verified-with-notes` for docs only. This does not claim + live provider execution, clean builds, platform support, sovereignty, or release publication. +- Follow-up verification on 2026-08-11: `GOWORK=off go vet ./...` and + `GOWORK=off go test ./...` pass after test-only cache diagnostics and opt-in Redis + test changes. Redis integration tests and benchmarks run only when + `DESKFORGE_TEST_REDIS_ADDR` is configured; no live Redis run is recorded. + MySQL/PostgreSQL, live provider, and the other release/build gates remain unverified. + ### Fixed (custom-client: relay_server port stripping + api-server key) - **custom-client: relay_server port stripped on blur, submit, and prefill** — now consistent with `host`: hostname only, no port. The client appends default relay port (21117) automatically. @@ -40,7 +95,11 @@ across three migration steps; `AutoMigrate` is idempotent so all schema addition 37 Rust test-code alerts dismissed as test-only. (PRs #24, #25, #26, #28, #29) - **api + admin-ui: encrypted secrets at rest** — `CustomBuild`, `CustomPreset`, `GithubBuildConfig` now store sensitive fields wrapped with AES-GCM via - `api/utils/secretcrypt`. Key from `SECRET_CRYPT_KEY` env var; absence fails closed at boot. + `api/utils/secretcrypt`. Uses the canonical `SECRET_ENCRYPTION_KEY` env var. + Without it, new non-empty secret writes and secret-bearing Custom Builder operations + are rejected rather than stored as plaintext. Legacy plaintext remains readable, and + re-saving encrypts it when the key exists. Existing ciphertext requires the same key; + key rotation is unsupported. - **api: OAuth delete guard** — last-enabled OAuth provider cannot be deleted while `oauth-only` registration is on (would lock the system out). - **rdgen: production startup guard** — refuses to boot with default `SECRET_KEY` / @@ -52,13 +111,13 @@ across three migration steps; `AutoMigrate` is idempotent so all schema addition - **api: capability-URL TTL** — public download links (`/build/k/{key}` and `/build/dl/{key}`) now return 410 Gone after expiry; check centralized in `findBuildByDownloadKey`. (B-006) -- **api: GitHub artifact name fallback** — `DownloadArtifact` falls back to the sole - artifact of the run if the expected name doesn't match, so renaming the workflow no - longer breaks `tryGithubDownload`. (AU-L-011) -- **api: Linux + Android via GitHub Actions** — `submitBuild` routes `platform=linux` and - `platform=android` to `rustqs-linux.yml` / `rustqs-android.yml` workflow_dispatch when - GitHub config is present, matching the Windows path (B-012). Artifact name is now - dynamic (`rustdesk-min-test-{windows,linux,android}`). +- **Historical implementation note (superseded): api: GitHub artifact name fallback** — + the former `DownloadArtifact` behavior fell back to the sole artifact of the run when + the expected name did not match. The current path requires exact artifact identity. + (AU-L-011) +- **Historical implementation note (superseded): api: Linux + Android via GitHub Actions** — + routing and platform-specific artifact naming were added, but current production + capability remains gated until PR11 evidence validates the complete path. (B-012) - **admin-ui: build history auto-refresh** — the custom-client list polls every 5 s while any row is in `pending`/`running`, stops when all are terminal — no manual reload. - **admin-ui: dispatch test payload** — `Test dispatch` button on `github-build.vue` now @@ -108,7 +167,8 @@ across three migration steps; `AutoMigrate` is idempotent so all schema addition AU-M-022, AU-L-007/010/011/015 carried forward). ### Reference -- See `BUGS.md` for the current tracker (25 fixed, 4 partial, 2 open). +- Historical tracker snapshot (2026-06-22): 25 fixed, 4 partial, 2 open. The + current bounded `BUGS.md` snapshot contains 1 fixed, 4 partial, and 1 open entry. - See PR #30 for the batch-merge detail and the DatabaseVersion conflict resolution. --- @@ -323,12 +383,13 @@ The work surfaced four real bugs that would have broken the actual GUI build flo succeeded in about 33 minutes. Final artifact: **one file `rustqs.exe`, 23.2 MB** instead of a small launcher plus a DLL folder. Exe metadata is `rustqs` and `custom_.txt` is packed inside the self-extracting exe. -- First attempt [27462157839](https://github.com/bashrusakh/rustdesk/actions/runs/27462157839) +- **Historical implementation note (superseded):** First attempt + [27462157839](https://github.com/bashrusakh/rustdesk/actions/runs/27462157839) failed almost immediately with `bad decrypt` in `Resolve build config` because `WORKFLOW_PAYLOAD_KEY` in the fork had diverged from local `offline-kit/artifacts/workflow-payload.key`. Open inputs were used as a temporary bypass to - validate §8.10. Remaining TODO: resync the key, either from the UI (`Push to GitHub Secrets`) or - by replacing the local file. + validate §8.10; the active workflow no longer permits that fallback. Remaining TODO at the + time: resync the key, either from the UI (`Push to GitHub Secrets`) or by replacing the local file. ### Fixed (Docker build) - Added root **`.dockerignore`** to exclude `node_modules/`, `.git/`, `data/`, `rdgen-data/`, @@ -360,9 +421,10 @@ The work surfaced four real bugs that would have broken the actual GUI build flo Axios request with a 95-minute timeout and shows `Build running...` -> success/failure. ### Fixed (GitHub fork cleanup) -- Removed **10 upstream workflows** from `bashrusakh/rustdesk@master`: +- Historical cleanup removed **10 upstream workflows** from `bashrusakh/rustdesk@master`: `bridge.yml`, `ci.yml`, `clear-cache.yml`, `fdroid.yml`, `flutter-build.yml`, `flutter-ci.yml`, `flutter-nightly.yml`, `flutter-tag.yml`, `playground.yml`, `wf-cliprdr-ci.yml`. + These are deleted legacy references, not active workflow evidence. Kept only the needed ones: `rustqs-windows-min-test.yml` and `third-party-RustDeskTempTopMostWindow.yml`. - Restored **`bridge.yml`** after cleanup because `rustqs-windows-min-test.yml` uses it as a reusable workflow. Without it, dispatch failed with HTTP 422 / workflow parse error. Restored from upstream `rustdesk/rustdesk@1.4.7`. @@ -375,6 +437,11 @@ The work surfaced four real bugs that would have broken the actual GUI build flo ## [0.4.0] - 2026-06-11 +> **Historical/superseded release entry (2026-06-11):** The offline-kit, SMB +> builder, and L1 sovereignty statements below describe that dated state only. +> They are not current proof of offline completeness, active SMB operation, +> upstream independence, or sovereignty; see the current-state reconciliation above. + ### Changed (Architecture - Sovereign Build Strategy) - Fully rewrote **`PLAN.md`** as the single source of truth. It now documents the sovereign build model (3 levels of independence: sources / build / toolchain), fork map (`rustdesk` + `hbb_common` + @@ -400,7 +467,9 @@ The work surfaced four real bugs that would have broken the actual GUI build flo - Captured the full list of Windows `vcpkg` dependencies from `vcpkg.json`: `aom`, `libjpeg-turbo`, `opus`, `libvpx`, `libyuv`, `mfx-dispatch`, **`ffmpeg`** (`amf`/`nvcodec`/`qsv` for `hwcodec`). -### Done (Offline kit frozen) +### Done (Offline kit frozen — historical/superseded, 2026-06-11) +> This dated offline-kit record is historical/superseded and is not current proof of +> offline completeness, release readiness, upstream independence, or sovereignty. - Ran `freeze.sh` on 2026-06-11 in `docker-build-linux-1`. Frozen into `rustdesk-cache` volume: 1.4.7 bundle, `vendor` (2.7G, all `rustdesk-org/*` + `hbb_common`), Flutter engine, Flutter SDK for win+linux, `vcpkg` at baseline, Rust 1.75.0 MSI. Manifest with sha256 stored in `artifacts/MANIFEST.txt`. @@ -423,7 +492,9 @@ The work surfaced four real bugs that would have broken the actual GUI build flo - Added `.gitignore` to exclude secrets (private key `id_ed25519`, DB, `data/`, `.env`), build output, `node_modules`, `offline-kit/artifacts`, `.claude/`. Secret scan found no leaks in source. -### Changed (Windows builder: container -> native, owner decision) +### Changed (Windows builder: container -> native, owner decision — historical/superseded, 2026-06-11) +> The SMB queue and standalone builder described here are historical/frozen material, +> not the current provider-backed API workflow. - Final decision: build the Windows client **natively** on a separate Windows Server, no Docker. - API <-> agent channel is an **SMB job queue folder**. Linux hosts Samba, Windows mounts it. - Added `win-builder/setup.ps1` (toolchain, `-KitPath` support), `win-builder/agent.ps1` @@ -443,10 +514,13 @@ The work surfaced four real bugs that would have broken the actual GUI build flo - Generated 43-char `WORKFLOW_PAYLOAD_KEY` and stored it in fork GitHub Secrets. Important pitfall: `gh secret set ... --body -` via pipe adds a trailing newline under PowerShell, causing `bad decrypt` on the runner. Fix: use `--body $secret` without a pipe. -- Refactored the workflow to take `enc_payload` and resolve it through `Resolve build config` - (OpenSSL AES-256-CBC + PBKDF2 + `jq` -> env vars), while still supporting open inputs as fallback. +- **Historical implementation note (superseded):** Refactored the workflow to take + `enc_payload` and resolve it through `Resolve build config` (OpenSSL AES-256-CBC + PBKDF2 + + `jq` -> env vars), while the then-current version still supported open inputs as a fallback. + The active workflow now accepts only authenticated `DFP1` payloads. - Migrated L1/L2/L3 steps from `inputs.X` to `RQS_*` env vars and masked sensitive values with `::add-mask::`. -- Verified with successful runs for both open-inputs and encrypted payload. +- **Historical validation note (superseded):** Verified successful runs for both open-inputs + and encrypted payload. This does not validate the current DFP1-only path. **§8.8.5 Go API - SCAFFOLD** - Added `model/github_build_config.go`: singleton with `Token`, `PayloadKey`, and safe view. @@ -468,7 +542,8 @@ The work surfaced four real bugs that would have broken the actual GUI build flo `pollAndDownload` loop (30-second poll, 90-minute timeout). - On success it downloads `rustdesk-min-test-windows.zip`, unpacks it, stores `{appname}.exe` + DLLs + `custom_.txt` into `/rdgen-data/output/{id}/`, and updates `CustomBuild.Status`. -- Falls back to file-queue mode for Linux/Android. +- **Historical implementation note (superseded):** the earlier API fell back to + file-queue mode for Linux/Android; the current production path fails closed instead. **Self review §8.8.5** - Prevented panic in background goroutine `pollAndDownload` via `defer recover()`. @@ -519,10 +594,11 @@ The work surfaced four real bugs that would have broken the actual GUI build flo (Hyper-V VM / physical / cloud), long paths, antivirus exclusions, SMB, service agent, first end-to-end test, and security. -### Verified / Fixed (offline-kit) -- Proved **L1 sovereignty**: `cargo metadata --offline` on the vendored tree resolves all 1049 crates +### Verified / Fixed (offline-kit — historical/superseded; not current proof) +- **Historical L1 claim (superseded):** `cargo metadata --offline` on the vendored tree resolved all 1049 crates from `vendor` with no network access. -- Rebuilt and verified the **bundle** on full history (70M). Clone-back on tag 1.4.7 succeeded. +- **Historical bundle check (superseded):** rebuilt and verified the **bundle** on full history (70M). + Clone-back on the dated 1.4.7 tag succeeded. Fixed the earlier shallow-clone defect (`remote did not send all necessary objects`). ### Notes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 276eee9..60554e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,8 +111,11 @@ Co-Authored-By: Claude ## License & attribution (AGPL-3.0) -DeskForge is distributed under **AGPL-3.0** (because `server/` is AGPL-3.0 and it's -the strongest copyleft in the bundle). +The covered DeskForge distribution is identified as **AGPL-3.0** because `server/` is +AGPL-3.0 and is the strongest copyleft in the covered bundle. This does not relicense +separate or independent components; preserve each component's applicable license and +notice. The license inventory is incomplete; no signatures or attestations are recorded, +and no full sovereignty claim is made. When you add new files derived from upstream sources: @@ -132,12 +135,25 @@ To run the server stack: ```bash cd docker -docker compose build server # rebuilds Go code; admin-ui needs pre-built dist -docker compose up -d server -docker compose logs -f server +docker compose build rustdesk # build the combined server image +docker compose up -d rustdesk +docker compose logs -f rustdesk ``` -For the GitHub-based Windows client builder workflow — see [PLAN.md](PLAN.md) §8.8. +For GitHub-based client-build workflow maintenance, the configured RustDesk fork's +`.github/workflows/` files are the sole active executable workflow source. See +[PLAN.md](PLAN.md) §7 for the historical maintenance notes. `github-build/` is +frozen reference/documentation material only, and `rdgen/` is frozen vendored +historical/reference material; neither is an active workflow source. + +The published RustDesk client source/ref is 1.4.8 and the published DeskForge API +schema is `DatabaseVersion` 272. The current local corrective candidate targets API +schema 283, which is not published. Schema 283 adds +`idx_custom_presets_user_id_name` on `(user_id, name)` and fails migration preflight +on existing duplicate groups without auto-selecting a row or deleting data. Schema 282 remains +the earlier workflow-approval migration. Live provider execution remains unverified. +Migration evidence is SQLite-only; MySQL/PostgreSQL migration/read/write coverage +remains unverified. ## What goes where @@ -148,9 +164,15 @@ For the GitHub-based Windows client builder workflow — see [PLAN.md](PLAN.md) | `admin-ui/` | Vue 3 admin panel. MIT. | | `libs/` | Shared Rust libs. | | `docker/` | Dockerfiles + compose. | -| `github-build/` | Workflow + docs for building Windows client via GitHub Actions. | -| `win-builder/` | Native Windows build agent (fallback path, frozen). | -| `offline-kit/` | Frozen toolchain + sources (sovereign build kit). | -| `rdgen/` | Vendored reference: rdgen workflow patches (not running as a service). GPL-3.0. | +| `github-build/` | Frozen reference/documentation for fork workflows; no executable workflow copies. | +| `win-builder/` | Frozen manual/historical-only Windows build material; not the API path. | +| `offline-kit/` | Frozen dependency-freeze tool; verification and license inventory are incomplete, with no signature/attestation or full-sovereignty claim. | +| `rdgen/` | Frozen vendored historical/reference workflow material, not the active source; not running as a service. GPL-3.0. | | `PLAN.md` | Single source of truth for the project plan. | | `CHANGELOG.md` | Chronological log of changes. | + +The current repository has no tracked `vendor/` tree, `rustdesk-deps/` archive, or +client-release directory. Rust and Go manifests still use active third-party upstream +dependencies, and the configured RustDesk fork's `hbb_common` submodule is currently +upstream-referenced, dirty, and unpublished; upstream independence and MySQL/PostgreSQL +cross-database support therefore remain unverified. diff --git a/NOTICE b/NOTICE index b61240e..19a6860 100644 --- a/NOTICE +++ b/NOTICE @@ -20,7 +20,10 @@ This product includes software developed by: https://github.com/bryangerlach/rdgen Used as reference: build workflow patches (not as a running service). -The combined work is distributed under AGPL-3.0 (because server/ is AGPL-3.0). -Per-component LICENSE files retained in their respective subdirectories. +The covered DeskForge work is identified as AGPL-3.0 because `server/` is AGPL-3.0. +This does not relicense separate or independent components; their applicable licenses +and notices remain in force. The current license inventory is incomplete, with no +signatures or attestations recorded and no full sovereignty claim. +Per-component LICENSE files are retained in their respective subdirectories. This is a modified version. Original RustDesk: https://github.com/rustdesk/rustdesk diff --git a/PLAN.md b/PLAN.md index 97a8e40..33b7fba 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,21 +1,57 @@ # PLAN.md — DeskForge: Single Source of Truth -> Last updated: 2026-06-28 +> Last updated: 2026-08-20 > Related: [CHANGELOG.md](CHANGELOG.md) · [BUGS.md](BUGS.md) · [CONTRIBUTING.md](CONTRIBUTING.md) +> **Evidence labels (2026-08-20):** The published RustDesk client source/ref is +> 1.4.8 and the published DeskForge API schema is `DatabaseVersion` 272. The current +> local corrective candidate targets API schema 283; it is not a public current-schema +> or release claim. Schema 283 adds `idx_custom_presets_user_id_name` on +> `(user_id, name)` and fails preflight on existing duplicate groups before +> `AutoMigrate`, without auto-selecting a row or deleting data. Schema 282 is the earlier +> workflow-approval migration preserved as history. +> The current API path is provider dispatch → exact run/artifact retrieval → local +> validation and publication. Runner callbacks, local file-queue fallback, and +> sole-artifact selection are not current production behavior. Windows is the only +> production capability admitted by the API gate; Linux/Android remain gated pending +> PR11 evidence. The dispatch contract under test requires +> `return_run_details=true` and an exact HTTP 200 run-details response containing +> the run identity; standard 204 is intentionally unsupported because it cannot +> correlate the exact run. No normal GitHub operation is verified. The Go API owns +> port 21114; Rust services use 21115–21119, with relay WebSocket on 21119; current +> production Compose exposure remains +> 21114–21118. No live provider run or clean-environment build proof is claimed. +> Workflow approval requires a provider-derived verified annotated tag, the +> aggregate of all applicable active protected-tag rulesets with effective update +> and deletion protections, no bypass actors, and rejection of tag/branch label +> collisions. At the resolved immutable workflow SHA, the exact provider-owned +> `# deskforge-workflow-identity-guard: v1` marker is required before approval, +> preparation, or secret-bearing dispatch; legacy unguarded tags fail closed. The +> local gate is not +> live-provider evidence. Its pending → building identity write is atomic, but +> provider dispatch and that database write are not an end-to-end atomic +> transaction: there is no durable outbox or distributed lease for the +> post-dispatch database-failure window. + --- ## 0. Project goal Self-hosted RustDesk server (hbbs/hbbr + API + admin panel) + **custom client builder** -that works without `rustdesk/rustdesk`, `rustdesk-org/*`, or `rustdesk.com`. +that is intended to reduce dependence on `rustdesk/rustdesk`, `rustdesk-org/*`, and +`rustdesk.com`; current source manifests still reference active third-party upstream +repositories, so full upstream independence is not verified. -**Active client build path:** GitHub Actions in the rustdesk fork. `win-builder/` -and `linux-build` are frozen fallbacks. +**Active client build path:** the API dispatches an owned workflow in the +configured RustDesk fork, polls the provider run, downloads the exact artifact +through the provider API, then validates, extracts, and publishes it locally. +`github-build/` and `rdgen/` are reference/documentation material only; +`win-builder/` and `linux-build` are manual/historical-only builders outside the +production API path. GitHub-first because: - free Windows runners -- fork is ready, min-test is green +- fork workflow is configured; the min-test result below is historical - standalone requires a separate Windows Server, not deployed --- @@ -25,46 +61,107 @@ GitHub-first because: ``` bashrusakh/ ├── DeskForge ← this repo (server, api, admin, docker) -├── rustdesk ← fork of rustdesk/rustdesk, tag 1.4.7 → 1.4.8 -│ ├── vendor/ ← cargo vendor (L1, ~20 rustdesk-org deps) -│ ├── .github/workflows/ ← rustqs-windows-min-test.yml, rustqs-linux.yml, rustqs-android.yml -│ └── releases/ ← offline-assets-1.4.7 (engine, usbmmidd, drivers) -├── hbb_common ← fork of rustdesk/hbb_common (required submodule) -└── rustdesk-deps/ ← archive of ~20 rustdesk-org repos (L1 backup) +├── rustdesk ← owned RustDesk fork and active workflow source +│ └── .github/workflows/ ← rustqs-windows.yml, rustqs-linux.yml, rustqs-android.yml +└── libs/hbb_common ← tracked shared source in DeskForge; not a submodule here ``` -**Current versions:** fork at 1.4.7 (tag), workflow updated for 1.4.8 (chore/bump-client-1.4.8). +**Published client source/ref:** 1.4.8, aligned with `offline-kit/versions.env`. The +current local corrective candidate targets API schema 283; the published DeskForge +schema remains `DatabaseVersion` 272. Schema 283 adds +`idx_custom_presets_user_id_name` on `(user_id, name)` and fails preflight on existing +duplicate groups before `AutoMigrate`, without auto-selecting a row or deleting data. Schema 282 +remains the earlier workflow-approval migration. +The configured RustDesk fork's `libs/hbb_common` is a separate git submodule currently +recorded against upstream `rustdesk/hbb_common`; its local checkout is dirty and +unpublished. Active RustDesk and DeskForge manifests also reference third-party upstream +repositories/modules. Provider-side live execution and clean-build proof are not recorded +here. On 2026-08-11, `GOWORK=off go vet ./...` and `GOWORK=off go test ./...` +passed after test-only cache diagnostics and opt-in Redis test changes. Redis +integration tests and benchmarks run only when `DESKFORGE_TEST_REDIS_ADDR` is +configured; no live Redis run is recorded. MySQL/PostgreSQL, live provider, and +the other release/build gates remain unverified. DeskForge does not currently track a repository `vendor/` tree, client-release +directory, or `rustdesk-deps/` archive. + +The combined DeskForge distribution is identified as AGPL-3.0 for the covered work; +separate or independent API/UI/reference components retain their applicable upstream +licenses and notices. The license inventory is incomplete; no signatures or attestations +are recorded, and no full sovereignty claim is made. See [LICENSE](LICENSE) and [NOTICE](NOTICE). --- ## 2. Architecture -### Active path (GitHub Actions) +### Current path (GitHub Actions) ``` admin-ui (Custom Client form) - ↓ POST /custom_build + ↓ custom-build request Go API (DeskForge) - ↓ workflow_dispatch + enc_payload (AES-256-CBC + PBKDF2) -GitHub Actions [rustdesk fork, windows-2022] + ↓ workflow_dispatch + provider-derived public workflow_sha + authenticated DFP1 enc_payload (AES-256-CBC + PBKDF2 + HMAC) +GitHub Actions [configured RustDesk fork, owned platform workflow] ↓ L1: config.rs (server + key) ↓ L2: custom_.txt (permanent password, allowCustom patch) ↓ L3: branding (rustqs, portable-packer) - ↓ POST /api/save_custom_client (encrypted) -Go API → /rdgen-data/output/{id}/ → admin-ui Download +provider run ← Go API polls provider status +provider artifact API → Go API downloads the exact artifact +Go API validates/extracts/publishes locally → admin-ui Download ``` **Security:** password never published — `enc_payload`, decrypted inside runner via -GitHub Secret `WORKFLOW_PAYLOAD_KEY`. Binary goes to your server, not a public release. - -### Fallback (frozen, do not deploy) +GitHub Secret `WORKFLOW_PAYLOAD_KEY`. Before approval, preparation, or secret-bearing +dispatch, the API requires the exact provider-owned +`# deskforge-workflow-identity-guard: v1` marker in workflow content resolved at the +immutable workflow SHA; legacy unguarded tags fail closed. The provider-derived outer +`workflow_sha` is checked against `github.sha` before secret-bearing jobs; the same +authenticated inner payload field is checked again before exports, checkout, or build +use. This is defense in depth, not an atomic defense against a malicious workflow file +or GitHub's selector/SHA binding; the verified tag and active no-bypass ruleset remain +required controls. The runner does not callback to the API; the API retrieves the +artifact through the provider API and publishes it locally, not to a public release. + +### Workflow approval and schema evidence + +Before a provider-backed build can run, the API accepts only a provider-derived +verified annotated tag. The provider must report an accepted verification reason, +the aggregate of all applicable active protected tag rulesets for that label with +effective update and deletion protections, and no bypass actors. A tag/branch +collision with the same label is rejected. At the resolved immutable workflow SHA, +the exact provider-owned `# deskforge-workflow-identity-guard: v1` marker is also +required before approval, preparation, or secret-bearing dispatch; legacy unguarded +tags fail closed. Raw refs, SHAs, credentials, and workflow internals are not normal +UI inputs. + +The published DeskForge schema is `DatabaseVersion` **272**. The current local +corrective candidate targets schema **283**, which is not published or live-provider +evidence. The historical workflow-approval additive fields are **280** +(`workflow_ref_approved`, approval status), **281** +(`workflow_ref_provider_verified`, provider-policy status), and **282** +(`workflow_ref_approval_sha`, the provider-resolved approval commit). Schema **283** +adds the `idx_custom_presets_user_id_name` composite index on `(user_id, name)`. +Before `AutoMigrate`, migration preflight fails if it finds an existing duplicate +group; it does not automatically select or delete a row. Legacy metadata-only +configuration saves remain compatible with plaintext secret rows: they can preserve +legacy values without `SECRET_ENCRYPTION_KEY`; new or replaced non-empty secret writes +require the key, and a resave with the key encrypts legacy plaintext values. + +Focused fake-transport and SQLite checks do not prove live GitHub/provider +approval, dispatch, polling, or artifact delivery. Dispatch requires the exact +HTTP 200 run-details contract (`return_run_details=true`), not a standard 204 or +latest-run inference. The local identity write is atomic, but no durable outbox +or distributed lease makes provider dispatch plus the database write one atomic +operation; a post-dispatch database failure remains a documented limitation. + +### Historical/frozen builders (not used by the current API) ``` admin-ui → Go API → jobs/{id}.json → SMB share → standalone Windows builder or Docker linux-build ``` -Not active: `win-builder/` untested (no Windows host), `build-linux` behind `--profile fallback`. +These local-queue builders are not a production fallback for the current API. +They remain only as historical/frozen material and must not be treated as an +alternative workflow source. --- @@ -72,14 +169,16 @@ Not active: `win-builder/` untested (no Windows host), `build-linux` behind `--p | Component | Status | Notes | | ------------------------------ | -------------- | ------------------------------------ | -| hbbs/hbbr (Rust) | ✅ running | ports 21114-21118 | +| hbbs/hbbr (Rust) | ✅ running | ports 21115-21119; relay WebSocket is 21119 | | Go API | ✅ running | users, address book, OAuth, LDAP, audit | | Admin UI (Vue 3) | ✅ running | 16 pages, 3 locales, DataTable, FilterBar | -| GitHub build (Windows) | ✅ active | min-test green, 3 layers, encryption | -| GitHub build (Linux) | 🟡 draft | workflow exists, not CI-validated | -| GitHub build (Android) | 🟡 draft | workflow exists, not CI-validated | -| win-builder standalone | ❄️ frozen | do not deploy, no Windows host | -| linux-build (Docker) | ❄️ frozen | manual fallback, `--profile fallback` | +| GitHub build (Windows) | 🟡 implemented | API path is provider-backed; no current live provider/clean-build proof | +| GitHub build (Linux) | 🟡 gated | workflow mapping exists; production capability remains disabled | +| GitHub build (Android) | 🟡 gated | workflow mapping exists; production capability remains disabled | +| `github-build/` | 📘 reference | documentation only; no executable workflow copies | +| `rdgen/` | 📘 reference | vendored historical/reference material, not the active source | +| win-builder standalone | ❄️ frozen | manual/historical only; not API path | +| linux-build (Docker) | ❄️ frozen | manual/historical only; not API path | | offline-kit | ❄️ frozen | re-freeze when client version changes | --- @@ -92,33 +191,43 @@ Not active: `win-builder/` untested (no Windows host), `build-linux` behind `--p | L2 | quick-support password| `custom_.txt` (signature checked — `allowCustom.py` patch removes check) | | L3 | branding (rustqs) | `Cargo.toml`, `Runner.rc`, portable-packer (`libs/portable/generate.py`) | -Full recipe: `rdgen/.github/workflows/generator-windows.yml` (vendored reference). +Historical reference: `rdgen/.github/workflows/generator-windows.yml`. +It is not the executable workflow source and must not be copied into the fork. --- ## 5. ✅ Completed milestones -- [x] Forks of rustdesk + hbb_common (1.4.7) -- [x] Offline kit: L1+L3, 11 artifacts, 5 GB (frozen) -- [x] GitHub min-test Windows: green, ~33 min, single-binary rustqs.exe -- [x] Go API: workflow_dispatch, poll, download, capability-URL TTL +- [x] Published RustDesk client source/ref is 1.4.8; current local corrective API schema target is + `DatabaseVersion 283`, while the published DeskForge schema remains 272 +- [x] Offline-kit freeze/verify tooling and local artifact set retained; real-kit + verification remains blocked under PR10 +- [x] GitHub min-test Windows: historical green run, ~33 min, single-binary rustqs.exe +- [x] Go API: provider workflow dispatch and polling; provider API artifact download; + local validation, extraction, publication, and capability-URL TTL - [x] Admin UI redesign: design tokens, DataTable, AppDialog, FilterBar, 16 pages - [x] Security: encrypted-at-rest (AES-GCM), OAuth delete guard, audit, TTL - [x] Rust server: atomic blocklist, aur-fix, JWT -- [x] Database: ~272 migrations, SQLite/MySQL/PostgreSQL +- [x] Current local schema target: `DatabaseVersion 283`; its composite-index migration + is exercised only on SQLite, while MySQL/PostgreSQL migration/read/write support + remains unverified --- ## 6. Open roadmap -- [ ] **Linux + Android GitHub workflows** — CI validation + platform picker in UI +- [ ] **Linux + Android capability validation** — live workflow/artifact evidence + guarded platform picker - [ ] **Full client rebrand** — About, URLs, icons — in workflow, not in the fork - [ ] **Smoke test** for built binary (`--version`) - [ ] **Ballast cleanup** — remove MinGW leftovers, test containers --- -## 7. Workflow: new upstream rustdesk-client release +## 7. Historical fork maintenance notes for a new upstream rustdesk-client release + +The steps below are historical maintenance notes for the owned RustDesk fork. They are not +the DeskForge build execution path; active workflow files live only in that +fork. Do not copy workflow files from `github-build/` or `rdgen/`. When `rustdesk/rustdesk` publishes a new tag (e.g. 1.5.0), follow these steps: @@ -147,7 +256,7 @@ git commit -m "chore: point hbb_common to v1.5.0" git push origin v1.5.0 ``` -### 7.3. Update vendor +### 7.3. Update vendor (historical procedure; not current repository state) ```bash # On a machine with Rust: @@ -156,7 +265,8 @@ git add vendor/ && git commit -m "chore: vendor deps for v1.5.0" git push origin v1.5.0 ``` -Or if vendor is too heavy — upload `vendor-1.5.0.tar.gz` as a release asset. +Or if vendor is too heavy — historically, upload `vendor-1.5.0.tar.gz` as a release +asset. DeskForge does not currently contain that vendor tree or release asset. ### 7.4. Update offline-kit @@ -166,7 +276,7 @@ cd DeskForge/offline-kit bash freeze.sh source vendor engine ``` -### 7.5. Update offline-assets release +### 7.5. Update offline-assets release (historical operator procedure) ```bash # Upload engine/usbmmidd/driver to the fork: @@ -176,13 +286,14 @@ gh release create offline-assets-1.5.0 --repo bashrusakh/rustdesk \ artifacts/rustdesk_printer_driver_v4-1.4.zip artifacts/printer_driver_adapter.zip ``` -> **Note:** After publishing the release, version `1.5.0` will automatically appear in the admin UI -> (the `GET /api/admin/custom_build/versions` endpoint fetches fork releases tagged -> `offline-assets-*`). No hardcoded values in UI or YAML need to be changed. +> **Note:** After publishing the release, the version catalog can expose it in +> the admin UI when the configured repository contains the matching +> `offline-assets-*` release and source tag. The catalog is repository-derived; +> there is no hardcoded repository or obsolete version fallback list. ### 7.6. Adapt workflow -Compare upstream `build-for-windows-flutter` with `rustqs-windows-min-test.yml`: +Compare upstream `build-for-windows-flutter` with `rustqs-windows.yml`: - New system dependencies? - Changed `build.py` flags? - Changed `config.rs` / `custom_.txt` format? @@ -192,35 +303,12 @@ Port changes to the fork workflow. > **Important:** `bridge.yml` must stay **without `inputs.version`** — same as upstream. > Bridge and build must work from the same code (the fork). Do not add `repository:` to checkout. -### 7.7. Deploy workflows to fork (3 branches) +### 7.7. Workflow ownership -Workflow files live on three fork branches. After changes, update all: - -```bash -# 1) rustqs/min-test — execution (all dispatches go here) -git checkout rustqs/min-test -cp /path/to/DeskForge/github-build/rustqs-*.yml .github/workflows/ -cp /path/to/DeskForge/rdgen/.github/workflows/bridge.yml .github/workflows/ -git add .github/workflows/ -git commit -m "feat: update rustqs-* workflows for v1.5.0" -git push origin rustqs/min-test - -# 2) master — API discovery (workflow must exist on default branch) -git checkout master -cp /path/to/DeskForge/github-build/rustqs-*.yml .github/workflows/ -cp /path/to/DeskForge/rdgen/.github/workflows/bridge.yml .github/workflows/ -git add .github/workflows/ -git commit -m "feat: update rustqs-* workflows for v1.5.0" -git push origin master - -# 3) rustqs/master-workflows — mirror (backup for upstream sync) -git checkout rustqs/master-workflows -cp /path/to/DeskForge/github-build/rustqs-*.yml .github/workflows/ -cp /path/to/DeskForge/rdgen/.github/workflows/bridge.yml .github/workflows/ -git add .github/workflows/ -git commit -m "feat: update rustqs-* workflows for v1.5.0" -git push origin rustqs/master-workflows -``` +The `.github/workflows/` files in the owned RustDesk fork are the sole +executable source. `github-build/` is reference/documentation only, and the +vendored `rdgen` workflows are historical/reference material. Maintain active +workflow logic in the fork; do not create deployment copies in this repository. ### 7.8. Verify @@ -244,12 +332,14 @@ git push origin rustqs/master-workflows | Entity | What it is | Where stored | | ----------------------- | ---------------------------------------------------- | ------------------------------------- | | `offline-kit/` | Scripts (`freeze.sh`) + config (`versions.env`) | In git, in this repo | -| `offline-kit/artifacts/`| Output of freeze.sh: vendor.tar.gz, engine, SDK, MSI | Locally, **not in git** | -| `offline-assets-{tag}` | GitHub Release with binaries for CI | GitHub Releases of the rustdesk fork | - -**Why:** without this insurance, if `rustdesk/rustdesk` gets deleted or `rustdesk.com` -goes down, building a custom client becomes impossible. The kit freezes everything -while upstream is still available. +| `offline-kit/artifacts/`| Local freeze output (ignored/untracked) | Local operator storage only | +| `offline-assets-{tag}` | Provider-side asset naming convention | Availability and release proof unverified | + +**Why:** without this insurance, loss of upstream source or services can block a custom +client build. The kit is frozen local operator material; active third-party upstream +dependencies remain in the source manifests, and its current incomplete verification +does not prove upstream independence, sovereignty, a network-denied full build, a +signature, or release readiness. --- diff --git a/README.md b/README.md index a242ddd..cbdd8b9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ - - # DeskForge Unified self-hosted RustDesk-compatible server. @@ -14,13 +12,13 @@ git clone https://github.com/bashrusakh/DeskForge.git cd DeskForge/docker # docker-compose.yml: replace your-server, your-secret-jwt-key-change-this docker compose up -d -# Get the public key: -docker compose logs | grep "Public Key" +# Read the public key from the mounted server data path: +docker compose exec rustdesk cat /data/id_ed25519.pub ``` **Admin:** `http://your-server:21114/admin/` — login `admin`, password in logs. -**RustDesk client:** ID Server `your-server:21116`, Relay `:21117`, API `http://your-server:21114`, Key — from logs. +**RustDesk client:** ID Server `your-server:21116`, Relay `your-server:21117`, API `http://your-server:21114`, Key — paste the public-key contents returned by `docker compose exec rustdesk cat /data/id_ed25519.pub`; the file is at `/data/id_ed25519.pub` inside the container. --- @@ -33,6 +31,18 @@ docker compose logs | grep "Public Key" | 21116 | TCP/UDP | ID Server (hbbs) | | 21117 | TCP | Relay Server (hbbr) | | 21118 | TCP | WebSocket | +| 21119 | TCP | Relay WebSocket (hbbr) | + +The protocol uses 21119 for the relay WebSocket path. The production Compose +file currently publishes 21114–21118; expose 21119 separately when that public +WebSocket path is required. + +The published RustDesk client source/ref is 1.4.8 and the published DeskForge API +schema is `DatabaseVersion` 272. The current local corrective candidate targets API +schema 283; it is not published or live-provider evidence. Schema 283 adds +`idx_custom_presets_user_id_name` on `(user_id, name)`. Before `AutoMigrate`, its +preflight fails on an existing duplicate group without auto-selecting a row or deleting +data. Schema 282 remains the earlier workflow-approval migration. --- @@ -41,6 +51,8 @@ docker compose logs | grep "Public Key" | Variable | Purpose | | ---------------------------------- | --------------------------------- | | `RELAY` | Relay server address | +| `HBBR_PORT` | Relay server port | +| `HBBS_PORT` | ID/Rendezvous server port | | `ENCRYPTED_ONLY` | Encrypted connections only | | `MUST_LOGIN` | Require login before connect | | `RUSTDESK_API_RUSTDESK_ID_SERVER` | ID server (hbbs) | @@ -48,9 +60,19 @@ docker compose logs | grep "Public Key" | `RUSTDESK_API_RUSTDESK_API_SERVER` | API server URL | | `RUSTDESK_API_KEY_FILE` | Path to public key file | | `RUSTDESK_API_JWT_KEY` | JWT secret | -| `RUSTDESK_API_GORM_TYPE` | sqlite / mysql / postgres | +| `RUSTDESK_API_GORM_TYPE` | sqlite / mysql / postgresql | | `RUSTDESK_API_LANG` | en / ru / zh-CN | -| `SECRET_CRYPT_KEY` | AES-GCM key for secrets at rest | +| `SECRET_ENCRYPTION_KEY` | AES-GCM key for secrets at rest | +| `SOURCE_DATE_EPOCH` | Trusted Docker build timestamp (optional) | + +New non-empty secret writes and secret-bearing Custom Builder operations require this +key and are rejected rather than stored as plaintext when it is missing. Legacy plaintext +rows remain readable; saving them again encrypts them when the key exists. + +Rust build metadata uses `SOURCE_DATE_EPOCH` when supplied. Without it, active +builds use the deterministic value `unknown`; wall-clock metadata is available +only for explicitly non-reproducible local debug builds with +`RUSTDESK_NON_REPRODUCIBLE_DEBUG=1`. --- @@ -58,17 +80,39 @@ docker compose logs | grep "Public Key" **Server (Rust + Go):** user CRUD, JWT, OAuth (GitHub/Google/OIDC), LDAP, groups, tags, address book (personal + shared with collections), peer-UUID binding, audit (login/connection/file-transfer), -server commands with persistence and audit log, encrypted-at-rest secrets, SQLite/MySQL/PostgreSQL, -captcha + brute-force protection. +server commands with persistence and audit log, encrypted-at-rest secrets, and configured +SQLite/MySQL/PostgreSQL drivers. SQLite is locally exercised; MySQL/PostgreSQL migration and +read/write coverage remain unverified. Captcha and brute-force protection are also included. **Admin UI (Vue 3):** Login, Dashboard, Devices, Users, Groups, Tags, OAuth, Server Config, Audit, Custom Client Builder, Profile, My Workspace, Guest Sharing. -3 locales (en/ru/en/ru/zh_CN). Light/Dark/Auto themes. +3 locales (en/ru/zh_CN). Light/Dark/Auto themes. Shared UI: DataTable, AppDialog, AppDrawer, FilterBar, ActionsToolbar. -**Custom client:** GitHub Actions → rustdesk fork → `rustqs.exe` (Windows). -Builds with your server, key, permanent password. Single-binary via portable-packer (23 MB). -Linux/Android — in development. +**Custom client:** the API dispatches an owned workflow in the configured RustDesk +fork and retrieves the exact provider artifact for local validation/publication. +The published client source/ref is **1.4.8**. The current local corrective candidate +targets API schema `DatabaseVersion 283`; the published DeskForge schema remains 272. +Schema 283 adds `idx_custom_presets_user_id_name` on `(user_id, name)` and fails +preflight on existing duplicate groups rather than auto-selecting a row or deleting data. +The earlier schema-282 workflow-approval migration remains historical context. +Windows is the only capability admitted today; +Linux and Android mappings remain gated pending PR11 evidence. No live provider +run or clean-environment build is claimed. Local workflow manifests, bridge/helper +source, Android app-ID/runtime-path checks, and package assertions are static evidence +only; no APK/package/install/runtime evidence exists. +Before approval, preparation, or secret-bearing dispatch, the API requires the exact +provider-owned marker `# deskforge-workflow-identity-guard: v1` in workflow content +resolved at the immutable workflow SHA. Legacy unguarded tags fail closed. This does +not make GitHub's selector/SHA binding atomic or establish live provider readiness. +Schema-283 migration checks are SQLite-only; MySQL/PostgreSQL migration and read/write +coverage remain unverified. + +### GitHub Actions PAT permissions + +See the [fine-grained PAT permission checklist](api/README.md#github-fine-grained-pat-permissions) +for the custom-client workflow, including why Administration, Actions, and +Secrets write access are required. **Not implemented (vs RustDesk Pro):** 2FA, RBAC, session recording, device policy, remote script, HA, backup/restore. @@ -87,39 +131,79 @@ HA, backup/restore. server/ — Rust hbbs/hbbr (signal + relay) api/ — Go REST API (Gin + GORM) admin-ui/ — Vue 3 + Element Plus admin panel -libs/hbb_common/ — shared Rust library (submodule) +libs/hbb_common/ — tracked shared Rust library in DeskForge docker/ — Dockerfile + compose + entrypoint -github-build/ — active client build workflow -win-builder/ — ❄️ FROZEN: standalone Windows builder -offline-kit/ — ❄️ FROZEN: dependency freeze tool (insurance against upstream death) -rdgen/ — vendored reference workflow (not a service) +github-build/ — reference/documentation for fork workflows (no executable copies) +win-builder/ — ❄️ FROZEN: manual/historical-only standalone builder +offline-kit/ — ❄️ FROZEN: dependency-freeze tool; current verification is incomplete +rdgen/ — ❄️ FROZEN: vendored historical/reference workflow material ``` +The configured RustDesk fork's `.github/workflows/` files are the sole executable +source for active client builds. `github-build/` and `rdgen/` contain reference or +frozen material only. + +The repository does not currently contain a `vendor/` tree, `rustdesk-deps/` archive, +or current client-release directory. The RustDesk fork and DeskForge server manifests +still reference active third-party upstream repositories/modules; upstream independence +is therefore a goal and boundary, not a verified current property. + --- ## Building ```bash cd docker -docker compose build # full build -docker compose up -d # start -docker compose -f docker-compose-dev.yaml up -d # dev +docker compose build rustdesk # full server image +docker compose up -d rustdesk # start the rustdesk service +``` + +For client-build workflow maintenance, edit and validate the configured fork's +`.github/workflows/` files. `github-build/` contains reference documentation only; +it is not a deployment source. For the API development stack: + +```bash +cd api +docker compose -f docker-compose-dev.yaml up -d ``` +### Go verification + +The broad local checks pass with the workspace disabled: + +```bash +cd api +GOWORK=off go vet ./... +GOWORK=off go test ./... +``` + +Redis integration tests and benchmarks are opt-in and run only when +`DESKFORGE_TEST_REDIS_ADDR` is configured. No live Redis run is recorded; +MySQL/PostgreSQL, live provider, and the other release/build gates remain +unverified. + --- ## Forks (for custom client builds) -- [`bashrusakh/rustdesk`](https://github.com/bashrusakh/rustdesk) — fork of rustdesk/rustdesk @ 1.4.7 -- [`bashrusakh/hbb_common`](https://github.com/bashrusakh/hbb_common) — fork of rustdesk/hbb_common +- [`bashrusakh/rustdesk`](https://github.com/bashrusakh/rustdesk) — owned RustDesk fork at published client source/ref 1.4.8; current local corrective API schema target is 283 +- [`bashrusakh/hbb_common`](https://github.com/bashrusakh/hbb_common) — intended fork of + `rustdesk/hbb_common`; the configured RustDesk fork currently records the upstream + `rustdesk/hbb_common` submodule, and its local checkout is dirty/unpublished, so clean + fork provenance is not verified -See [PLAN.md §7](PLAN.md#7-workflow-new-upstream-rustdesk-client-release) for the upstream update workflow. +See [PLAN.md §7](PLAN.md#7-historical-fork-maintenance-notes-for-a-new-upstream-rustdesk-client-release) for the upstream update workflow. --- ## License -AGPL-3.0 (server) + MIT (api/admin-ui). See [LICENSE](LICENSE) and [NOTICE](NOTICE). +The combined DeskForge distribution is identified as **AGPL-3.0** for the covered +DeskForge work. This does not relicense separate or independent components: component-level +notices and upstream licenses remain documented in [LICENSE](LICENSE) and [NOTICE](NOTICE), +and the API/admin UI retain their upstream MIT notices where applicable. The license +inventory is incomplete; no signatures or attestations are recorded, and no full +sovereignty claim is made. Based on: - [rustdesk/rustdesk-server](https://github.com/rustdesk/rustdesk-server) (AGPL-3.0) diff --git a/admin-ui/src/api/custom_client.js b/admin-ui/src/api/custom_client.js index f756f4f..607904f 100644 --- a/admin-ui/src/api/custom_client.js +++ b/admin-ui/src/api/custom_client.js @@ -12,6 +12,7 @@ export function create (data) { url: '/custom_build/create', method: 'post', data, + useServerErrorMessage: true, }) } @@ -23,14 +24,18 @@ export function remove (data) { }) } -export function detailByKey (key) { +export function download (id) { return request({ - url: '/custom_build/public/detailByKey/' + key, + url: `/custom_build/download/${id}`, + responseType: 'blob', + useServerErrorMessage: true, }) } export function getVersions () { return request({ url: '/custom_build/versions', + skipErrorMessage: true, + useServerErrorMessage: true, }) } diff --git a/admin-ui/src/api/github_build_config.js b/admin-ui/src/api/github_build_config.js index 4fe1a65..616117f 100644 --- a/admin-ui/src/api/github_build_config.js +++ b/admin-ui/src/api/github_build_config.js @@ -1,30 +1,46 @@ import request from '@/utils/request' export function get () { - return request({ url: '/github_build_config/get' }) + return request({ url: '/github_build_config/get', skipErrorMessage: true, useServerErrorMessage: true }) } export function save (data) { - return request({ url: '/github_build_config/save', method: 'post', data }) + return request({ url: '/github_build_config/save', method: 'post', data, skipErrorMessage: true, useServerErrorMessage: true }) +} + +export function getWorkflowTags () { + return request({ url: '/github_build_config/workflow_tags', skipErrorMessage: true, useServerErrorMessage: true }) +} + +export function approveWorkflowRef (workflowTag) { + return request({ + url: '/github_build_config/approve_workflow_ref', + method: 'post', + data: { confirm: true, workflow_tag: workflowTag }, + skipErrorMessage: true, + useServerErrorMessage: true, + }) } export function generateKey () { - return request({ url: '/github_build_config/generate_key', method: 'post' }) + return request({ url: '/github_build_config/generate_key', method: 'post', skipErrorMessage: true, useServerErrorMessage: true }) } export function test () { - return request({ url: '/github_build_config/test', method: 'post' }) + return request({ url: '/github_build_config/test', method: 'post', skipErrorMessage: true, useServerErrorMessage: true }) } export function syncSecret () { - return request({ url: '/github_build_config/sync_secret', method: 'post' }) -} - -export function syncPat () { - return request({ url: '/github_build_config/sync_pat', method: 'post' }) + return request({ url: '/github_build_config/sync_secret', method: 'post', skipErrorMessage: true, useServerErrorMessage: true }) } export function dispatchTest () { // B-009: confirm=true — это реальный билд (тратит минуты Actions), не дешёвый чек. - return request({ url: '/github_build_config/dispatch_test', method: 'post', data: { confirm: true } }) + return request({ + url: '/github_build_config/dispatch_test', + method: 'post', + data: { confirm: true }, + skipErrorMessage: true, + useServerErrorMessage: true, + }) } diff --git a/admin-ui/src/api/user.js b/admin-ui/src/api/user.js index 2e40595..bc5e3e7 100644 --- a/admin-ui/src/api/user.js +++ b/admin-ui/src/api/user.js @@ -8,6 +8,16 @@ export function login (data) { }) } +export function logout () { + return request({ + url: '/logout', + method: 'post', + timeout: 5000, + skipErrorMessage: true, + skipAuthRedirect: true, + }) +} + export function current () { return request({ url: '/user/current', diff --git a/admin-ui/src/components/changePwdDialog.vue b/admin-ui/src/components/changePwdDialog.vue index b3a19d6..3d11556 100644 --- a/admin-ui/src/components/changePwdDialog.vue +++ b/admin-ui/src/components/changePwdDialog.vue @@ -116,8 +116,8 @@ ElMessageBox.alert(T('OperationSuccess'), T('ChangePassword'), { autofocus: true, confirmButtonText: 'OK', - callback: (action) => { - userStore.logout() + callback: async (action) => { + await userStore.logout() router.push('/login') }, }) diff --git a/admin-ui/src/layout/components/setting/index.vue b/admin-ui/src/layout/components/setting/index.vue index 30ce037..aec28bb 100644 --- a/admin-ui/src/layout/components/setting/index.vue +++ b/admin-ui/src/layout/components/setting/index.vue @@ -47,14 +47,29 @@ import { ref } from 'vue' import { T } from '@/utils/i18n' import ThemeSwitch from '@/components/ui/ThemeSwitch.vue' + import { useRouter } from 'vue-router' + import { ElMessage } from 'element-plus' const userStore = useUserStore() const user = userStore const appStore = useAppStore() + const router = useRouter() + let logoutPromise = null const logout = () => { - userStore.logout() - window.location.reload() + if (logoutPromise) { + return logoutPromise + } + + logoutPromise = (async () => { + const logoutSucceeded = await userStore.logout() + await router.replace('/login') + if (!logoutSucceeded) { + ElMessage.error(T('LogoutFailed')) + } + })() + + return logoutPromise } const changePwdVisible = ref(false) diff --git a/admin-ui/src/permission.js b/admin-ui/src/permission.js index 6730570..31e6aa6 100644 --- a/admin-ui/src/permission.js +++ b/admin-ui/src/permission.js @@ -36,7 +36,7 @@ router.beforeEach(async (to, from, next) => { if (!userStore.route_names.length) { const info = await userStore.info() if (!info) { - userStore.logout() + await userStore.logout() next(`/login?redirect=${to.path}`) } else { next({ ...to, replace: true }) diff --git a/admin-ui/src/store/user.js b/admin-ui/src/store/user.js index a647e77..833bc29 100644 --- a/admin-ui/src/store/user.js +++ b/admin-ui/src/store/user.js @@ -1,10 +1,12 @@ import { defineStore, acceptHMRUpdate } from 'pinia' -import { current, login } from '@/api/user' -import { setToken, removeToken, setCode, removeCode } from '@/utils/auth' +import { current, login, logout as logoutRequest } from '@/api/user' +import { getToken, setToken, removeToken, setCode, removeCode } from '@/utils/auth' import { useRouteStore } from '@/store/router' import { useAppStore } from '@/store/app' import { oidcAuth, oidcQuery } from '@/api/login' +let pendingLogout = null + export const useUserStore = defineStore({ id: 'user', state: () => ({ @@ -18,10 +20,32 @@ export const useUserStore = defineStore({ }), actions: { - logout () { - removeToken() - removeCode() - this.$reset() + async logout () { + if (pendingLogout) { + return pendingLogout + } + + const token = this.token || getToken() + pendingLogout = (async () => { + let revoked = true + try { + if (token) { + await logoutRequest() + } + } catch (_) { + // Local cleanup is the fallback when the server cannot revoke the session. + revoked = false + } finally { + removeToken() + removeCode() + localStorage.removeItem('user_info') + this.$reset() + pendingLogout = null + } + return revoked + })() + + return pendingLogout }, saveUserData (userData) { diff --git a/admin-ui/src/utils/auth.js b/admin-ui/src/utils/auth.js index 0a6d674..40fdb35 100644 --- a/admin-ui/src/utils/auth.js +++ b/admin-ui/src/utils/auth.js @@ -12,7 +12,8 @@ export function setToken (token) { } export function removeToken () { - return localStorage.removeItem(TokenKey) + localStorage.removeItem(TokenKey) + return localStorage.removeItem('wc-option:local:access_token') } // 设置 code,并存储当前时间戳(单位:毫秒) diff --git a/admin-ui/src/utils/i18n/en.json b/admin-ui/src/utils/i18n/en.json index 882ba62..0aa06eb 100644 --- a/admin-ui/src/utils/i18n/en.json +++ b/admin-ui/src/utils/i18n/en.json @@ -5,6 +5,9 @@ "Logout": { "One": "Logout" }, + "LogoutFailed": { + "One": "Session revocation failed; you were signed out locally." + }, "Register": { "One": "Register" }, @@ -17,6 +20,21 @@ "Password": { "One": "Password" }, + "ShowPassword": { + "One": "Show password" + }, + "HidePassword": { + "One": "Hide password" + }, + "GeneratedKeyLabel": { + "One": "Generated encryption key" + }, + "GeneratedKeyWarning": { + "One": "Copy this key now; it will not be shown again." + }, + "GeneratedKeyCreated": { + "One": "A new encryption key was generated." + }, "LoginSuccess": { "One": "Login Success" }, @@ -35,6 +53,33 @@ "ParamRequired": { "One": "{param} is required" }, + "CustomClientPlatformRequired": { + "One": "Choose Windows as the supported build platform." + }, + "CustomClientVersionRequired": { + "One": "Choose the client version to build." + }, + "CustomClientAppNameRequired": { + "One": "Enter the artifact application name." + }, + "CustomClientHostRequired": { + "One": "Provide the ID server endpoint (hostname/IP and optional port)." + }, + "CustomClientKeyRequired": { + "One": "Provide the public key." + }, + "CustomClientApiServerRequired": { + "One": "Provide the API server URL." + }, + "CustomClientRelayServerRequired": { + "One": "Provide the relay endpoint (hostname/IP and optional port)." + }, + "CustomClientPermanentPasswordRequired": { + "One": "Provide a permanent password when connection management is hidden." + }, + "ClearSavedPassword": { + "One": "Clear saved password" + }, "HasBind": { "One": "Has bind" }, @@ -56,6 +101,108 @@ "Platform": { "One": "Platform" }, + "PlatformWindows": { + "One": "Windows 64-bit" + }, + "PlatformLinuxUnavailable": { + "One": "Linux x64 — experimental / unavailable (PR11)" + }, + "PlatformAndroidUnavailable": { + "One": "Android arm64 — experimental / unavailable (PR11)" + }, + "ProductionPlatformUnavailable": { + "One": "This platform is experimental and unavailable for production builds until PR11 evidence is complete." + }, + "WorkflowApprovalTitle": { + "One": "Workflow approval" + }, + "WorkflowApprovalDescription": { + "One": "Build workflows require a provider-derived, verified annotated tag and current live-provider evidence before approval." + }, + "WorkflowApprovalProtectedExplanation": { + "One": "The provider must confirm an active protected-tag policy for this label: updates and deletions are explicitly blocked, no bypass actors are present, and no branch has the same label. The provider must also verify the annotated tag and exact workflow at its resolved commit. Only the tag label is selectable; refs, SHAs, credentials, and workflow internals are never entered here. Local checks alone do not prove approval." + }, + "WorkflowApprovalState": { + "One": "Approval state" + }, + "WorkflowApprovalStatusApprovalRequired": { + "One": "Approval required" + }, + "WorkflowApprovalStatusProviderPolicyUnverified": { + "One": "Provider policy unverified" + }, + "WorkflowApprovalStatusApproved": { + "One": "Approved" + }, + "WorkflowApprovalTagLabel": { + "One": "Verified workflow tag" + }, + "WorkflowApprovalTagPlaceholder": { + "One": "Choose a provider-verified tag" + }, + "WorkflowApprovalCurrentTag": { + "One": "Current approved tag: {param}" + }, + "WorkflowApprovalLoading": { + "One": "Loading provider-verified tags..." + }, + "WorkflowApprovalLoadError": { + "One": "Provider-verified workflow tags could not be loaded." + }, + "WorkflowApprovalConfigError": { + "One": "Workflow approval state could not be loaded." + }, + "WorkflowApprovalEmpty": { + "One": "No provider-verified workflow tags are available." + }, + "WorkflowApprovalRetry": { + "One": "Retry" + }, + "WorkflowApprovalApprove": { + "One": "Approve selected tag" + }, + "WorkflowApprovalApproving": { + "One": "Approving..." + }, + "WorkflowApprovalRequestFailed": { + "One": "Approval was not recorded. Try again after reviewing the provider status." + }, + "GithubPatPermissionsIntro": { + "One": "Required fine-grained PAT repository permissions:" + }, + "GithubPatPermissionMetadata": { + "One": "Metadata — Read" + }, + "GithubPatPermissionContents": { + "One": "Contents — Read" + }, + "GithubPatPermissionActions": { + "One": "Actions — Read and write" + }, + "GithubPatPermissionAdministration": { + "One": "Administration — Read and write" + }, + "GithubPatPermissionSecrets": { + "One": "Secrets — Read and write" + }, + "GithubPatPermissionsNote": { + "One": "Actions write is required for dispatch; Administration write is required for ruleset bypass metadata; Secrets write is required for secret synchronization. Empty value keeps the existing token." + }, + "GithubBuildSaveError": { + "One": "GitHub build settings could not be saved. Check the configuration and try again." + }, + "GithubBuildSaveSaving": { + "One": "Saving GitHub build settings..." + }, + "GithubBuildSaveSuccess": { + "One": "GitHub build settings saved." + }, + "GithubBuildRepositoryRequired": { + "One": "Enter a repository in owner/name format before continuing." + }, + "ViewBuildLog": { + "One": "View build log" + }, "Status": { "One": "Status" }, @@ -736,8 +883,11 @@ "Host": { "One": "Host" }, - "HostnameOnlyHint": { - "One": "Hostname only, no port. Client appends the default port automatically." + "HostEndpointHint": { + "One": "Provide the ID server endpoint: a hostname or IP address with an optional port. RustDesk uses port 21116 when omitted." + }, + "HostEndpointPlaceholder": { + "One": "e.g. your-server.com or your-server.com:21116 (default when omitted)" }, "ApiServer": { "One": "API Server" @@ -745,6 +895,12 @@ "RelayServer": { "One": "Relay Server" }, + "RelayEndpointHint": { + "One": "Provide the relay endpoint: a hostname or IP address with an optional port. RustDesk uses port 21117 when omitted." + }, + "RelayEndpointPlaceholder": { + "One": "e.g. your-server.com or your-server.com:21117 (default when omitted)" + }, "Key": { "One": "Key" }, @@ -865,12 +1021,21 @@ "BuildStatus": { "One": "Status" }, + "BuildHistoryStatusChanged": { + "One": "Build status updated: {param}." + }, "Pending": { "One": "Pending" }, "Building": { "One": "Building" }, + "Downloading": { + "One": "Downloading" + }, + "Extracting": { + "One": "Extracting" + }, "Done": { "One": "Done" }, diff --git a/admin-ui/src/utils/i18n/ru.json b/admin-ui/src/utils/i18n/ru.json index fe677ee..367a5e4 100644 --- a/admin-ui/src/utils/i18n/ru.json +++ b/admin-ui/src/utils/i18n/ru.json @@ -5,6 +5,9 @@ "Logout": { "One": "Выход" }, + "LogoutFailed": { + "One": "Не удалось отозвать сессию; локальный выход выполнен." + }, "Register": { "One": "Регистрация" }, @@ -17,6 +20,21 @@ "Password": { "One": "Пароль" }, + "ShowPassword": { + "One": "Показать пароль" + }, + "HidePassword": { + "One": "Скрыть пароль" + }, + "GeneratedKeyLabel": { + "One": "Созданный ключ шифрования" + }, + "GeneratedKeyWarning": { + "One": "Скопируйте этот ключ сейчас — повторно он показан не будет." + }, + "GeneratedKeyCreated": { + "One": "Создан новый ключ шифрования." + }, "LoginSuccess": { "One": "Успешный вход" }, @@ -35,6 +53,33 @@ "ParamRequired": { "One": "Поле {param} обязательно" }, + "CustomClientPlatformRequired": { + "One": "Выберите поддерживаемую платформу сборки (Windows)." + }, + "CustomClientVersionRequired": { + "One": "Выберите версию клиента для сборки." + }, + "CustomClientAppNameRequired": { + "One": "Введите имя приложения для артефакта." + }, + "CustomClientHostRequired": { + "One": "Укажите адрес ID-сервера (имя хоста/IP и необязательный порт)." + }, + "CustomClientKeyRequired": { + "One": "Укажите публичный ключ." + }, + "CustomClientApiServerRequired": { + "One": "Укажите URL API-сервера." + }, + "CustomClientRelayServerRequired": { + "One": "Укажите адрес relay-сервера (имя хоста/IP и необязательный порт)." + }, + "CustomClientPermanentPasswordRequired": { + "One": "Если управление подключением скрыто, укажите постоянный пароль." + }, + "ClearSavedPassword": { + "One": "Очистить сохранённый пароль" + }, "HasBind": { "One": "Связано" }, @@ -56,6 +101,111 @@ "Platform": { "One": "Платформа" }, + "PlatformWindows": { + "One": "Windows 64-bit" + }, + "PlatformLinuxUnavailable": { + "One": "Linux x64 — экспериментальная / недоступна (PR11)" + }, + "PlatformAndroidUnavailable": { + "One": "Android arm64 — экспериментальная / недоступна (PR11)" + }, + "ProductionPlatformUnavailable": { + "One": "Эта платформа экспериментальна и недоступна для production-сборок до подтверждения в PR11." + }, + "WorkflowApprovalTitle": { + "One": "Подтверждение workflow" + }, + "WorkflowApprovalDescription": { + "One": "Для одобрения workflow требуется полученный от провайдера проверенный аннотированный тег и актуальное подтверждение от провайдера." + }, + "WorkflowApprovalProtectedExplanation": { + "One": "Провайдер должен подтвердить действующую политику защиты тега для этой метки: обновление и удаление явно запрещены, обходных участников нет, а ветка с такой же меткой отсутствует. Он также должен проверить аннотированный тег и точный workflow на разрешённом коммите. Выбирается только метка тега; refs, SHA, учётные данные и внутренние данные workflow здесь не вводятся. Одних локальных проверок недостаточно для одобрения." + }, + "WorkflowApprovalState": { + "One": "Состояние подтверждения" + }, + "WorkflowApprovalStatusApprovalRequired": { + "One": "Требуется подтверждение" + }, + "WorkflowApprovalStatusProviderPolicyUnverified": { + "One": "Политика провайдера не подтверждена" + }, + "WorkflowApprovalStatusApproved": { + "One": "Подтверждено" + }, + "WorkflowApprovalTagLabel": { + "One": "Подтверждённый workflow-тег" + }, + "WorkflowApprovalTagPlaceholder": { + "One": "Выберите подтверждённый провайдером тег" + }, + "WorkflowApprovalCurrentTag": { + "One": "Текущий подтверждённый тег: {param}" + }, + "WorkflowApprovalLoading": { + "One": "Загрузка подтверждённых провайдером тегов..." + }, + "WorkflowApprovalLoadError": { + "One": "Не удалось загрузить подтверждённые провайдером workflow-теги." + }, + "WorkflowApprovalConfigError": { + "One": "Не удалось загрузить состояние подтверждения workflow." + }, + "WorkflowApprovalEmpty": { + "One": "Нет доступных подтверждённых провайдером workflow-тегов." + }, + "WorkflowApprovalRetry": { + "One": "Повторить" + }, + "WorkflowApprovalApprove": { + "One": "Подтвердить выбранный тег" + }, + "WorkflowApprovalApproving": { + "One": "Подтверждение..." + }, + "WorkflowApprovalRequestFailed": { + "One": "Подтверждение не записано. Проверьте состояние провайдера и повторите попытку." + }, + "GithubPatPermissionsIntro": { + "One": "Необходимые разрешения репозитория для fine-grained PAT:" + }, + "GithubPatPermissionMetadata": { + "One": "Metadata — Read" + }, + "GithubPatPermissionContents": { + "One": "Contents — Read" + }, + "GithubPatPermissionActions": { + "One": "Actions — Read and write" + }, + "GithubPatPermissionAdministration": { + "One": "Administration — Read and write" + }, + "GithubPatPermissionSecrets": { + "One": "Secrets — Read and write" + }, + "GithubPatPermissionsNote": { + "One": "Для dispatch требуется Actions write; для метаданных обхода защиты workflow требуется Administration write; для синхронизации секретов требуется Secrets write. Пустое значение сохраняет текущий токен." + }, + "GithubBuildSaveError": { + "One": "Не удалось сохранить настройки GitHub Build. Проверьте конфигурацию и повторите попытку." + }, + "GithubBuildSaveSaving": { + "One": "Сохранение настроек GitHub Build..." + }, + "GithubBuildSaveSuccess": { + "One": "Настройки GitHub Build сохранены." + }, + "GithubBuildRepositoryRequired": { + "One": "Перед продолжением укажите репозиторий в формате owner/name." + }, + "ViewBuildLog": { + "One": "Просмотреть журнал сборки" + }, + "BuildHistoryStatusChanged": { + "One": "Статус сборки обновлён: {param}." + }, "Status": { "One": "Статус" }, @@ -571,6 +721,12 @@ "StartBuild": { "One": "Запустить сборку" }, + "Downloading": { + "One": "Загрузка артефакта" + }, + "Extracting": { + "One": "Распаковка артефакта" + }, "VersionListLoading": { "One": "Список версий загружается, подождите..." }, @@ -606,5 +762,17 @@ }, "Upload": { "One": "Загрузить" + }, + "HostEndpointHint": { + "One": "Укажите адрес ID-сервера: имя хоста или IP-адрес с необязательным портом. RustDesk использует порт 21116, если порт не указан." + }, + "HostEndpointPlaceholder": { + "One": "например, your-server.com или your-server.com:21116 (по умолчанию, если порт не указан)" + }, + "RelayEndpointHint": { + "One": "Укажите адрес relay-сервера: имя хоста или IP-адрес с необязательным портом. RustDesk использует порт 21117, если порт не указан." + }, + "RelayEndpointPlaceholder": { + "One": "например, your-server.com или your-server.com:21117 (по умолчанию, если порт не указан)" } } diff --git a/admin-ui/src/utils/i18n/zh_CN.json b/admin-ui/src/utils/i18n/zh_CN.json index 2f9e36e..231e685 100644 --- a/admin-ui/src/utils/i18n/zh_CN.json +++ b/admin-ui/src/utils/i18n/zh_CN.json @@ -5,6 +5,9 @@ "Logout": { "One": "退出登录" }, + "LogoutFailed": { + "One": "会话撤销失败;已在本地退出登录。" + }, "Register": { "One": "注册" }, @@ -17,6 +20,21 @@ "Password": { "One": "密码" }, + "ShowPassword": { + "One": "显示密码" + }, + "HidePassword": { + "One": "隐藏密码" + }, + "GeneratedKeyLabel": { + "One": "生成的加密密钥" + }, + "GeneratedKeyWarning": { + "One": "请立即复制此密钥;之后将不再显示。" + }, + "GeneratedKeyCreated": { + "One": "已生成新的加密密钥。" + }, "LoginSuccess": { "One": "登录成功" }, @@ -35,6 +53,33 @@ "ParamRequired": { "One": "{param} 是必须的" }, + "CustomClientPlatformRequired": { + "One": "请选择受支持的构建平台(Windows)。" + }, + "CustomClientVersionRequired": { + "One": "请选择要构建的客户端版本。" + }, + "CustomClientAppNameRequired": { + "One": "请输入产物应用名称。" + }, + "CustomClientHostRequired": { + "One": "请输入 ID 服务器端点(主机名/IP,可选端口)。" + }, + "CustomClientKeyRequired": { + "One": "请输入公钥。" + }, + "CustomClientApiServerRequired": { + "One": "请输入 API 服务器 URL。" + }, + "CustomClientRelayServerRequired": { + "One": "请输入中继端点(主机名/IP,可选端口)。" + }, + "CustomClientPermanentPasswordRequired": { + "One": "隐藏连接管理时,请输入永久密码。" + }, + "ClearSavedPassword": { + "One": "清除已保存的密码" + }, "HasBind": { "One": "已绑定" }, @@ -56,6 +101,111 @@ "Platform": { "One": "平台" }, + "PlatformWindows": { + "One": "Windows 64 位" + }, + "PlatformLinuxUnavailable": { + "One": "Linux x64 — 实验性 / 暂不可用(PR11)" + }, + "PlatformAndroidUnavailable": { + "One": "Android arm64 — 实验性 / 暂不可用(PR11)" + }, + "ProductionPlatformUnavailable": { + "One": "此平台为实验性平台,在 PR11 完成验证前不可用于生产构建。" + }, + "WorkflowApprovalTitle": { + "One": "Workflow 审批" + }, + "WorkflowApprovalDescription": { + "One": "审批需要服务商提供的、已验证的注释标签,以及当前实时服务商证据。" + }, + "WorkflowApprovalProtectedExplanation": { + "One": "服务商必须确认该标签受到有效的受保护标签策略约束:明确禁止更新和删除、没有绕过参与者,并且不存在同名分支。服务商还必须验证注释标签及其解析到的确切 workflow 提交。这里只能选择标签名称;不能输入 ref、SHA、凭据或 workflow 内部信息。仅有本地检查不足以证明审批。" + }, + "WorkflowApprovalState": { + "One": "审批状态" + }, + "WorkflowApprovalStatusApprovalRequired": { + "One": "需要审批" + }, + "WorkflowApprovalStatusProviderPolicyUnverified": { + "One": "服务商策略未验证" + }, + "WorkflowApprovalStatusApproved": { + "One": "已审批" + }, + "WorkflowApprovalTagLabel": { + "One": "已验证的 workflow 标签" + }, + "WorkflowApprovalTagPlaceholder": { + "One": "选择服务商验证的标签" + }, + "WorkflowApprovalCurrentTag": { + "One": "当前已审批标签:{param}" + }, + "WorkflowApprovalLoading": { + "One": "正在加载服务商验证的标签..." + }, + "WorkflowApprovalLoadError": { + "One": "无法加载服务商验证的 workflow 标签。" + }, + "WorkflowApprovalConfigError": { + "One": "无法加载 workflow 审批状态。" + }, + "WorkflowApprovalEmpty": { + "One": "没有可用的服务商验证 workflow 标签。" + }, + "WorkflowApprovalRetry": { + "One": "重试" + }, + "WorkflowApprovalApprove": { + "One": "审批所选标签" + }, + "WorkflowApprovalApproving": { + "One": "审批中..." + }, + "WorkflowApprovalRequestFailed": { + "One": "审批未记录。请查看服务商状态后重试。" + }, + "GithubPatPermissionsIntro": { + "One": "细粒度 PAT 所需的仓库权限:" + }, + "GithubPatPermissionMetadata": { + "One": "Metadata — Read" + }, + "GithubPatPermissionContents": { + "One": "Contents — Read" + }, + "GithubPatPermissionActions": { + "One": "Actions — Read and write" + }, + "GithubPatPermissionAdministration": { + "One": "Administration — Read and write" + }, + "GithubPatPermissionSecrets": { + "One": "Secrets — Read and write" + }, + "GithubPatPermissionsNote": { + "One": "触发 workflow 需要 Actions write;读取规则绕过元数据需要 Administration write;同步仓库密钥需要 Secrets write。留空将保留现有令牌。" + }, + "GithubBuildSaveError": { + "One": "无法保存 GitHub 构建设置。请检查配置后重试。" + }, + "GithubBuildSaveSaving": { + "One": "正在保存 GitHub 构建设置..." + }, + "GithubBuildSaveSuccess": { + "One": "GitHub 构建设置已保存。" + }, + "GithubBuildRepositoryRequired": { + "One": "继续之前,请输入 owner/name 格式的仓库。" + }, + "ViewBuildLog": { + "One": "查看构建日志" + }, + "BuildHistoryStatusChanged": { + "One": "构建状态已更新:{param}。" + }, "Status": { "One": "状态" }, @@ -584,6 +734,12 @@ "StartBuild": { "One": "开始构建" }, + "Downloading": { + "One": "正在下载构建产物" + }, + "Extracting": { + "One": "正在解压构建产物" + }, "VersionListLoading": { "One": "版本列表加载中,请稍候..." }, @@ -619,5 +775,17 @@ }, "Upload": { "One": "上传" + }, + "HostEndpointHint": { + "One": "请输入 ID 服务器端点:主机名或 IP 地址,可选择是否指定端口。未指定端口时使用 21116。" + }, + "HostEndpointPlaceholder": { + "One": "例如 your-server.com 或 your-server.com:21116(未填写端口时使用默认端口)" + }, + "RelayEndpointHint": { + "One": "请输入中继端点:主机名或 IP 地址,可选择是否指定端口。未指定端口时使用 21117。" + }, + "RelayEndpointPlaceholder": { + "One": "例如 your-server.com 或 your-server.com:21117(未填写端口时使用默认端口)" } } diff --git a/admin-ui/src/utils/request.js b/admin-ui/src/utils/request.js index fc173a1..2f8efc6 100644 --- a/admin-ui/src/utils/request.js +++ b/admin-ui/src/utils/request.js @@ -12,6 +12,51 @@ const service = axios.create({ timeout: 50000, // request timeout }) +const getEnvelopeErrorMessage = data => { + const messages = [data?.message, data?.data?.message] + + return messages.find(message => typeof message === 'string' && message.trim()) || '' +} + +const redirectToLogin = config => { + if (!config?.skipAuthRedirect) { + removeToken() + window.location.reload() + } +} + +const redirectOnEnvelopeAuthFailure = (config, code) => { + if (code === 403) { + redirectToLogin(config) + } +} + +const redirectOnHttpAuthFailure = (config, status) => { + if (config?.useServerErrorMessage && (status === 401 || status === 403)) { + redirectToLogin(config) + } +} + +const GENERIC_RESPONSE_ERROR = 'Unable to process server response' + +const markInterceptorHandled = error => { + Object.defineProperty(error, 'interceptorHandled', { value: true }) + return error +} + +const getResponseErrorMessage = async error => { + if (!error.config?.useServerErrorMessage) return '' + let data = error.response?.data + if (error.config?.responseType === 'blob' && typeof Blob !== 'undefined' && data instanceof Blob) { + try { + data = JSON.parse(await data.text()) + } catch (_) { + return '' + } + } + return getEnvelopeErrorMessage(data) +} + // request interceptor service.interceptors.request.use( config => { @@ -60,6 +105,50 @@ service.interceptors.response.use( * You can also judge the status by HTTP Status Code */ response => { + // Binary downloads do not use the normal JSON response envelope. + if (response.config.responseType === 'blob') { + const contentType = response.headers?.['content-type'] || '' + if (!contentType.includes('application/json')) { + return response + } + + // Auth/API failures can still arrive as a JSON envelope with HTTP 200. + // Parse those responses so a failed download is not saved as a .zip file. + return response.data.text().then(text => { + let res + try { + res = JSON.parse(text) + } catch (_) { + const malformedResponseError = new Error(GENERIC_RESPONSE_ERROR) + if (response.config.useServerErrorMessage && !response.config.skipErrorMessage) { + ElMessage({ + message: GENERIC_RESPONSE_ERROR, + type: 'error', + duration: 5 * 1000, + }) + return Promise.reject(markInterceptorHandled(malformedResponseError)) + } + return Promise.reject(malformedResponseError) + } + + if (res.code !== 0) { + if (!response.config.skipErrorMessage) { + ElMessage({ + message: response.config.useServerErrorMessage + ? getEnvelopeErrorMessage(res) || GENERIC_RESPONSE_ERROR + : res.message || 'error', + type: 'error', + duration: 5 * 1000, + }) + } + + redirectOnEnvelopeAuthFailure(response.config, res.code) + return Promise.reject(res) + } + return response + }) + } + const res = response.data // for the endpoint /login-options @@ -70,31 +159,36 @@ service.interceptors.response.use( // if the custom code is not 20000, it is judged as an error. if (res.code !== 0) { - ElMessage({ - message: res.message || 'error', - type: 'error', - duration: 5 * 1000, - }) - - if (res.code === 403) { - removeToken() - window.location.reload() + if (!response.config.skipErrorMessage) { + ElMessage({ + message: res.message || 'error', + type: 'error', + duration: 5 * 1000, + }) } + + redirectOnEnvelopeAuthFailure(response.config, res.code) return Promise.reject(res) } else { return res } }, - error => { + async error => { if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') > -1) { error.message = 'Connection Time Out!' } - ElMessage({ - message: error.message, - type: 'error', - duration: 5 * 1000, - }) + if (!error.config?.skipErrorMessage) { + const message = await getResponseErrorMessage(error) + ElMessage({ + message: error.config?.useServerErrorMessage + ? message || GENERIC_RESPONSE_ERROR + : error.message, + type: 'error', + duration: 5 * 1000, + }) + } + redirectOnHttpAuthFailure(error.config, error.response?.status) return Promise.reject(error) }, ) diff --git a/admin-ui/src/views/custom-client/index.vue b/admin-ui/src/views/custom-client/index.vue index cf3dbff..7cc3050 100644 --- a/admin-ui/src/views/custom-client/index.vue +++ b/admin-ui/src/views/custom-client/index.vue @@ -7,47 +7,99 @@ pulse="warning" /> - + - - + +
+ {{ p.name }} {{ p.platform }} - {{ T('Delete') }} - - {{ T('SaveAsPreset') }} + +
+
+ {{ T('SaveAsPreset') }} +
+
+ {{ T('Delete') }} +
+
+
{{ T('Platform') }} - + - - - - - + + + + + + + + + - - - - + + + + + + + - - + + + + + @@ -55,37 +107,97 @@ {{ T('CustomServer') }} - - - - + + + + + + + - - + + + + + - - + + + + + - - - - + + + + + + + @@ -112,8 +224,44 @@ - - + + + + + + + + {{ T('ClearSavedPassword') }} + + @@ -149,7 +297,7 @@ {{ T('HideConnectionManagement') }} - + @@ -281,11 +429,13 @@ - {{ T('StartBuild') }} + {{ T('StartBuild') }} {{ T('Reset') }} - {{ T('VersionListLoading') }} - {{ T('VersionListEmpty') }} - {{ T('VersionListError') }} + + {{ T('VersionListLoading') }} + {{ T('VersionListEmpty') }} + {{ versionsError || T('VersionListError') }} +
@@ -306,19 +456,29 @@ ]" >