Standalone Shonar Desktop: vendor portable sources + local engine; decouple from ~/Projects/Shonar

- shared/ = portable Android-origin sources vendored from deferred/desktop-server
  (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only)
- backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the
  old checkout, PYTHONPATH pins THIS backend's code over any editable install
- repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh
  watches shared/ + backend/
- Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010,
  self-migrates, /healthz ok
This commit is contained in:
avi 2026-09-14 17:14:54 -05:00
commit 76c867fca4
136 changed files with 21099 additions and 0 deletions

View file

@ -0,0 +1,91 @@
package com.shonar.provider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLHandshakeException
/**
* Unauthenticated handshake against the SHONAR backend's
* GET /api/v1/provider-info. Used by the provider-selection screen and by
* Start9/Umbrel platform probes to identify SHONAR-compatible services.
*
* TLS policy (docs/server-providers.md §4):
* - full system verification by default, no bypass, ever.
* - a handshake failure yields ProbeResult.TlsFailure with the peer cert's
* SPKI SHA-256 (recorded by the TOFU trust manager) so the UI can run
* explicit trust-on-first-use approval; this client NEVER retries with
* verification disabled.
*/
class ShonarHandshake(
private val client: OkHttpClient = defaultClient(),
private val tofu: TofuManager? = null,
) {
suspend fun probe(url: ServerUrl): ProbeResult = withContext(Dispatchers.IO) {
val endpoint = url.origin + "/api/v1/provider-info"
val request = Request.Builder().url(endpoint).get().build()
try {
client.newCall(request).execute().use { resp ->
if (resp.code == 200) {
parseBody(resp.body?.string().orEmpty())
} else {
ProbeResult.Incompatible
}
}
} catch (e: SSLHandshakeException) {
ProbeResult.TlsFailure(fingerprintFor(url.host))
} catch (e: javax.net.ssl.SSLPeerUnverifiedException) {
// Hostname mismatch: strict verifier stays strict, but the UI
// should still say *why* instead of a generic network error.
ProbeResult.TlsFailure(fingerprintFor(url.host))
} catch (e: Exception) {
// message must stay generic: exception text can contain URLs but
// never credentials (this call sends no credentials at all)
ProbeResult.NetworkError(e.javaClass.simpleName)
}
}
private fun parseBody(json: String): ProbeResult = try {
val root = org.json.JSONObject(json)
if (root.optString("kind") != "shonar") {
ProbeResult.Incompatible
} else {
val caps = root.optJSONObject("capabilities") ?: org.json.JSONObject()
val set = mutableSetOf<ProviderDescriptor.Capability>()
if (caps.optBoolean("chunked_upload")) set += ProviderDescriptor.Capability.CHUNKED_UPLOAD
if (caps.optBoolean("server_transcription")) set += ProviderDescriptor.Capability.SERVER_TRANSCRIPTION
if (caps.optBoolean("server_summary")) set += ProviderDescriptor.Capability.SERVER_SUMMARY
if (caps.optBoolean("account_deletion")) set += ProviderDescriptor.Capability.ACCOUNT_DELETION
ProbeResult.Compatible(
descriptor = ProviderDescriptor(
id = ProviderRegistry.CUSTOM_SHONAR_ID,
displayName = "Custom SHONAR server",
capabilities = set,
),
serverName = root.optString("storage_backend", "server"),
version = root.optString("version", "unknown"),
)
}
} catch (e: Exception) {
ProbeResult.Incompatible
}
/**
* SPKI SHA-256 recorded by the TOFU trust manager during the failed
* handshake, or "unknown" when nothing was captured (plain HTTP,
* pre-handshake failure, or no TOFU manager wired).
*/
private fun fingerprintFor(host: String): String =
tofu?.failureFor(host)?.spkiHex ?: "unknown"
companion object {
fun defaultClient(): OkHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.followRedirects(false) // do not silently follow redirects to other hosts
.build()
}
}