This commit is contained in:
23 files changed
+2224
-53
No files matched your search
@@ -0,0 +1,61 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models for the Credential Vault RPC surface (issue #1090).
|
||||
*
|
||||
* Backend: `tui_gateway/methods_vault.py`.
|
||||
* Sources represent local encrypted vault or external managers (1Password, Bitwarden).
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class VaultSource(
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String = name,
|
||||
val enabled: Boolean = false,
|
||||
@SerialName("needs_unlock") val needsUnlock: Boolean = false,
|
||||
val unlocked: Boolean = false,
|
||||
val installed: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VaultSourcesResponse(
|
||||
val sources: List<VaultSource> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VaultItem(
|
||||
val id: String,
|
||||
val backend: String = "local",
|
||||
val kind: String = "login",
|
||||
val label: String = "",
|
||||
val origin: String? = null,
|
||||
@SerialName("created_at") val createdAt: String? = null,
|
||||
@SerialName("identifier_type") val identifierType: String? = null,
|
||||
val identifier: String? = null,
|
||||
@SerialName("has_otp") val hasOtp: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VaultListResponse(
|
||||
val items: List<VaultItem> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VaultSourceSetResponse(
|
||||
val name: String? = null,
|
||||
val enabled: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VaultUnlockResponse(
|
||||
val name: String? = null,
|
||||
val unlocked: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VaultLockResponse(
|
||||
val locked: Boolean? = null,
|
||||
)
|
||||
@@ -341,6 +341,43 @@ object EventParser {
|
||||
WsEvent.SecretExpire(requestId, sessionId)
|
||||
}
|
||||
|
||||
// ── Credential Vault prompts (issue #1090) ──────────────────
|
||||
"vault.unlock.request" -> {
|
||||
val requestId = payload?.get("request_id") as? String
|
||||
val backend = payload?.get("backend") as? String
|
||||
val displayName = payload?.get("display_name") as? String
|
||||
WsEvent.VaultUnlockRequest(requestId, sessionId, backend, displayName)
|
||||
}
|
||||
|
||||
"vault.unlock.expire" -> {
|
||||
val requestId = payload?.get("request_id") as? String
|
||||
WsEvent.VaultUnlockExpire(requestId, sessionId)
|
||||
}
|
||||
|
||||
"vault.save_login.request" -> {
|
||||
val requestId = payload?.get("request_id") as? String
|
||||
val origin = payload?.get("origin") as? String
|
||||
val site = payload?.get("site") as? String
|
||||
WsEvent.VaultSaveLoginRequest(requestId, sessionId, origin, site)
|
||||
}
|
||||
|
||||
"vault.save_login.expire" -> {
|
||||
val requestId = payload?.get("request_id") as? String
|
||||
WsEvent.VaultSaveLoginExpire(requestId, sessionId)
|
||||
}
|
||||
|
||||
"vault.code.request" -> {
|
||||
val requestId = payload?.get("request_id") as? String
|
||||
val site = payload?.get("site") as? String
|
||||
val hint = payload?.get("hint") as? String
|
||||
WsEvent.VaultCodeRequest(requestId, sessionId, site, hint)
|
||||
}
|
||||
|
||||
"vault.code.expire" -> {
|
||||
val requestId = payload?.get("request_id") as? String
|
||||
WsEvent.VaultCodeExpire(requestId, sessionId)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown event type: $eventType")
|
||||
WsEvent.Unknown(rawJson)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.m57.hermescontrol.data.ws
|
||||
|
||||
import com.m57.hermescontrol.data.model.VaultItem
|
||||
import com.m57.hermescontrol.data.model.VaultListResponse
|
||||
import com.m57.hermescontrol.data.model.VaultLockResponse
|
||||
import com.m57.hermescontrol.data.model.VaultSource
|
||||
import com.m57.hermescontrol.data.model.VaultSourceSetResponse
|
||||
import com.m57.hermescontrol.data.model.VaultSourcesResponse
|
||||
import com.m57.hermescontrol.data.model.VaultUnlockResponse
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
/**
|
||||
* Repository for Credential Vault RPC operations over WebSocket (issue #1090).
|
||||
*/
|
||||
object VaultRepository {
|
||||
private val json get() = OkHttpProvider.json
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private inline fun <reified T> decode(result: Any?): T? {
|
||||
if (result == null) return null
|
||||
val element: JsonElement =
|
||||
when (result) {
|
||||
is JsonElement -> result
|
||||
is Map<*, *> -> anyToJsonElement(result)
|
||||
is List<*> -> JsonArray(result.map { anyToJsonElement(it) })
|
||||
else -> JsonPrimitive(result.toString())
|
||||
}
|
||||
return json.decodeFromJsonElement(serializer<T>(), element)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun anyToJsonElement(value: Any?): JsonElement =
|
||||
when (value) {
|
||||
null -> JsonNull
|
||||
is JsonElement -> value
|
||||
is Map<*, *> -> JsonObject((value as Map<String, Any?>).mapValues { (_, v) -> anyToJsonElement(v) })
|
||||
is List<*> -> JsonArray(value.map { anyToJsonElement(it) })
|
||||
is String -> JsonPrimitive(value)
|
||||
is Boolean -> JsonPrimitive(value)
|
||||
is Number -> JsonPrimitive(value)
|
||||
else -> JsonPrimitive(value.toString())
|
||||
}
|
||||
|
||||
suspend fun getSources(): List<VaultSource> {
|
||||
val result = HermesWsClient.request(WsMethods.VAULT_SOURCES).await()
|
||||
return decode<VaultSourcesResponse>(result)?.sources ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun setSourceEnabled(
|
||||
name: String,
|
||||
enabled: Boolean,
|
||||
): Boolean {
|
||||
val result =
|
||||
HermesWsClient
|
||||
.request(
|
||||
WsMethods.VAULT_SOURCE_SET,
|
||||
mapOf("name" to name, "enabled" to enabled),
|
||||
).await()
|
||||
return decode<VaultSourceSetResponse>(result)?.enabled == enabled
|
||||
}
|
||||
|
||||
suspend fun unlockSource(
|
||||
name: String,
|
||||
password: String,
|
||||
): Boolean {
|
||||
val result =
|
||||
HermesWsClient
|
||||
.request(
|
||||
WsMethods.VAULT_UNLOCK,
|
||||
mapOf("name" to name, "password" to password),
|
||||
).await()
|
||||
return decode<VaultUnlockResponse>(result)?.unlocked == true
|
||||
}
|
||||
|
||||
suspend fun lockSource(name: String? = null): Boolean {
|
||||
val params = if (name != null) mapOf("name" to name) else emptyMap<String, Any>()
|
||||
val result = HermesWsClient.request(WsMethods.VAULT_LOCK, params).await()
|
||||
return decode<VaultLockResponse>(result)?.locked == true
|
||||
}
|
||||
|
||||
suspend fun listItems(): List<VaultItem> {
|
||||
val result = HermesWsClient.request(WsMethods.VAULT_LIST).await()
|
||||
return decode<VaultListResponse>(result)?.items ?: emptyList()
|
||||
}
|
||||
}
|
||||
@@ -303,6 +303,44 @@ sealed class WsEvent {
|
||||
val sessionId: String?,
|
||||
) : WsEvent()
|
||||
|
||||
// ── Credential Vault prompts (issue #1090) ──────────────────────────
|
||||
|
||||
data class VaultUnlockRequest(
|
||||
val requestId: String?,
|
||||
val sessionId: String? = null,
|
||||
val backend: String? = null,
|
||||
val displayName: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
data class VaultUnlockExpire(
|
||||
val requestId: String?,
|
||||
val sessionId: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
data class VaultSaveLoginRequest(
|
||||
val requestId: String?,
|
||||
val sessionId: String? = null,
|
||||
val origin: String? = null,
|
||||
val site: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
data class VaultSaveLoginExpire(
|
||||
val requestId: String?,
|
||||
val sessionId: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
data class VaultCodeRequest(
|
||||
val requestId: String?,
|
||||
val sessionId: String? = null,
|
||||
val site: String? = null,
|
||||
val hint: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
data class VaultCodeExpire(
|
||||
val requestId: String?,
|
||||
val sessionId: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
// ── Gateway-level errors ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,6 +43,16 @@ object WsMethods {
|
||||
const val SUDO_RESPOND = "sudo.respond"
|
||||
const val SECRET_RESPOND = "secret.respond"
|
||||
|
||||
// ── Credential Vault (issue #1090) ────────────────────────────────────
|
||||
const val VAULT_UNLOCK_RESPOND = "vault.unlock.respond"
|
||||
const val VAULT_SAVE_LOGIN_RESPOND = "vault.save_login.respond"
|
||||
const val VAULT_CODE_RESPOND = "vault.code.respond"
|
||||
const val VAULT_LIST = "vault.list"
|
||||
const val VAULT_SOURCES = "vault.sources"
|
||||
const val VAULT_SOURCE_SET = "vault.source.set"
|
||||
const val VAULT_UNLOCK = "vault.unlock"
|
||||
const val VAULT_LOCK = "vault.lock"
|
||||
|
||||
// ── Heartbeat & Latency (issue #1017) ──────────────────────────────────
|
||||
|
||||
/** Ultra-lightweight liveness check and round-trip latency measurement. */
|
||||
|
||||
Reference in new issue
Block a user