fix: sync mobile gateway auth v3
This commit is contained in:
@@ -1,15 +1,33 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "5d29ee82825e0764775562242cfa1ff4dc79584797dd638f76c9876545454748",
|
"originHash" : "2e6f580ad7d1e839d513aa883350369bf2e4193fad872030fdaea7827f34d8ef",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "commander",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/steipete/Commander.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "9e349575c8e3c6745e81fe19e5bb5efa01b078ce",
|
||||||
|
"version" : "0.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "elevenlabskit",
|
"identity" : "elevenlabskit",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/steipete/ElevenLabsKit",
|
"location" : "https://github.com/steipete/ElevenLabsKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
|
"revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d",
|
||||||
"version" : "0.1.0"
|
"version" : "0.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-concurrency-extras",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
|
||||||
|
"version" : "1.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-syntax",
|
"identity" : "swift-syntax",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -27,6 +45,24 @@
|
|||||||
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
|
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
|
||||||
"version" : "0.99.0"
|
"version" : "0.99.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftui-math",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/gonzalezreal/swiftui-math",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71",
|
||||||
|
"version" : "0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "textual",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/gonzalezreal/textual",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a03c1e103d88de4ea0dd8320ea1611ec0d4b29b3",
|
||||||
|
"version" : "0.2.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.clawdbot.android.chat.ChatMessage
|
|||||||
import com.clawdbot.android.chat.ChatPendingToolCall
|
import com.clawdbot.android.chat.ChatPendingToolCall
|
||||||
import com.clawdbot.android.chat.ChatSessionEntry
|
import com.clawdbot.android.chat.ChatSessionEntry
|
||||||
import com.clawdbot.android.chat.OutgoingAttachment
|
import com.clawdbot.android.chat.OutgoingAttachment
|
||||||
|
import com.clawdbot.android.gateway.DeviceAuthStore
|
||||||
import com.clawdbot.android.gateway.DeviceIdentityStore
|
import com.clawdbot.android.gateway.DeviceIdentityStore
|
||||||
import com.clawdbot.android.gateway.GatewayClientInfo
|
import com.clawdbot.android.gateway.GatewayClientInfo
|
||||||
import com.clawdbot.android.gateway.GatewayConnectOptions
|
import com.clawdbot.android.gateway.GatewayConnectOptions
|
||||||
@@ -62,6 +63,7 @@ class NodeRuntime(context: Context) {
|
|||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
val prefs = SecurePrefs(appContext)
|
val prefs = SecurePrefs(appContext)
|
||||||
|
private val deviceAuthStore = DeviceAuthStore(prefs)
|
||||||
val canvas = CanvasController()
|
val canvas = CanvasController()
|
||||||
val camera = CameraCaptureManager(appContext)
|
val camera = CameraCaptureManager(appContext)
|
||||||
val location = LocationCaptureManager(appContext)
|
val location = LocationCaptureManager(appContext)
|
||||||
@@ -153,6 +155,7 @@ class NodeRuntime(context: Context) {
|
|||||||
GatewaySession(
|
GatewaySession(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
identityStore = identityStore,
|
identityStore = identityStore,
|
||||||
|
deviceAuthStore = deviceAuthStore,
|
||||||
onConnected = { name, remote, mainSessionKey ->
|
onConnected = { name, remote, mainSessionKey ->
|
||||||
operatorConnected = true
|
operatorConnected = true
|
||||||
operatorStatusText = "Connected"
|
operatorStatusText = "Connected"
|
||||||
@@ -188,6 +191,7 @@ class NodeRuntime(context: Context) {
|
|||||||
GatewaySession(
|
GatewaySession(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
identityStore = identityStore,
|
identityStore = identityStore,
|
||||||
|
deviceAuthStore = deviceAuthStore,
|
||||||
onConnected = { _, _, _ ->
|
onConnected = { _, _, _ ->
|
||||||
nodeConnected = true
|
nodeConnected = true
|
||||||
nodeStatusText = "Connected"
|
nodeStatusText = "Connected"
|
||||||
|
|||||||
@@ -189,6 +189,18 @@ class SecurePrefs(context: Context) {
|
|||||||
prefs.edit { putString(key, fingerprint.trim()) }
|
prefs.edit { putString(key, fingerprint.trim()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getString(key: String): String? {
|
||||||
|
return prefs.getString(key, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putString(key: String, value: String) {
|
||||||
|
prefs.edit { putString(key, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(key: String) {
|
||||||
|
prefs.edit { remove(key) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadOrCreateInstanceId(): String {
|
private fun loadOrCreateInstanceId(): String {
|
||||||
val existing = prefs.getString("node.instanceId", null)?.trim()
|
val existing = prefs.getString("node.instanceId", null)?.trim()
|
||||||
if (!existing.isNullOrBlank()) return existing
|
if (!existing.isNullOrBlank()) return existing
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.clawdbot.android.gateway
|
||||||
|
|
||||||
|
import com.clawdbot.android.SecurePrefs
|
||||||
|
|
||||||
|
class DeviceAuthStore(private val prefs: SecurePrefs) {
|
||||||
|
fun loadToken(deviceId: String, role: String): String? {
|
||||||
|
val key = tokenKey(deviceId, role)
|
||||||
|
return prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveToken(deviceId: String, role: String, token: String) {
|
||||||
|
val key = tokenKey(deviceId, role)
|
||||||
|
prefs.putString(key, token.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearToken(deviceId: String, role: String) {
|
||||||
|
val key = tokenKey(deviceId, role)
|
||||||
|
prefs.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tokenKey(deviceId: String, role: String): String {
|
||||||
|
val normalizedDevice = deviceId.trim().lowercase()
|
||||||
|
val normalizedRole = role.trim().lowercase()
|
||||||
|
return "gateway.deviceToken.$normalizedDevice.$normalizedRole"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ data class GatewayConnectOptions(
|
|||||||
class GatewaySession(
|
class GatewaySession(
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
private val identityStore: DeviceIdentityStore,
|
private val identityStore: DeviceIdentityStore,
|
||||||
|
private val deviceAuthStore: DeviceAuthStore,
|
||||||
private val onConnected: (serverName: String?, remoteAddress: String?, mainSessionKey: String?) -> Unit,
|
private val onConnected: (serverName: String?, remoteAddress: String?, mainSessionKey: String?) -> Unit,
|
||||||
private val onDisconnected: (message: String) -> Unit,
|
private val onDisconnected: (message: String) -> Unit,
|
||||||
private val onEvent: (event: String, payloadJson: String?) -> Unit,
|
private val onEvent: (event: String, payloadJson: String?) -> Unit,
|
||||||
@@ -177,6 +178,7 @@ class GatewaySession(
|
|||||||
private val connectDeferred = CompletableDeferred<Unit>()
|
private val connectDeferred = CompletableDeferred<Unit>()
|
||||||
private val closedDeferred = CompletableDeferred<Unit>()
|
private val closedDeferred = CompletableDeferred<Unit>()
|
||||||
private val isClosed = AtomicBoolean(false)
|
private val isClosed = AtomicBoolean(false)
|
||||||
|
private val connectNonceDeferred = CompletableDeferred<String?>()
|
||||||
private val client: OkHttpClient = buildClient()
|
private val client: OkHttpClient = buildClient()
|
||||||
private var socket: WebSocket? = null
|
private var socket: WebSocket? = null
|
||||||
private val loggerTag = "ClawdbotGateway"
|
private val loggerTag = "ClawdbotGateway"
|
||||||
@@ -253,7 +255,8 @@ class GatewaySession(
|
|||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
sendConnect()
|
val nonce = awaitConnectNonce()
|
||||||
|
sendConnect(nonce)
|
||||||
} catch (err: Throwable) {
|
} catch (err: Throwable) {
|
||||||
connectDeferred.completeExceptionally(err)
|
connectDeferred.completeExceptionally(err)
|
||||||
closeQuietly()
|
closeQuietly()
|
||||||
@@ -288,16 +291,30 @@ class GatewaySession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendConnect() {
|
private suspend fun sendConnect(connectNonce: String?) {
|
||||||
val payload = buildConnectParams()
|
val identity = identityStore.loadOrCreate()
|
||||||
|
val storedToken = deviceAuthStore.loadToken(identity.deviceId, options.role)
|
||||||
|
val trimmedToken = token?.trim().orEmpty()
|
||||||
|
val authToken = if (storedToken.isNullOrBlank()) trimmedToken else storedToken
|
||||||
|
val canFallbackToShared = !storedToken.isNullOrBlank() && trimmedToken.isNotBlank()
|
||||||
|
val payload = buildConnectParams(identity, connectNonce, authToken, password?.trim())
|
||||||
val res = request("connect", payload, timeoutMs = 8_000)
|
val res = request("connect", payload, timeoutMs = 8_000)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
val msg = res.error?.message ?: "connect failed"
|
val msg = res.error?.message ?: "connect failed"
|
||||||
|
if (canFallbackToShared) {
|
||||||
|
deviceAuthStore.clearToken(identity.deviceId, options.role)
|
||||||
|
}
|
||||||
throw IllegalStateException(msg)
|
throw IllegalStateException(msg)
|
||||||
}
|
}
|
||||||
val payloadJson = res.payloadJson ?: throw IllegalStateException("connect failed: missing payload")
|
val payloadJson = res.payloadJson ?: throw IllegalStateException("connect failed: missing payload")
|
||||||
val obj = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: throw IllegalStateException("connect failed")
|
val obj = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: throw IllegalStateException("connect failed")
|
||||||
val serverName = obj["server"].asObjectOrNull()?.get("host").asStringOrNull()
|
val serverName = obj["server"].asObjectOrNull()?.get("host").asStringOrNull()
|
||||||
|
val authObj = obj["auth"].asObjectOrNull()
|
||||||
|
val deviceToken = authObj?.get("deviceToken").asStringOrNull()
|
||||||
|
val authRole = authObj?.get("role").asStringOrNull() ?: options.role
|
||||||
|
if (!deviceToken.isNullOrBlank()) {
|
||||||
|
deviceAuthStore.saveToken(identity.deviceId, authRole, deviceToken)
|
||||||
|
}
|
||||||
val rawCanvas = obj["canvasHostUrl"].asStringOrNull()
|
val rawCanvas = obj["canvasHostUrl"].asStringOrNull()
|
||||||
canvasHostUrl = normalizeCanvasHostUrl(rawCanvas, endpoint)
|
canvasHostUrl = normalizeCanvasHostUrl(rawCanvas, endpoint)
|
||||||
val sessionDefaults =
|
val sessionDefaults =
|
||||||
@@ -308,7 +325,12 @@ class GatewaySession(
|
|||||||
connectDeferred.complete(Unit)
|
connectDeferred.complete(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildConnectParams(): JsonObject {
|
private fun buildConnectParams(
|
||||||
|
identity: DeviceIdentity,
|
||||||
|
connectNonce: String?,
|
||||||
|
authToken: String,
|
||||||
|
authPassword: String?,
|
||||||
|
): JsonObject {
|
||||||
val client = options.client
|
val client = options.client
|
||||||
val locale = Locale.getDefault().toLanguageTag()
|
val locale = Locale.getDefault().toLanguageTag()
|
||||||
val clientObj =
|
val clientObj =
|
||||||
@@ -323,22 +345,20 @@ class GatewaySession(
|
|||||||
client.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
client.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val authToken = token?.trim().orEmpty()
|
val password = authPassword?.trim().orEmpty()
|
||||||
val authPassword = password?.trim().orEmpty()
|
|
||||||
val authJson =
|
val authJson =
|
||||||
when {
|
when {
|
||||||
authToken.isNotEmpty() ->
|
authToken.isNotEmpty() ->
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("token", JsonPrimitive(authToken))
|
put("token", JsonPrimitive(authToken))
|
||||||
}
|
}
|
||||||
authPassword.isNotEmpty() ->
|
password.isNotEmpty() ->
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("password", JsonPrimitive(authPassword))
|
put("password", JsonPrimitive(password))
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
val identity = identityStore.loadOrCreate()
|
|
||||||
val signedAtMs = System.currentTimeMillis()
|
val signedAtMs = System.currentTimeMillis()
|
||||||
val payload =
|
val payload =
|
||||||
buildDeviceAuthPayload(
|
buildDeviceAuthPayload(
|
||||||
@@ -349,6 +369,7 @@ class GatewaySession(
|
|||||||
scopes = options.scopes,
|
scopes = options.scopes,
|
||||||
signedAtMs = signedAtMs,
|
signedAtMs = signedAtMs,
|
||||||
token = if (authToken.isNotEmpty()) authToken else null,
|
token = if (authToken.isNotEmpty()) authToken else null,
|
||||||
|
nonce = connectNonce,
|
||||||
)
|
)
|
||||||
val signature = identityStore.signPayload(payload, identity)
|
val signature = identityStore.signPayload(payload, identity)
|
||||||
val publicKey = identityStore.publicKeyBase64Url(identity)
|
val publicKey = identityStore.publicKeyBase64Url(identity)
|
||||||
@@ -359,6 +380,9 @@ class GatewaySession(
|
|||||||
put("publicKey", JsonPrimitive(publicKey))
|
put("publicKey", JsonPrimitive(publicKey))
|
||||||
put("signature", JsonPrimitive(signature))
|
put("signature", JsonPrimitive(signature))
|
||||||
put("signedAt", JsonPrimitive(signedAtMs))
|
put("signedAt", JsonPrimitive(signedAtMs))
|
||||||
|
if (!connectNonce.isNullOrBlank()) {
|
||||||
|
put("nonce", JsonPrimitive(connectNonce))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -416,6 +440,13 @@ class GatewaySession(
|
|||||||
val event = frame["event"].asStringOrNull() ?: return
|
val event = frame["event"].asStringOrNull() ?: return
|
||||||
val payloadJson =
|
val payloadJson =
|
||||||
frame["payload"]?.let { it.toString() } ?: frame["payloadJSON"].asStringOrNull()
|
frame["payload"]?.let { it.toString() } ?: frame["payloadJSON"].asStringOrNull()
|
||||||
|
if (event == "connect.challenge") {
|
||||||
|
val nonce = extractConnectNonce(payloadJson)
|
||||||
|
if (!connectNonceDeferred.isCompleted) {
|
||||||
|
connectNonceDeferred.complete(nonce)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event == "node.invoke.request" && payloadJson != null && onInvoke != null) {
|
if (event == "node.invoke.request" && payloadJson != null && onInvoke != null) {
|
||||||
handleInvokeEvent(payloadJson)
|
handleInvokeEvent(payloadJson)
|
||||||
return
|
return
|
||||||
@@ -423,6 +454,21 @@ class GatewaySession(
|
|||||||
onEvent(event, payloadJson)
|
onEvent(event, payloadJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun awaitConnectNonce(): String? {
|
||||||
|
if (isLoopbackHost(endpoint.host)) return null
|
||||||
|
return try {
|
||||||
|
withTimeout(2_000) { connectNonceDeferred.await() }
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractConnectNonce(payloadJson: String?): String? {
|
||||||
|
if (payloadJson.isNullOrBlank()) return null
|
||||||
|
val obj = parseJsonOrNull(payloadJson)?.asObjectOrNull() ?: return null
|
||||||
|
return obj["nonce"].asStringOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleInvokeEvent(payloadJson: String) {
|
private fun handleInvokeEvent(payloadJson: String) {
|
||||||
val payload =
|
val payload =
|
||||||
try {
|
try {
|
||||||
@@ -544,19 +590,26 @@ class GatewaySession(
|
|||||||
scopes: List<String>,
|
scopes: List<String>,
|
||||||
signedAtMs: Long,
|
signedAtMs: Long,
|
||||||
token: String?,
|
token: String?,
|
||||||
|
nonce: String?,
|
||||||
): String {
|
): String {
|
||||||
val scopeString = scopes.joinToString(",")
|
val scopeString = scopes.joinToString(",")
|
||||||
val authToken = token.orEmpty()
|
val authToken = token.orEmpty()
|
||||||
return listOf(
|
val version = if (nonce.isNullOrBlank()) "v1" else "v2"
|
||||||
"v1",
|
val parts =
|
||||||
deviceId,
|
mutableListOf(
|
||||||
clientId,
|
version,
|
||||||
clientMode,
|
deviceId,
|
||||||
role,
|
clientId,
|
||||||
scopeString,
|
clientMode,
|
||||||
signedAtMs.toString(),
|
role,
|
||||||
authToken,
|
scopeString,
|
||||||
).joinToString("|")
|
signedAtMs.toString(),
|
||||||
|
authToken,
|
||||||
|
)
|
||||||
|
if (!nonce.isNullOrBlank()) {
|
||||||
|
parts.add(nonce)
|
||||||
|
}
|
||||||
|
return parts.joinToString("|")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun normalizeCanvasHostUrl(raw: String?, endpoint: GatewayEndpoint): String? {
|
private fun normalizeCanvasHostUrl(raw: String?, endpoint: GatewayEndpoint): String? {
|
||||||
|
|||||||
@@ -84,5 +84,7 @@ private fun sha256Hex(data: ByteArray): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun normalizeFingerprint(raw: String): String {
|
private fun normalizeFingerprint(raw: String): String {
|
||||||
return raw.lowercase().filter { it in '0'..'9' || it in 'a'..'f' }
|
val stripped = raw.trim()
|
||||||
|
.replace(Regex("^sha-?256\\s*:?\\s*", RegexOption.IGNORE_CASE), "")
|
||||||
|
return stripped.lowercase().filter { it in '0'..'9' || it in 'a'..'f' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2026.1.11-4</string>
|
<string>2026.1.9</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>202601113</string>
|
<string>20260109</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
Sources/Bridge/BridgeClient.swift
|
Sources/Gateway/GatewayConnectionController.swift
|
||||||
Sources/Bridge/BridgeConnectionController.swift
|
Sources/Gateway/GatewayDiscoveryDebugLogView.swift
|
||||||
Sources/Bridge/BridgeDiscoveryDebugLogView.swift
|
Sources/Gateway/GatewayDiscoveryModel.swift
|
||||||
Sources/Bridge/BridgeDiscoveryModel.swift
|
Sources/Gateway/GatewaySettingsStore.swift
|
||||||
Sources/Bridge/BridgeEndpointID.swift
|
Sources/Gateway/KeychainStore.swift
|
||||||
Sources/Bridge/BridgeSession.swift
|
|
||||||
Sources/Bridge/BridgeSettingsStore.swift
|
|
||||||
Sources/Bridge/KeychainStore.swift
|
|
||||||
Sources/Camera/CameraController.swift
|
Sources/Camera/CameraController.swift
|
||||||
Sources/Chat/ChatSheet.swift
|
Sources/Chat/ChatSheet.swift
|
||||||
Sources/Chat/IOSBridgeChatTransport.swift
|
Sources/Chat/IOSGatewayChatTransport.swift
|
||||||
Sources/ClawdbotApp.swift
|
Sources/ClawdbotApp.swift
|
||||||
|
Sources/Location/LocationService.swift
|
||||||
Sources/Model/NodeAppModel.swift
|
Sources/Model/NodeAppModel.swift
|
||||||
Sources/RootCanvas.swift
|
Sources/RootCanvas.swift
|
||||||
Sources/RootTabs.swift
|
Sources/RootTabs.swift
|
||||||
@@ -17,6 +15,7 @@ Sources/Screen/ScreenController.swift
|
|||||||
Sources/Screen/ScreenRecordService.swift
|
Sources/Screen/ScreenRecordService.swift
|
||||||
Sources/Screen/ScreenTab.swift
|
Sources/Screen/ScreenTab.swift
|
||||||
Sources/Screen/ScreenWebView.swift
|
Sources/Screen/ScreenWebView.swift
|
||||||
|
Sources/SessionKey.swift
|
||||||
Sources/Settings/SettingsNetworkingHelpers.swift
|
Sources/Settings/SettingsNetworkingHelpers.swift
|
||||||
Sources/Settings/SettingsTab.swift
|
Sources/Settings/SettingsTab.swift
|
||||||
Sources/Settings/VoiceWakeWordsSettingsView.swift
|
Sources/Settings/VoiceWakeWordsSettingsView.swift
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2026.1.11-4</string>
|
<string>2026.1.9</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>202601113</string>
|
<string>20260109</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ public actor GatewayChannelActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func decodeMessageData(_ msg: URLSessionWebSocketTask.Message) -> Data? {
|
private nonisolated func decodeMessageData(_ msg: URLSessionWebSocketTask.Message) -> Data? {
|
||||||
let data: Data? = switch msg {
|
let data: Data? = switch msg {
|
||||||
case let .data(data): data
|
case let .data(data): data
|
||||||
case let .string(text): text.data(using: .utf8)
|
case let .string(text): text.data(using: .utf8)
|
||||||
|
|||||||
@@ -108,5 +108,9 @@ private func sha256Hex(_ data: Data) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func normalizeFingerprint(_ raw: String) -> String {
|
private func normalizeFingerprint(_ raw: String) -> String {
|
||||||
raw.lowercased().filter(\.isHexDigit)
|
let stripped = raw.replacingOccurrences(
|
||||||
|
of: #"(?i)^sha-?256\s*:?\s*"#,
|
||||||
|
with: "",
|
||||||
|
options: .regularExpression)
|
||||||
|
return stripped.lowercased().filter(\.isHexDigit)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user