Compare commits

...
Author SHA1 Message Date
Bailey Dixon 6395e73927 Merge remote-tracking branch 'origin/dev' into fix/android-basic-auth-paste
# Conflicts:
#	CHANGELOG.md
2026-09-09 21:20:33 -04:00
Bailey Dixon 0c337c9384 Merge pull request #569 from Codename-11/fix/plugin-availability-probe
fix(plugin): share relay availability checks
2026-09-09 21:06:46 -04:00
Bailey Dixon ac1f42b7d5 Merge remote-tracking branch 'origin/dev' into fix/android-basic-auth-paste
# Conflicts:
#	CHANGELOG.md
2026-09-09 20:21:19 -04:00
Bailey Dixon 88591fca89 fix(android): normalize pasted dashboard credentials
Refs #541
2026-09-09 20:20:48 -04:00
3 changed files with 42 additions and 2 deletions
+1
View File
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
### Fixed
- Android Dashboard sign-in removes pasted line breaks from username and password fields, matching the browser login while preserving every other credential character. (#541)
- **Relay tool availability avoids repeated Windows loopback delays and preserves multi-PC capabilities.** Host-local Android, Desktop, and Phone paths use explicit IPv4 loopback, while Desktop checks share a bounded health snapshot that preserves per-client advertisements and fails closed when Hermes-Relay is unavailable. (#562, #563)
- Android keeps saved Dashboard sign-ins bound to their connection when switching gateways, rather than letting a stale resolver route invalidate another connection's session.
- Bot Mode no longer crashes when different connections have bots with the same profile name. Both the conversation list and Active Now strip preserve each bot's connection, and opening progress appears only on the selected bot.
@@ -1525,10 +1525,14 @@ class DashboardApiClient(
password: String,
next: String = "/",
): Result<DashboardLoginResponse> = withContext(Dispatchers.IO) {
// Match the Dashboard's single-line HTML username/password controls:
// remove only forbidden line breaks and preserve every other code point.
val normalizedUsername = username.replace("\r", "").replace("\n", "")
val normalizedPassword = password.replace("\r", "").replace("\n", "")
val payload = buildJsonObject {
put("provider", provider)
put("username", username)
put("password", password)
put("username", normalizedUsername)
put("password", normalizedPassword)
put("next", next)
}
val httpUrl = resolveUrl("/auth/password-login")
@@ -12,6 +12,7 @@ import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.ResponseBody.Companion.toResponseBody
@@ -659,6 +660,40 @@ class DashboardApiClientTest {
assertEquals("basic", session.provider)
}
@Test
fun passwordLogin_stripsOnlyBrowserForbiddenLineBreaksFromCredentials() = runTest {
val preservedCredential = " \t\u00A0påss\u200B "
val cases = listOf(
listOf("user", "line\rbreak", "user", "linebreak"),
listOf("user", "line\nbreak", "user", "linebreak"),
listOf("user", "line\r\nbreak", "user", "linebreak"),
listOf("us\r\ner", "secret", "user", "secret"),
listOf(
preservedCredential,
preservedCredential,
preservedCredential,
preservedCredential,
),
)
repeat(cases.size) {
server.enqueue(
MockResponse()
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody("""{"ok": true, "next": "/"}"""),
)
}
val client = DashboardApiClient(baseUrl = server.url("/").toString())
cases.forEach { (username, password, expectedUsername, expectedPassword) ->
client.loginPassword(username = username, password = password).getOrThrow()
val body = Json.parseToJsonElement(server.takeRequest().body.readUtf8()).jsonObject
assertEquals(expectedUsername, body["username"]?.jsonPrimitive?.content)
assertEquals(expectedPassword, body["password"]?.jsonPrimitive?.content)
}
}
private fun storedCookie(
name: String,
value: String,