The link both projects wrote twice
dev-updater serves APKs to a phone and ai-app runs model sessions for one. Above the waterline they share nothing. Underneath they are the same program: bound to wg0 so they are not on the LAN, presenting a certificate from a CA the app pins, answering only requests carrying a token enrolled by scanning a QR off the terminal, keeping state in owner-only files outside the repo. Three modules, each extracted only after diffing the two copies and finding nothing between them but a product name and a type parameter. netif fails closed when the tunnel is down. enroll generates, stores and compares the token, and prints the QR, with the URI scheme as the one per-project part. private owns the file modes, taken from ai-app's version because it had already factored out what dev-updater still has inline in two places. Nothing is removed from either project. This is a proposal with a working core, and the README carries the measured evidence -- the enrollment scanner activity differs by its package line and nothing else, the RON format module is byte-identical, and the two copies have each drifted into holding an improvement the other lacks, which is the cost being paid today.
This commit is contained in:
1 parent
995b29f10d
commit
7651d491ac
9 files changed
+871
No files matched your search
@@ -1,2 +1,102 @@
|
||||
# wg-server-app
|
||||
|
||||
The private link between a phone and a machine you run, extracted from the
|
||||
two projects that had each written it.
|
||||
|
||||
`dev-updater` serves locally-built APKs to a phone. `ai-app` runs model
|
||||
sessions for one. They have nothing in common above the waterline — and
|
||||
underneath they are the same program: a server bound to a WireGuard
|
||||
interface so it is not on the LAN, presenting a certificate from a CA the
|
||||
app pins, answering only requests carrying a bearer token that was enrolled
|
||||
by scanning a QR code off the terminal, and keeping its state in
|
||||
owner-only files outside the repo.
|
||||
|
||||
That link was written twice. This is it written once.
|
||||
|
||||
> **Status: proposal with a working core.** Nothing has been removed from
|
||||
> either project yet. The Rust modules here are real, tested and
|
||||
> lint-clean; the rest of this file is the case for what should follow and
|
||||
> what should not.
|
||||
|
||||
## The evidence
|
||||
|
||||
Measured, not estimated — `difflib` over the two working trees on
|
||||
2026-08-28:
|
||||
|
||||
| file | dev-updater | ai-app | identical |
|
||||
|---|---|---|---|
|
||||
| `EnrollmentScanActivity.kt` | 32 lines | 32 lines | **98%** |
|
||||
| `ServerConfig.kt` | 139 | 122 | **89%** |
|
||||
| `auth.rs` | 250 | 230 | **83%** |
|
||||
| `PinnedCert.kt` | 60 | 59 | **75%** |
|
||||
| `certs.rs` | 218 | 199 | **70%** |
|
||||
| RON `format` module | — | — | **byte-identical** |
|
||||
|
||||
The percentages understate it, because the differences are almost entirely
|
||||
a product name and a type parameter:
|
||||
|
||||
- **`EnrollmentScanActivity.kt`** differs by its `package` line. That is all.
|
||||
- **`PinnedCert.kt`** differs by `package` and one string, `"dev-updater-dev-ca"` against `"ai-app-dev-ca"`.
|
||||
- **`certs.rs`** differs by the organisation name in the certificate, and by ai-app having factored the file-mode handling into a `private` module that dev-updater still has inline in two places.
|
||||
- **`auth.rs`** differs by the state type the middleware is generic over (`AppState` against `SessionManager`), and by dev-updater keeping `print_enrollment` in `auth.rs` where ai-app keeps the same function in `main.rs`.
|
||||
- **`wg_address` and `local_addresses`** are the same logic in both, in files that are otherwise 8% alike.
|
||||
|
||||
ai-app's own `Cargo.toml` already says the quiet part: RON is used there
|
||||
because it is *"the same choice, and the same house rules, as the sibling
|
||||
dev-updater project's config."*
|
||||
|
||||
## Why this is worth doing, in one observation
|
||||
|
||||
**The two copies have each drifted into holding an improvement the other
|
||||
lacks.** Not hypothetically — as of the day this was written:
|
||||
|
||||
| improvement | in dev-updater | in ai-app |
|
||||
|---|---|---|
|
||||
| `private::` module owning every file mode | no, inline twice | yes |
|
||||
| `SharedPreferences.edit { }` instead of the deprecated builder | no | yes |
|
||||
| `existingTokenKey()` — reads the key without creating one, so a stored blob with no key means "not enrolled" rather than leaving a stray key behind | yes | no |
|
||||
| `hasLocalNetworkPermission` — tells a denied permission apart from an unreachable server, which are identical at the socket | yes | no |
|
||||
| Catppuccin theme with colour-by-consequence buttons | yes | no, ad-hoc `Color(0xFF…)` per screen |
|
||||
|
||||
Every one of those is a fix somebody made once, in one repo, that the other
|
||||
will either never get or get by being written a third time. That is the
|
||||
cost this repo removes, and it is already being paid.
|
||||
|
||||
## What is here
|
||||
|
||||
`server/` — the `wg-link` crate. Three modules, each extracted only after
|
||||
diffing the two copies and finding nothing but a name between them.
|
||||
|
||||
- **`netif`** — `wg_address()`, which fails closed when the tunnel is down, and `local_addresses()` for the certificate's SANs. The product name is a parameter so the failure reads as advice rather than as a library complaining.
|
||||
- **`enroll`** — token generation, hex-SHA-256 storage, constant-time comparison, the `<scheme>://enroll?…` URI, and the terminal QR. The URI scheme is the parameter, because it is what routes a scan back to the right app.
|
||||
- **`private`** — owner-only files and directories, taken from ai-app's version, with an `append_file` alongside `create_file` because a transcript must never be truncated by being opened.
|
||||
|
||||
## What should follow, and what should not
|
||||
|
||||
**Should follow, in this order.** Each is already near-identical:
|
||||
|
||||
1. `certs` — CA generated once and never replaced, leaf reissued every start. One `product: &str`.
|
||||
2. The RON `format` house rules — byte-identical today, so this is pure deletion.
|
||||
3. The Kotlin `EnrollmentScanActivity`, `PinnedCert`, and the enrollment/Keystore half of `ServerConfig`, as an Android library module. This is where the sharing pays most, because it is where the two copies have drifted furthest apart in *both* directions.
|
||||
4. Atomic owner-only config save. Both do temp-file-then-rename with the mode set before the rename; only the schema differs.
|
||||
|
||||
**Should not.** Naming these is the point of the exercise:
|
||||
|
||||
- **The auth middleware itself.** It is generic over each project's state type. Share the primitives (`enroll`), let each keep the six lines that wire them to its own state — the alternative is a trait that exists only to let one function be shared.
|
||||
- **The HTTP clients.** dev-updater's `DownloadServer.kt` and ai-app's `Api.kt` are 14% alike and 135 against 494 lines. They have diverged because they are genuinely different programs. A shared *pinned transport* underneath them may be worth it later; a shared API is not.
|
||||
- **Config schemas.** Shared house rules, separate contents.
|
||||
- **Anything above the link.** Projects, builds, sessions, providers. If a third project would not want it, it does not belong here.
|
||||
|
||||
## The name
|
||||
|
||||
`wg-server-app` describes the two things it was extracted from. `wg-link`
|
||||
— the crate's name here — describes what it actually is: neither a server
|
||||
nor an app, but the link between them. Renaming the repo to match is a
|
||||
`mv` and a rename on gitea; the crate can also be renamed the other way.
|
||||
Whichever is preferred, it should be one name rather than two.
|
||||
|
||||
## Testing
|
||||
|
||||
`./run-tests.sh`. The crate is warning-clean under `cargo clippy
|
||||
--all-targets` and formatted with plain `cargo fmt` at its defaults, per
|
||||
the house rules.
|
||||
Reference in new issue
Block a user