S.H.O.N.A.R._Desktop_Companion/shared/com/shonar/provider/ShonarProvider.kt
avi 76c867fca4 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
2026-09-14 17:14:54 -05:00

77 lines
3 KiB
Kotlin

package com.shonar.provider
import java.io.File
/**
* The single contract between SHONAR and any server (or the device itself).
*
* Everything the app needs from "the cloud" goes through this interface;
* UI, sync engine, and database reference providers by descriptor id only.
* LocalOnlyProvider is a first-class implementation, so a missing server is
* never a special case.
*
* Contract rules (enforced by the shared provider contract test suite):
* - all suspends are safe to cancel; partial uploads leave no visible object
* - upload() is idempotent per RecordingDraft.id (re-upload replaces the
* same key, never duplicates)
* - originals are immutable after successful upload until delete()
* - no method ever logs credentials, tokens, audio bytes, or transcripts
* - errors are ProviderError subtypes with secret-free messages
*/
interface ShonarProvider {
val descriptor: ProviderDescriptor
/** Current auth lifecycle; providers push updates here. */
val authState: kotlinx.coroutines.flow.StateFlow<AuthState>
/**
* Is there a SHONAR-compatible service at [baseUrl]? Purely read-only;
* must not require or request credentials. TlsFailure carries the SPKI
* fingerprint so the UI can run explicit trust-on-first-use approval —
* never silently accept.
*/
suspend fun probe(baseUrl: ServerUrl): ProbeResult
/** Validate [credential] against the server, then hand it to the secure store. */
suspend fun connect(credential: ProviderCredential): Unit
/** Re-validate stored credential (app start / after network return). */
suspend fun reconnect(): AuthState
/**
* Disconnect locally; if [revokeOnServer] and the provider supports it,
* revoke the credential server-side first (best effort — local state is
* cleared even if revoke fails, with the failure surfaced).
*/
suspend fun disconnect(revokeOnServer: Boolean)
/** Provider-native account/data deletion, then wipe local state. */
suspend fun deleteAccountAndData()
// ---- storage -----------------------------------------------------------
/**
* Upload the original audio for [draft], reporting [onProgress] 0..1.
* Implementations use chunked/resumable transfers when the capability is
* advertised. Returns the ref for later download/delete/sidecars.
*/
suspend fun upload(draft: RecordingDraft, onProgress: (Float) -> Unit): RemoteRef
suspend fun download(ref: RemoteRef, dest: File, onProgress: (Float) -> Unit)
suspend fun delete(ref: RemoteRef)
suspend fun list(cursor: String?): Page<RemoteRecording>
// ---- sidecars (transcript/summary/... JSON, synced independently) ------
suspend fun putSidecar(ref: RemoteRef, kind: SidecarKind, bytes: ByteArray)
/** null when the sidecar does not exist remotely. */
suspend fun getSidecar(ref: RemoteRef, kind: SidecarKind): ByteArray?
// ---- status ------------------------------------------------------------
suspend fun storageLocationSummary(): StorageLocation
}