Skip to content

feat(app): add safe backend URL override - #12308

Open
arhxam wants to merge 1 commit into
BasedHardware:mainfrom
arhxam:feat/self-hosted-backend-override
Open

feat(app): add safe backend URL override#12308
arhxam wants to merge 1 commit into
BasedHardware:mainfrom
arhxam:feat/self-hosted-backend-override

Conversation

@arhxam

@arhxam arhxam commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What changed and why

Expose the existing API-base override in Developer Settings for local/community development, persist and restore it before services initialize, and validate schemes/hosts before activation. Custom authorities never receive Omi credentials, public endpoints require HTTPS, invalid persisted values fail closed, and release builds remain locked to their flavor-controlled data plane.

Refs #6603

Product invariants affected

  • INV-DATA-1 — preserves the existing production-family authority: runtime overrides are unavailable in release builds and malformed persisted state fails closed to the flavor backend.

How it was verified

  • flutter test test/unit/backend_url_override_test.dart test/unit/env_test.dart — all 32 validation, persistence, credential-boundary, and production-routing tests passed.
  • bash scripts/analyze_ratchet.sh — analyzer ratchet passed.
  • No requests were sent to a real self-hosted server; parsing, startup restoration, release gating, and credential attachment are covered hermetically.

Tests

  • Added URL normalization and private-network/HTTPS validation coverage.
  • Added startup restore, clear, invalid-state, and release fail-closed coverage.
  • Added exact-host credential-boundary coverage, including a deceptive subdomain.
  • Ran the locked production data-plane routing suite.
  • Real self-hosted backend connection (no deployment configured locally).

Failure-Class: none

Review in cubic

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @arhxam - this is a careful implementation of #6603 and the credential-isolation work is genuinely well done. Reviewed all 8 changed files at cfd41bc.

What I verified

  • app/lib/env/backend_url_override.dart - parse() enforces http/https only, rejects embedded credentials, query strings, and fragments, requires HTTPS for anything outside loopback/host.docker.internal/::1/RFC1918/CGNAT, and normalizes the trailing slash. restore() fails closed on invalid persisted state and on release builds (runtimeAllowed defaults to !kReleaseMode).
  • app/lib/backend/http/shared.dart - the old substring rule url.contains('api.omi.me') is replaced by exact-host matching in shouldAttachOmiCredentials over {api.omi.me, api.omiapi.com} (https/wss only). That is stricter and also fixes a latent weakness: a deceptive host like api.omi.me.attacker.example no longer matches. Under an active override, _isRequiredAuthCheck returns early, shouldHonorRequestedOmiAuth de-authenticates legacy unconditional-auth callers, and X-Account-Generation is suppressed too, so neither the bearer token nor account metadata crosses to a custom authority. The 401-replay path in makeRawApiCall recomputes the effective auth check inside buildHeaders, so a custom backend returning 401 cannot trigger an Omi token refresh/replay against itself.
  • app/lib/env/env.dart - hasApiBaseUrlOverride plus the production clear() alias; override takes precedence over the dart-define and flavor values in apiBaseUrl, which is the intended precedence for a runtime override.
  • app/lib/main.dart - SharedPreferencesUtil.init() moved earlier so BackendUrlOverride.restore() lands after startup routing validation and before service init; a persisted override therefore cannot trip validateStartupRouting on relaunch, and services see the final base URL.
  • app/lib/backend/preferences.dart - plain get/set passthrough for customBackendUrl, consistent with neighboring keys.
  • app/lib/pages/settings/developer.dart - the section is behind !kReleaseMode, so release builds never render it.
  • app/lib/providers/developer_mode_provider.dart - save path is also release-gated; parse errors surface via snackbar and abort the save without persisting a bad value; an empty field clears the override.
  • app/test/unit/backend_url_override_test.dart - good hermetic coverage: normalization, cleartext scope, rejection cases (public cleartext, user:pass@host, fragments, ftp://), startup restore/clear, invalid fail-closed, release fail-closed, and credential-boundary cases including the deceptive subdomain.

Checks are green (Dart Analyze & Tests, Android Compile Smoke, Hygiene, Formatting).

Non-blocking suggestions

  1. The credential boundary is unit-tested at the helper level; one integration-style test that calls buildHeaders end-to-end with an override active and asserts no Authorization/X-Account-Generation headers would lock the boundary in at the seam where regressions would actually happen.
  2. Worth being aware (not a defect): with a localDev debug build, pointing the override at a public HTTPS host does not re-run startup routing validation - that is precisely the self-hosting use case, just noting the invariant surface.

Human review needed for

  • A product-direction decision: An ability to change link and add a self-hosted backend inside of an app #842 asked for the same underlying capability and was closed as not planned citing e2e auth. The debug-only scoping and credential isolation here address the safety side of that objection, but whether the official app should carry a self-hosting backend override is a maintainer call.
  • Sign-off on the modified credential-attachment logic in shared.dart (exact-host allowlist) since it changes when Authorization is attached for all API traffic, not just the override path.

by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added needs-maintainer-review Needs a human maintainer to sign off before merge security-review Touches auth, provider routing, secrets, or security-sensitive surfaces flutter flutter work labels Aug 27, 2026
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Following up on the earlier review of this head — the technical assessment stands, and one additional consequence is worth having on the record before the product call.

Identity-plane note: Env.authApiBaseUrl resolves through authApiBaseUrlForProfile(profile, servingApiBaseUrl: apiBaseUrl) (app/lib/env/env.dart), so for every non-mobileBeta profile it follows the runtime override. With the override active in a debug build, the OAuth endpoints in app/lib/services/auth_service.dart (v1/auth/authorize and v1/auth/token) also target the custom backend — the override moves the identity plane, not just the serving/data plane. This isn't a regression: OMI_API_BASE_URL via dart-define already had the same reach, everything here is debug-gated and user-initiated, and a usable self-hosted stack arguably needs auth to follow the backend. But since the PR description frames the invariant as data-plane (INV-DATA-1), maintainers ruling on #6603 may want to decide explicitly whether runtime overrides should keep OAuth pinned to the production identity plane (the way mobileBeta does) or follow the backend. One sentence in the PR description settling that intent would make the invariant complete.

Everything else from the earlier verification holds: credential isolation in app/lib/backend/http/shared.dart (exact-host matching over api.omi.me/api.omiapi.com, https/wss only) is strictly stricter than the old substring rule; under an active override neither the bearer token nor X-Account-Generation crosses to a custom authority, and the 401-replay path can't leak either. Validation in BackendUrlOverride.parse (scheme, credentials, query/fragment, HTTPS-off-private-networks, trailing-slash normalization) and the fail-closed restore in release builds are all covered by the new unit tests.

Still leaving the merge decision to a human maintainer: the product-direction call on a runtime self-hosting override (#6603, with #842 previously closed as not planned) — now including the identity-plane question above — and sign-off on the changed credential-attachment logic in shared.dart, which affects Authorization attachment for all API traffic.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flutter flutter work needs-maintainer-review Needs a human maintainer to sign off before merge security-review Touches auth, provider routing, secrets, or security-sensitive surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants