Merge ios-completion: device builds unblocked, access token on both clients, P2 wave
Closes the six remediation items from the 2026-07-29 iOS completion audit plus the whole P2 wave and Android access-token parity. The audit's headline was that the client was code-complete but stuck at the device door: no DEVELOPMENT_TEAM, no entitlements, so it had never run on real hardware once, and it had fallen two months behind the server (Android had the git panel, iOS had none) while neither native client could connect at all once WEBTERM_TOKEN was set. Package tests 310 -> 452, app bundle 296 -> 550 (iPhone and iPad, zero known issues), integration 10 -> 32, Android 687 -> 691. ClientTLS went 55.76% -> 89.49% and is now actually in the coverage gate, which it never was. Device build now succeeds on the free personal team. src/ and public/ are untouched — the git-panel endpoints already existed server-side; iOS simply never consumed them. # Conflicts: # android/.gitignore # android/README.md # android/api-client/src/main/kotlin/wang/yaojia/webterm/api/routes/Endpoints.kt # android/app/src/main/java/wang/yaojia/webterm/screens/PairingScreen.kt # android/app/src/main/java/wang/yaojia/webterm/viewmodels/PairingViewModel.kt # android/app/src/main/java/wang/yaojia/webterm/wiring/AppEnvironment.kt # android/transport-okhttp/src/main/kotlin/wang/yaojia/webterm/transport/OkHttpClientFactory.kt # docs/PROGRESS_LOG.md
This commit is contained in:
@@ -46,8 +46,16 @@ Setup: `local.properties` → `sdk.dir=/usr/local/share/android-commandlinetools
|
||||
| HostRegistry | `:host-registry` | Android (DataStore) | ✅ built |
|
||||
| SwiftTerm host view | `:terminal-view` | Android (Termux wrap) | ✅ built |
|
||||
| App/WebTerm | `:app` | Android app (Compose/Hilt/FCM)| ✅ built |
|
||||
| `URLSession*Transport` | `:transport-okhttp` | pure Kotlin/JVM (OkHttp) | ✅ built |
|
||||
| — (no iOS counterpart) | `:macrobenchmark` | `com.android.test` harness | ⬜ scaffolded, no sources |
|
||||
|
||||
> `:transport-okhttp` (A7) holds the OkHttp `TermTransport`/`HttpTransport`
|
||||
> implementations that the two iOS `URLSession*Transport`s consolidate into
|
||||
> (plan §3 framing note). OkHttp is a plain JVM library, so the module builds and
|
||||
> unit-tests (MockWebServer) with **no** Android SDK — including the
|
||||
> access-token cookie on the WS upgrade (`OkHttpAccessTokenTest`) and the
|
||||
> public-host cleartext refusal (`CleartextGuardTest`).
|
||||
|
||||
`:macrobenchmark` (A35) is the one module with no iOS counterpart. It is a
|
||||
`com.android.test` module — a **separate APK** that drives `:app` out of process via
|
||||
UiAutomator, which is the only way startup/frame timings are real. It therefore cannot
|
||||
@@ -76,8 +84,45 @@ written yet.
|
||||
`:wire-protocol` is the **frozen shared contract** (Android analogue of
|
||||
`src/types.ts` + WireProtocol) — `ClientMessage`/`ServerMessage`, `MessageCodec`,
|
||||
`Validation`, `WireConstants`, `HostEndpoint` (the single Origin/wsURL derivation),
|
||||
and the `TermTransport` / `HttpTransport` / `PingableTermTransport` boundary
|
||||
interfaces. New wire types are added **only** here (a coordination point).
|
||||
`AuthCookie`/`AccessTokenRule`/`AccessTokenSource` (the single access-token-cookie
|
||||
derivation), and the `TermTransport` / `HttpTransport` / `PingableTermTransport`
|
||||
boundary interfaces. New wire types are added **only** here (a coordination point).
|
||||
|
||||
## Access token (`WEBTERM_TOKEN`) — how the Android client authenticates
|
||||
|
||||
A server started with `WEBTERM_TOKEN=<16–512 chars of [A-Za-z0-9._~+/=-]>` gates **every**
|
||||
HTTP route *and* the WebSocket upgrade behind a `webterm_auth` cookie. The Android
|
||||
client therefore:
|
||||
|
||||
- **hand-writes `Cookie: webterm_auth=<t>` itself** for a token it already holds — one
|
||||
derivation point (`AuthCookie`), stamped in exactly three places: `:api-client`'s
|
||||
`ApiRoute.toHttpRequest` (all 12+ routes, RO GETs included), the pairing probe's two
|
||||
hand-built legs, and `:transport-okhttp`'s WS upgrade. The hand-built
|
||||
`GET /projects/diff` fetcher in `:app` stamps it too.
|
||||
- **also installs one `AuthCookieJar`** on the single shared `OkHttpClient`, which captures
|
||||
the `Set-Cookie` a live pairing (`POST /auth`) hands back and replays it on REST *and* on
|
||||
the WS upgrade (OkHttp's `BridgeInterceptor` runs for WebSocket calls). The two are
|
||||
orthogonal: the hand-written header covers a token restored from the Keystore store, the
|
||||
jar covers a cookie the server just issued. A host with neither sends no cookie at all.
|
||||
- keeps the header **orthogonal to `Origin`**: the token never replaces the CSWSH
|
||||
Origin check (the server tests Origin first, then the cookie), and read-only routes
|
||||
still carry no Origin.
|
||||
- validates a typed token ONCE at pairing time with `POST /auth`
|
||||
(`{"token":"…"}`, `Accept: application/json` — an `Accept` containing `text/html`
|
||||
makes the server answer 302 instead of 204/401). Four outcomes: 204+`Set-Cookie` =
|
||||
correct → store it; 204 **without** `Set-Cookie` = that host has no auth → store
|
||||
nothing; 401 = wrong token; 429 = rate-limited (10/min/IP).
|
||||
- stores it per host (keyed by the canonical origin) in `TinkAccessTokenStore`:
|
||||
Tink AEAD under an **AndroidKeystore-wrapped, non-exportable** master key, in an
|
||||
app-private `SharedPreferences` file, with `android:allowBackup="false"` +
|
||||
`data_extraction_rules.xml` excluding cloud backup and device transfer. The token is
|
||||
**never logged, never in a URL, never in app UI state**.
|
||||
- treats a **401 on the WS upgrade as terminal** (`FailureReason.UNAUTHORIZED`): no
|
||||
reconnect back-off loop, and the banner tells the user to re-enter the token. A 401 on
|
||||
any REST route is the typed `ApiClientError.Unauthorized`.
|
||||
|
||||
A host with no `WEBTERM_TOKEN` is byte-identical to before the feature: no token stored ⇒
|
||||
no `Cookie` header at all (LAN zero-config preserved).
|
||||
|
||||
## Toolchain
|
||||
|
||||
@@ -111,8 +156,9 @@ JUnit Platform (`tasks.test { useJUnitPlatform() }`).
|
||||
```
|
||||
|
||||
> Testing target: **≥80% Kover coverage** on the pure modules (`:wire-protocol`,
|
||||
> `:session-core`, `:api-client`, `:client-tls` pure half). TDD, immutable data,
|
||||
> small focused files — same discipline as the rest of the repo.
|
||||
> `:session-core`, `:api-client`, `:client-tls` pure half — the four modules that
|
||||
> apply the Kover plugin, and therefore the only ones `koverVerify` gates). TDD,
|
||||
> immutable data, small focused files — same discipline as the rest of the repo.
|
||||
>
|
||||
> **Do not use `--rerun-tasks` in a release gate.** It trips an AGP lint/K2 internal bug
|
||||
> (`FirDeclaration was not found for class KtProperty, fir is null`, on
|
||||
@@ -199,8 +245,7 @@ Before adding a rule, read the merged configuration R8 actually used:
|
||||
`hilt-android-testing` with `kspAndroidTest`) and `testInstrumentationRunner` is set to
|
||||
`wang.yaojia.webterm.HiltTestRunner`.
|
||||
|
||||
⚠️ **That runner class does not exist yet** — it is the first file the A34 author must write,
|
||||
into `app/src/androidTest/java/wang/yaojia/webterm/HiltTestRunner.kt`:
|
||||
`app/src/androidTest/java/wang/yaojia/webterm/HiltTestRunner.kt` supplies it:
|
||||
|
||||
```kotlin
|
||||
class HiltTestRunner : AndroidJUnitRunner() {
|
||||
@@ -215,6 +260,24 @@ replace the app-under-test's `Application` with the generated `HiltTestApplicati
|
||||
*test* APK, not into the app under test. `assembleDebugAndroidTest` builds fine without the
|
||||
class (the runner name is just a manifest value); an actual instrumentation *run* needs it.
|
||||
|
||||
The suite has been **run green on real hardware** (67/0 instrumented tests against this
|
||||
repo's own server), so `src/androidTest/**` is verified, not aspirational — but it still
|
||||
needs a connected device/emulator and is therefore **not** part of the `./gradlew test`
|
||||
gate; run it with `connectedAndroidTest`. An emulator additionally needs
|
||||
`ALLOWED_ORIGINS=http://10.0.2.2:<port>` on the server, because the server derives its
|
||||
allowed origins from NIC IPs and `10.0.2.2` (the emulator's alias for the host) is not one.
|
||||
|
||||
### What is still NOT verified here
|
||||
|
||||
- **DEFERRED — end-to-end access token**: the `WEBTERM_TOKEN` path is covered by
|
||||
unit tests (`OkHttpAccessTokenTest`, `AuthProbeTest`, `AccessTokenCookieTest`, the
|
||||
`:api-client` route tests, the `AuthCookie` derivation tests) and by
|
||||
`TinkAccessTokenStoreTest` on device, but **no automated leg on the Android side starts
|
||||
a real server with `WEBTERM_TOKEN` set and completes a cookie-gated WS upgrade** — that
|
||||
end-to-end coverage exists only on the iOS side (`ios/IntegrationTests`).
|
||||
- **DEFERRED — the rest of `DEVICE_QA_CHECKLIST.md`**: FCM push delivery under Doze on two
|
||||
physical handsets (S2) and the lock-screen Allow/Deny walkthrough have no automated cover.
|
||||
|
||||
## Android SDK setup (proven working)
|
||||
|
||||
The pure JVM modules need only a JDK + Gradle. The **Android-framework** modules
|
||||
|
||||
Reference in New Issue
Block a user