Commit Graph

6 Commits

Author SHA1 Message Date
26b8eecde6 fix(security): declare explicit network security config to forbid cleartext
All checks were successful
PR Checks / android-build (pull_request) Successful in 10m30s
The app is purely an HTTPS API client, but the manifest left
usesCleartextTraffic implicit, which SonarQube S5332 flags (cleartext is
implicitly permitted on older Android and a merged library manifest could
re-enable it). Add an explicit network security config:

- main/release: base-config cleartextTrafficPermitted="false" (no cleartext).
- debug override (app/src/debug/res/xml): re-permits cleartext to loopback
  (127.0.0.1/localhost) only, for local dev against http://127.0.0.1:3000.

This mirrors ServerUrl's rule (HTTPS required in release, HTTP allowed in
debug via allowInsecureHttp = BuildConfig.DEBUG) at the platform socket
layer. It also fixes a latent gap: at targetSdk 28+ the platform default
already blocks cleartext, so the debug loopback path only actually works
with the explicit domain-config now added.

Docs updated in RunicGateway/docs (android/PLAN.md M1).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 23:39:12 -05:00
987ddb54f8 feat(sso): App Links autoVerify callback + paired-host trust check
All checks were successful
PR Checks / android-build (pull_request) Successful in 20m53s
Add the app side of Android App Links (M9 follow-up, docs/android/APP_LINKS.md),
layered on the M9 Part 2 native SSO callback:

- Build-time `appLinkHost` Gradle property -> BuildConfig.APP_LINK_HOST +
  manifestPlaceholders["appLinkHost"]. autoVerify needs a literal host, so the
  generic multi-tenant build leaves it empty (placeholder falls back to the
  reserved runic-gateway.invalid sentinel, making the filter inert); a
  white-label build bakes one host with -PappLinkHost=play.myshard.com.
- Manifest: an autoVerify https `/mobile/callback` intent-filter beside the
  unchanged custom-scheme one (the permanent fallback).
- SsoAuthManager: request the https App Link redirect_uri iff the baked host
  matches the paired shard host; matchesAppLinkCallback() enforces a paired-host
  trust check (host must equal the currently-paired base URL host) as
  defense-in-depth. Both matchers feed the same complete()/exchange path.
- MainActivity routes custom-scheme and App Link callbacks identically.

+5 JVM tests (SsoAuthManagerTest -> 14). Built green (JDK 21,
-Pksp.incremental=false); white-label host substitution verified in the merged
manifest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-20 18:37:32 -05:00
7665975d59 feat(auth): M9 Part 2 — native in-app SSO via the mobile bridge
All checks were successful
PR Checks / android-build (pull_request) Successful in 20m34s
Add the app client for the Mobile SSO Authorization Bridge (PLAN.md §4.2):
native "Sign in with <provider>" without shipping any OAuth secret.

- Pkce: pure-JVM RFC 7636 S256 verifier/challenge + CSRF state, encoded to
  match the backend's base64url(SHA-256) exactly.
- SsoAuthManager (Singleton): mints PKCE+state, builds the /auth/mobile/sso/start
  URL for a Custom Tab, verifies the returned state, exchanges the one-time code
  with the stashed verifier, and drives the existing SessionManager.onSignedIn —
  no new token-storage or refresh code. Pending flow is in-memory (fails closed on
  process death). Exposes an outcome StateFlow the login screen consumes.
- SsoApi + DTOs: GET /auth/providers discovery and POST /auth/mobile/sso/exchange
  (tagged NO_SESSION so a credential 401 isn't read as an expired session).
- MainActivity: runicgateway://auth/callback intent-filter + singleTop; parses the
  callback Uri (the Android edge) and hands raw params to SsoAuthManager.
- LoginScreen/ViewModel: render a button per discovered provider, opening the
  bridge in a Custom Tab; fall back to the website login hand-off when none.

Additive — no other screen's data flow changes; no backend work. Custom scheme
only for now (App Links deferred, APP_LINKS.md).

Tests (JVM, +14): Pkce vector/charset, start-URL building, and the full
complete() flow over a fake SsoApi + real SessionManager (success signs in;
state mismatch / missing pending fail without exchanging; error callback →
declined; 401 → expired-code; replay finds no pending).

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-20 17:52:36 -05:00
e2ced06a83 feat(push): M7 Part 2 — opt-in push notifications (embedded ntfy distributor)
All checks were successful
PR Checks / android-build (pull_request) Successful in 20m16s
Implements the app side of M7 push (docs/android/PLAN.md §11). The app EMBEDS
its own distributor — ntfy is only the relay server, no second app installed,
no Google Play Services. New feature slice; no existing screen's data flow
changes.

- core/push: NtfyTopic (random unguessable topic + endpoint/SSE URL builders),
  PushTickle (content-free { stream, ref } parser over ntfy's SSE envelope),
  NtfyStreamClient (bare-client OkHttp SSE to <ntfy>/<topic>/sse, reconnect/
  backoff cloned from ShardStreamClient), PushNotifier (channels + per-stream
  deep-link notification), PushService (foreground service holding the
  connection), PushManager (mint topic / register-unregister device / start-stop,
  keyed to the session), PushPreferences (DataStore state).
- data: NotificationsApi + DTOs + NotificationsRepository over the merged
  /auth/me/devices + /auth/me/notifications/* contract; push block on SettingsDto.
- ui/notifications: settings screen + VM — per-stream toggles, personal streams
  greyed until a game account is linked, POST_NOTIFICATIONS request on enable.
- Navigation: Routes.NOTIFICATIONS + stream→route deep-link map, menu entry,
  RunicApp + MainActivity intent handling; teardown wired into logout + server
  switch (deregister while bearer valid) and every sign-out (local, via session
  observer).
- Manifest: POST_NOTIFICATIONS + FOREGROUND_SERVICE(_DATA_SYNC) + the service.

Deviation (recorded in PLAN.md): direct-ntfy transport, no UnifiedPush library
— the plan's stated likely path; keeps the APK Google-free and dependency-light,
with a PushResult/transport seam for a future FCM Play flavor. Requires the small
companion push.ntfyUrl settings field (website#<pr>).

18 new JVM tests; :app:testDebugUnitTest + lintDebug + assembleDebug green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 15:25:59 -05:00
3647d25be2 ci(android): fix CI runner (JDK/SDK/gradlew) + variant test
Some checks failed
PR Checks / android-build (pull_request) Failing after 2m48s
PR #2 merged the M0 scaffold with the initial CI, which fails on this
self-hosted runner. Fix forward:

- JDK 17 via apt (openjdk-17-jdk-headless) + JAVA_HOME, replacing
  actions/setup-java: the runner can't resolve api.adoptium.net (EAI_AGAIN).
- Explicit `sdkmanager` install of platform-tools + platforms;android-35 +
  build-tools;35.0.0 so the build never relies on AGP auto-download.
- `chmod +x ./gradlew` in the run step (and set the 100755 git bit): this
  runner's checkout drops file modes, so `./gradlew` alone hit
  "Permission denied" (exit 126).
- ScaffoldSanityTest: drop the BuildConfig.DEBUG assertion, which fails under
  testReleaseUnitTest (the `test` task runs both variants); assert VERSION_NAME.
- Remove the redundant android:label on MainActivity (RedundantLabel lint).

Verified locally against the Android Studio SDK (platform 35 + build-tools
35.0.0): `./gradlew lint test assembleDebug` passes and builds a debug APK.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 13:38:12 -05:00
c920e8805b chore(scaffold): M0 Gradle + Compose + Hilt skeleton with CI
Some checks failed
PR Checks / android-build (pull_request) Failing after 5m13s
Stand up the Android-app repo per docs/android/PLAN.md M0: a buildable
Kotlin + Jetpack Compose (Material 3) single-activity skeleton wired for
Hilt, ready for the M1-M4 functional pass.

- Gradle 8.7 wrapper; AGP 8.6.1 / Kotlin 2.0.20, JDK 17, minSdk 29, target 35.
- Version catalog (gradle/libs.versions.toml) pins the full planned stack
  (Compose, Hilt, Retrofit/OkHttp + kotlinx.serialization, DataStore,
  security-crypto, Coil, Navigation) so later milestones reference by alias.
- RunicGatewayApp (@HiltAndroidApp) + MainActivity (Compose) + ui/theme/*.
- Strings externalized from day one; adaptive launcher icon; backup rules
  exclude the token store / DataStore (no session material off-device).
- CI: .gitea/workflows/pr-checks.yml gates PRs with lint + test + assembleDebug
  (JDK 17 + Android SDK on the self-hosted runner; debug builds auto-signed,
  no secrets). Placeholder JVM unit test so the test gate runs.
- .gitattributes forces LF on gradlew so the wrapper runs on the Linux runner.

Verified locally: `gradle help`/`projects` configure the :app module and
resolve all six plugins cleanly (full assemble needs the Android SDK, done in CI).

app id: com.runicgateway.app (PLAN.md §13, pending runicgateway.app domain).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 12:52:35 -05:00