The live five-rung smoke test of the visibility framework found that Part
A enforced it on the SSE path and on /guilds + /governors, but not on the
remaining public REST reads - so one event was projected live and served
verbatim from stored history.
link/v3.md gains 3.6.1 with the full list (the anonymous acct/webId leak
on /feed, the flattened ownerAcct on /idoc, the dead `houses` field
rules, /feed ignoring live config, the empty-allowlist fall-through, and
the Date-to-{} projection bug), plus the rule it leaves behind: a read
path that returns shard data and does not project is a bug, and every new
Part B/C surface must gate its kind set on live config rather than on
PUBLIC_KINDS.
3.5 also corrected: the table is NOT seeded on boot. An absent row means
"use the compiled default", which keeps the defaults in one place instead
of duplicating them into a seeder that could drift.
BACKEND_DESIGN.md 6.5 records the same as a security contract: rule 1
locks a field by meaning rather than spelling; PUBLIC_KINDS is a
module-load constant and must not answer per-caller questions;
projectFeature walks arrays and plain objects only.
SHARD_VISIBILITY.md gets the admin-facing version - that stored history
answers the same way the live stream does, and that turning live updates
off stops the push, not the reading.
Co-Authored-By: Claude <noreply@anthropic.com>
7.9 KiB
Handoff — Trusted Devices & MFA: remaining work
Written 2026-07-22 at the end of the backend/web implementation session, for a fresh session to continue. Canonical design:
docs/website/TRUSTED_DEVICES_MFA.md. App-side plan:docs/android/PLAN.md §4.1.1. This doc is the "what's left + how".
1. Status at handoff
Done, in review, and live-smoke-tested (real MariaDB + AVD):
- Backend (schema, session service, web + mobile login, self-service + admin
endpoints, invalidation), web client UI, admin front-end UI, OpenAPI spec,
33 server tests — all in website PR #93 (branch
feature/trusted-devices-mfa). - Docs (BACKEND_DESIGN §3/§4/§6, TRUSTED_DEVICES_MFA.md, PLAN §4.1.1) — docs PR #32
(branch
docs/trusted-devices-mfa). - Live smoke test passed end-to-end: trusted-device TOTP-skip (web + mobile
X-Trust-Token), recovery-code login (single-use), cap 409, admin MFA reset, audit logging, and a real 2FA login through the Android app on an emulator.
Not done — the two remaining items below.
2. Remaining item A — merge gate (no code)
- website#93 and docs#32 need CI green + review, then merge to
main. - CI (
.gitea/workflows/pr-checks.yml) runs server tests, client build, bot install. - Nothing to build here; just get them reviewed/merged. The Android work should land after #93 merges so the app builds against the merged contract.
3. Remaining item B — Android app implementation (the real work)
The backend is fully ready and additive; the existing app is unaffected (verified).
The app just needs to consume the new endpoints. Own PR in the android-app
repo, branch feature/trusted-devices-mfa. Package id com.runicgateway.app.
3.1 Scope (from PLAN §4.1.1)
- Login/TOTP screen: on the existing
401 { totpRequired }step, add- a "Trust this device" checkbox, and
- a "use a recovery code instead" toggle (send
recoveryCodeinstead ofcode).
- Trust token storage: when a login response carries
trustToken, store it in EncryptedSharedPreferences (same secure store as the bearer tokens — never plain prefs/logs). On subsequent logins send it as theX-Trust-Tokenheader so the server skips the TOTP prompt. - Cap handling: a login response with
{ trustLimitReached: true, devices }means show the device list and prompt the user to revoke one (DELETE /auth/me/trusted-devices/:id) then retry trusting. - Account screens:
- Trusted Devices: list (
GET /auth/me/trusted-devices), revoke one, untrust all; "trust this device" (POST /auth/me/trusted-devices→ returnstrustTokenfor native — store it). - Recovery Codes: show the one-time batch returned by TOTP enable; remaining
count (
GET …/recovery-codes/status); regenerate (POST …/recovery-codes/generate, password step-up) with a show-once display + copy/share.
- Trusted Devices: list (
- Invalidation: on logout / dead-refresh sign-out / Settings→Server switch,
clear the stored
trustTokenalongside the bearer tokens. (A server-side password change/reset or TOTP disable already revokes it.) - Tests: JVM
:app:testDebugUnitTest— DTO decode for the new fields, and repository logic (trust-token persist/clear, recoveryCode vs code branch).
3.2 Exact API contract the app consumes
POST /auth/mobile/login — body { username, password, code?, recoveryCode?, trustDevice?, device_name? }, optional header X-Trust-Token: <token>
200→{ accessToken, refreshToken, expiresIn, user:{id,username,role}, trustToken?, trustLimitReached?, devices? }trustTokenpresent only whentrustDevice:truewas accepted (store it).trustLimitReached:true+devices[]when at the cap (login still succeeded).
401→{ totpRequired:true, message }(missing/invalid 2nd factor) — reveal the code field (existing behavior); or generic{ message }for bad credentials.- A valid
X-Trust-Tokenbound to the user makes a code unnecessary → straight200.
Self-service (Bearer access token):
GET /auth/me/trusted-devices→[{ id, platform, deviceName, userAgent, createdAt, lastUsedAt, expiresAt }]POST /auth/me/trusted-devicesbody{ deviceName? }→{ trusted:true, trustToken }(native) |409 { error:'trusted_device_limit', devices }DELETE /auth/me/trusted-devices/:id→{ revoked:boolean }DELETE /auth/me/trusted-devices→{ revoked:number }GET /auth/me/account/recovery-codes/status→{ remaining:number }POST /auth/me/account/recovery-codes/generatebody{ currentPassword? }→{ recoveryCodes:[string] }POST /auth/me/account/totp/enablebody{ code }→{ totp_enabled:true, recoveryCodes:[string] }(codes shown once)
The OpenAPI spec (website/server/swagger/swagger-output.json, schemas TrustedDevice,
TrustDeviceResult, TrustedDeviceLimit, RecoveryCodes) is the source of truth.
3.3 Where it likely goes in the app
The app already handles totpRequired (reveals a code field — verified live), so the
auth surface exists. Extend the existing auth Retrofit API + repository + login
ViewModel/screen, add a secure trustToken accessor to the encrypted token store,
and add two account screens. Explore android-app/app/src/main/java/com/runicgateway/app/
(auth/data/core modules) at the start — don't assume file names.
4. Environment & how-to (verified this session)
- DB: Docker container
uomm-db, MariaDB on host port 3307, dbuomysticmoon, useruomm(password:docker exec uomm-db printenv MARIADB_PASSWORD). - Run the server:
cd website/server && node src/server.js(uses.env, already points at 127.0.0.1:3307). It ensures schema + seeds on boot. For cap testing setMAX_TRUSTED_DEVICES=2; recovery count viaRECOVERY_CODE_COUNT.- Pre-existing noise:
uo-link-socket … Unsupported statedecrypt errors are an old encrypteduo_link_configrow, unrelated — ignore.
- Pre-existing noise:
- Server tests:
cd website/server && DB_HOST=127.0.0.1 DB_PORT=59999 node --test(dead port by design; models stubbed). Client:cd website/client && npm test. - TOTP codes for manual testing: from
website/server,node -e "process.stdout.write(require('speakeasy').totp({secret:'<BASE32>',encoding:'base32'}))". - Android build: JDK 21;
cd android-app && ./gradlew :app:assembleDebug -Pksp.incremental=false. Unit tests:./gradlew :app:testDebugUnitTest -Pksp.incremental=false. - Emulator: SDK at
~/AppData/Local/Android/Sdk; AVDsMedium_Phone_API_36.1,s22_ultra.adb install -r -g app/build/outputs/apk/debug/app-debug.apk. - ⚠ Cleartext HTTP gotcha (dev only): the app blocks plain HTTP to
10.0.2.2(CLEARTEXT communication … not permitted). Cleartext to127.0.0.1is allowed, so for local testing runadb reverse tcp:3000 tcp:3000and set the app's server URL tohttp://127.0.0.1:3000(its default). Production uses HTTPS via the proxy — not a code issue. (Consider whether v1 wants anetwork_security_configdev exception; not required for the feature.)
5. Conventions (CLAUDE.md)
- Branch from up-to-date
main; commit/push aswtclaudeusing the token atC:\Users\colby\.gitea_token_claudeviahttp.extraHeader(never inline in URL). - Conventional Commits; end commit messages with the
Co-Authored-By: Claude …+Claude-Session:trailers. PR template requires ticking the AI-assisted box (tool: Claude Code) and the license box. - Use
mcp__gitea__*for PRs/issues. GPL-3.0-or-later.
6. First moves for the fresh session
- Check whether website#93 / docs#32 have merged (
mcp__gitea__pull_request_read). - In
android-app: syncmain,git checkout -b feature/trusted-devices-mfa. - Explore the app's auth module; implement §3 against the §3.2 contract.
- Test with the local server + emulator via the §4 cleartext workaround.
- Open a single
android-appPR; updatedocs/android/PLAN.mdif the app design deviates from §4.1.1.