refactor: proper OkHttp CookieJar integration (issue #470) (#483)

Replace the manual session-cookie interceptor in ApiClient and the
manual cookie branch in TokenRefreshAuthenticator with a shared
PersistentCookieJar backed by an encrypted, per-server EncryptedCookieStore.

- Add CookieStore iface + EncryptedCookieStore (EncryptedSharedPreferences,
  per-server scope, legacy single-cookie migration)
- Add CookieSerialization (OkHttp Cookie <-> holder) + PersistentCookieJar
  (in-mem cache, async persist, atomic useStore scope switching, prune,
  session-cookie accessor)
- Add CookieManager coordinator (init/useStore/prune/clear) and wire its
  cookieJar into OkHttpProvider base/websocket/probe (lazy)
- AuthManager.get/setSessionCookie now delegate to CookieManager; add
  pruneServerCache(); clear path drops the scope's session cookie
- AuthLoginViewModel + HermesWsClient rely on the jar for Set-Cookie capture
  and session-cookie re-attachment (no manual header injection)
- Add fakes (FakeEncryptedCookieStore/buildFakePersistentCookieJar) + unit
  tests (PersistentCookieJarTest, CookieManagerTest); inject fake jar into
  ApiClientTest/HermesWsClientTest setup

WS token-out-of-URL (step 5) deferred: backend hermes-agent authenticates
WS only via ws.query_params, so moving it requires a coordinated backend
change (noted for follow-up).

Verified: testDebugUnitTest (334 passing) + assembleDebug green via local SDK.
This commit is contained in:
M57 authored and GitHub committed 2026-07-07 21:51:04 +03:00
1 parent ce7048c07e
commit d178d7f69e
15 files changed
+953 -88

No files matched your search

@@ -176,6 +176,9 @@ object HermesWsClient {
* If a session cookie is present (gated mode), mint a fresh WS ticket
* from the dashboard. The ticket is single-use and has a 30-second TTL,
* so we must mint a new one on every connect (first launch and reconnect).
*
* The session cookie is attached automatically by the shared CookieJar on
* OkHttpProvider.probe (issue #470), so we no longer inject it manually.
*/
private fun refreshWsTicketIfNeeded() {
val sessionCookie = AuthManager.getSessionCookie()
@@ -188,13 +191,12 @@ object HermesWsClient {
Request
.Builder()
.url("http://${AuthManager.getHost()}:${AuthManager.getPort()}/api/auth/ws-ticket")
.header("Cookie", "hermes_session_at=$sessionCookie")
.post("{}".toRequestBody())
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
val ticketMatch = Regex(""""ticket":"([^"]+)"""").find(body)
val ticketMatch = Regex("""\"ticket\":\"([^\"]+)\""").find(body)
val ticket = ticketMatch?.groupValues?.getOrNull(1)
if (!ticket.isNullOrBlank()) {
AuthManager.setToken(ticket)