From 87316e07d80eff6be4c6616d0ff93d7feb7af77d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 12:13:35 +0000 Subject: [PATCH] refactor(macos): share pairing and ui dedupe utilities --- .../CanvasWindowController+Testing.swift | 15 +---- .../DevicePairingApprovalPrompter.swift | 24 ++------ .../OpenClaw/GatewayDiscoveryMenu.swift | 30 ++-------- .../OpenClaw/GatewayEndpointStore.swift | 55 ++++++++--------- .../NodePairingApprovalPrompter.swift | 26 ++------ .../OpenClaw/OnboardingView+Pages.swift | 22 +------ .../OpenClaw/PairingAlertSupport.swift | 59 +++++++++++++++++++ .../Sources/OpenClaw/SelectableRow.swift | 40 +++++++++++++ .../OpenClawMacCLI/WizardCommand.swift | 14 ++--- .../OpenClawChatUI/ChatMessageViews.swift | 32 +++++----- .../OpenClawKit/DeviceAuthPayload.swift | 21 +++++++ .../Sources/OpenClawKit/GatewayChannel.swift | 15 ++--- .../Sources/OpenClawKit/LoopbackHost.swift | 4 +- 13 files changed, 196 insertions(+), 161 deletions(-) create mode 100644 apps/macos/Sources/OpenClaw/SelectableRow.swift diff --git a/apps/macos/Sources/OpenClaw/CanvasWindowController+Testing.swift b/apps/macos/Sources/OpenClaw/CanvasWindowController+Testing.swift index c2442d7e17b..ae6551b861c 100644 --- a/apps/macos/Sources/OpenClaw/CanvasWindowController+Testing.swift +++ b/apps/macos/Sources/OpenClaw/CanvasWindowController+Testing.swift @@ -25,22 +25,11 @@ extension CanvasWindowController { } static func _testParseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? { - let parts = host.split(separator: ".", omittingEmptySubsequences: false) - guard parts.count == 4 else { return nil } - let bytes: [UInt8] = parts.compactMap { UInt8($0) } - guard bytes.count == 4 else { return nil } - return (bytes[0], bytes[1], bytes[2], bytes[3]) + LoopbackHost.parseIPv4(host) } static func _testIsLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool { - let (a, b, _, _) = ip - if a == 10 { return true } - if a == 172, (16...31).contains(Int(b)) { return true } - if a == 192, b == 168 { return true } - if a == 127 { return true } - if a == 169, b == 254 { return true } - if a == 100, (64...127).contains(Int(b)) { return true } - return false + LoopbackHost.isLocalNetworkIPv4(ip) } static func _testIsLocalNetworkCanvasURL(_ url: URL) -> Bool { diff --git a/apps/macos/Sources/OpenClaw/DevicePairingApprovalPrompter.swift b/apps/macos/Sources/OpenClaw/DevicePairingApprovalPrompter.swift index ad4c38e8d24..92ca5796337 100644 --- a/apps/macos/Sources/OpenClaw/DevicePairingApprovalPrompter.swift +++ b/apps/macos/Sources/OpenClaw/DevicePairingApprovalPrompter.swift @@ -17,9 +17,7 @@ final class DevicePairingApprovalPrompter { private var queue: [PendingRequest] = [] var pendingCount: Int = 0 var pendingRepairCount: Int = 0 - private var activeAlert: NSAlert? - private var activeRequestId: String? - private var alertHostWindow: NSWindow? + private let alertState = PairingAlertState() private var resolvedByRequestId: Set = [] private struct PairingList: Codable { @@ -78,12 +76,10 @@ final class DevicePairingApprovalPrompter { private func stopPushTask() { PairingAlertSupport.stopPairingPrompter( isStopping: &self.isStopping, - activeAlert: &self.activeAlert, - activeRequestId: &self.activeRequestId, task: &self.task, queue: &self.queue, isPresenting: &self.isPresenting, - alertHostWindow: &self.alertHostWindow) + state: self.alertState) } private func loadPendingRequestsFromGateway() async { @@ -121,20 +117,10 @@ final class DevicePairingApprovalPrompter { requestId: req.requestId, messageText: "Allow device to connect?", informativeText: Self.describe(req), - activeAlert: &self.activeAlert, - activeRequestId: &self.activeRequestId, - alertHostWindow: &self.alertHostWindow, - clearActive: self.clearActiveAlert(hostWindow:), + state: self.alertState, onResponse: self.handleAlertResponse) } - private func clearActiveAlert(hostWindow: NSWindow) { - PairingAlertSupport.clearActivePairingAlert( - activeAlert: &self.activeAlert, - activeRequestId: &self.activeRequestId, - hostWindow: hostWindow) - } - private func handleAlertResponse(_ response: NSApplication.ModalResponse, request: PendingRequest) async { var shouldRemove = response != .alertFirstButtonReturn defer { @@ -194,7 +180,7 @@ final class DevicePairingApprovalPrompter { } private func endActiveAlert() { - PairingAlertSupport.endActiveAlert(activeAlert: &self.activeAlert, activeRequestId: &self.activeRequestId) + PairingAlertSupport.endActiveAlert(state: self.alertState) } private func handle(push: GatewayPush) { @@ -234,7 +220,7 @@ final class DevicePairingApprovalPrompter { let resolution = resolved.decision == PairingAlertSupport.PairingResolution.approved.rawValue ? PairingAlertSupport.PairingResolution.approved : PairingAlertSupport.PairingResolution.rejected - if let activeRequestId, activeRequestId == resolved.requestId { + if let activeRequestId = self.alertState.activeRequestId, activeRequestId == resolved.requestId { self.resolvedByRequestId.insert(resolved.requestId) self.endActiveAlert() let decision = resolution.rawValue diff --git a/apps/macos/Sources/OpenClaw/GatewayDiscoveryMenu.swift b/apps/macos/Sources/OpenClaw/GatewayDiscoveryMenu.swift index babab5866fd..f45e4301abc 100644 --- a/apps/macos/Sources/OpenClaw/GatewayDiscoveryMenu.swift +++ b/apps/macos/Sources/OpenClaw/GatewayDiscoveryMenu.swift @@ -48,27 +48,11 @@ struct GatewayDiscoveryInlineList: View { .truncationMode(.middle) } Spacer(minLength: 0) - if selected { - Image(systemName: "checkmark.circle.fill") - .foregroundStyle(Color.accentColor) - } else { - Image(systemName: "arrow.right.circle") - .foregroundStyle(.secondary) - } + SelectionStateIndicator(selected: selected) } - .padding(.horizontal, 10) - .padding(.vertical, 8) - .frame(maxWidth: .infinity, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(self.rowBackground( - selected: selected, - hovered: self.hoveredGatewayID == gateway.id))) - .overlay( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .strokeBorder( - selected ? Color.accentColor.opacity(0.45) : Color.clear, - lineWidth: 1)) + .openClawSelectableRowChrome( + selected: selected, + hovered: self.hoveredGatewayID == gateway.id) .contentShape(Rectangle()) } .buttonStyle(.plain) @@ -106,12 +90,6 @@ struct GatewayDiscoveryInlineList: View { } } - private func rowBackground(selected: Bool, hovered: Bool) -> Color { - if selected { return Color.accentColor.opacity(0.12) } - if hovered { return Color.secondary.opacity(0.08) } - return Color.clear - } - private func trimmed(_ value: String?) -> String { value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } diff --git a/apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift b/apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift index 0edb2e65122..141b7c43685 100644 --- a/apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift +++ b/apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift @@ -347,21 +347,8 @@ actor GatewayEndpointStore { /// Explicit action: ensure the remote control tunnel is established and publish the resolved endpoint. func ensureRemoteControlTunnel() async throws -> UInt16 { - let mode = await self.deps.mode() - guard mode == .remote else { - throw NSError( - domain: "RemoteTunnel", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "Remote mode is not enabled"]) - } - let root = OpenClawConfigFile.loadDict() - if GatewayRemoteConfig.resolveTransport(root: root) == .direct { - guard let url = GatewayRemoteConfig.resolveGatewayUrl(root: root) else { - throw NSError( - domain: "GatewayEndpoint", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "gateway.remote.url missing or invalid"]) - } + try await self.requireRemoteMode() + if let url = try self.resolveDirectRemoteURL() { guard let port = GatewayRemoteConfig.defaultPort(for: url), let portInt = UInt16(exactly: port) else { @@ -425,22 +412,9 @@ actor GatewayEndpointStore { } private func ensureRemoteConfig(detail: String) async throws -> GatewayConnection.Config { - let mode = await self.deps.mode() - guard mode == .remote else { - throw NSError( - domain: "RemoteTunnel", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "Remote mode is not enabled"]) - } + try await self.requireRemoteMode() - let root = OpenClawConfigFile.loadDict() - if GatewayRemoteConfig.resolveTransport(root: root) == .direct { - guard let url = GatewayRemoteConfig.resolveGatewayUrl(root: root) else { - throw NSError( - domain: "GatewayEndpoint", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "gateway.remote.url missing or invalid"]) - } + if let url = try self.resolveDirectRemoteURL() { let token = self.deps.token() let password = self.deps.password() self.cancelRemoteEnsure() @@ -491,6 +465,27 @@ actor GatewayEndpointStore { } } + private func requireRemoteMode() async throws { + guard await self.deps.mode() == .remote else { + throw NSError( + domain: "RemoteTunnel", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Remote mode is not enabled"]) + } + } + + private func resolveDirectRemoteURL() throws -> URL? { + let root = OpenClawConfigFile.loadDict() + guard GatewayRemoteConfig.resolveTransport(root: root) == .direct else { return nil } + guard let url = GatewayRemoteConfig.resolveGatewayUrl(root: root) else { + throw NSError( + domain: "GatewayEndpoint", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "gateway.remote.url missing or invalid"]) + } + return url + } + private func removeSubscriber(_ id: UUID) { self.subscribers[id] = nil } diff --git a/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift b/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift index e20aa8d53bb..bd27e49626b 100644 --- a/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift +++ b/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift @@ -32,9 +32,7 @@ final class NodePairingApprovalPrompter { private var queue: [PendingRequest] = [] var pendingCount: Int = 0 var pendingRepairCount: Int = 0 - private var activeAlert: NSAlert? - private var activeRequestId: String? - private var alertHostWindow: NSWindow? + private let alertState = PairingAlertState() private var remoteResolutionsByRequestId: [String: PairingResolution] = [:] private var autoApproveAttempts: Set = [] @@ -99,12 +97,10 @@ final class NodePairingApprovalPrompter { private func stopPushTask() { PairingAlertSupport.stopPairingPrompter( isStopping: &self.isStopping, - activeAlert: &self.activeAlert, - activeRequestId: &self.activeRequestId, task: &self.task, queue: &self.queue, isPresenting: &self.isPresenting, - alertHostWindow: &self.alertHostWindow) + state: self.alertState) } private func loadPendingRequestsFromGateway() async { @@ -180,7 +176,7 @@ final class NodePairingApprovalPrompter { if pendingById[req.requestId] != nil { continue } let resolution = self.inferResolution(for: req, list: list) - if self.activeRequestId == req.requestId, self.activeAlert != nil { + if self.alertState.activeRequestId == req.requestId, self.alertState.activeAlert != nil { self.remoteResolutionsByRequestId[req.requestId] = resolution self.logger.info( """ @@ -222,7 +218,7 @@ final class NodePairingApprovalPrompter { } private func endActiveAlert() { - PairingAlertSupport.endActiveAlert(activeAlert: &self.activeAlert, activeRequestId: &self.activeRequestId) + PairingAlertSupport.endActiveAlert(state: self.alertState) } private func handle(push: GatewayPush) { @@ -284,20 +280,10 @@ final class NodePairingApprovalPrompter { requestId: req.requestId, messageText: "Allow node to connect?", informativeText: Self.describe(req), - activeAlert: &self.activeAlert, - activeRequestId: &self.activeRequestId, - alertHostWindow: &self.alertHostWindow, - clearActive: self.clearActiveAlert(hostWindow:), + state: self.alertState, onResponse: self.handleAlertResponse) } - private func clearActiveAlert(hostWindow: NSWindow) { - PairingAlertSupport.clearActivePairingAlert( - activeAlert: &self.activeAlert, - activeRequestId: &self.activeRequestId, - hostWindow: hostWindow) - } - private func handleAlertResponse(_ response: NSApplication.ModalResponse, request: PendingRequest) async { defer { if self.queue.first == request { @@ -575,7 +561,7 @@ final class NodePairingApprovalPrompter { let resolution: PairingResolution = resolved.decision == PairingResolution.approved.rawValue ? .approved : .rejected - if self.activeRequestId == resolved.requestId, self.activeAlert != nil { + if self.alertState.activeRequestId == resolved.requestId, self.alertState.activeAlert != nil { self.remoteResolutionsByRequestId[resolved.requestId] = resolution self.logger.info( """ diff --git a/apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift b/apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift index 4f942dfe8a4..7edd422be03 100644 --- a/apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift +++ b/apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift @@ -311,29 +311,13 @@ extension OnboardingView { .font(.caption.monospaced()) .foregroundStyle(.secondary) .lineLimit(1) - .truncationMode(.middle) + .truncationMode(.middle) } } Spacer(minLength: 0) - if selected { - Image(systemName: "checkmark.circle.fill") - .foregroundStyle(Color.accentColor) - } else { - Image(systemName: "arrow.right.circle") - .foregroundStyle(.secondary) - } + SelectionStateIndicator(selected: selected) } - .padding(.horizontal, 10) - .padding(.vertical, 8) - .frame(maxWidth: .infinity, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(selected ? Color.accentColor.opacity(0.12) : Color.clear)) - .overlay( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .strokeBorder( - selected ? Color.accentColor.opacity(0.45) : Color.clear, - lineWidth: 1)) + .openClawSelectableRowChrome(selected: selected) } .buttonStyle(.plain) } diff --git a/apps/macos/Sources/OpenClaw/PairingAlertSupport.swift b/apps/macos/Sources/OpenClaw/PairingAlertSupport.swift index 024cec43d5b..84f0da698e3 100644 --- a/apps/macos/Sources/OpenClaw/PairingAlertSupport.swift +++ b/apps/macos/Sources/OpenClaw/PairingAlertSupport.swift @@ -12,6 +12,13 @@ final class PairingAlertHostWindow: NSWindow { } } +@MainActor +final class PairingAlertState { + var activeAlert: NSAlert? + var activeRequestId: String? + var alertHostWindow: NSWindow? +} + @MainActor enum PairingAlertSupport { enum PairingResolution: String { @@ -34,6 +41,10 @@ enum PairingAlertSupport { activeRequestId = nil } + static func endActiveAlert(state: PairingAlertState) { + self.endActiveAlert(activeAlert: &state.activeAlert, activeRequestId: &state.activeRequestId) + } + static func requireAlertHostWindow(alertHostWindow: inout NSWindow?) -> NSWindow { if let alertHostWindow { return alertHostWindow @@ -179,6 +190,30 @@ enum PairingAlertSupport { } } + static func presentPairingAlert( + request: Request, + requestId: String, + messageText: String, + informativeText: String, + state: PairingAlertState, + onResponse: @escaping @MainActor (NSApplication.ModalResponse, Request) async -> Void) + { + self.presentPairingAlert( + request: request, + requestId: requestId, + messageText: messageText, + informativeText: informativeText, + activeAlert: &state.activeAlert, + activeRequestId: &state.activeRequestId, + alertHostWindow: &state.alertHostWindow) + { response, hostWindow in + Task { @MainActor in + self.clearActivePairingAlert(state: state, hostWindow: hostWindow) + await onResponse(response, request) + } + } + } + static func clearActivePairingAlert( activeAlert: inout NSAlert?, activeRequestId: inout String?, @@ -189,6 +224,13 @@ enum PairingAlertSupport { hostWindow.orderOut(nil) } + static func clearActivePairingAlert(state: PairingAlertState, hostWindow: NSWindow) { + self.clearActivePairingAlert( + activeAlert: &state.activeAlert, + activeRequestId: &state.activeRequestId, + hostWindow: hostWindow) + } + static func stopPairingPrompter( isStopping: inout Bool, activeAlert: inout NSAlert?, @@ -210,6 +252,23 @@ enum PairingAlertSupport { alertHostWindow = nil } + static func stopPairingPrompter( + isStopping: inout Bool, + task: inout Task?, + queue: inout [Request], + isPresenting: inout Bool, + state: PairingAlertState) + { + self.stopPairingPrompter( + isStopping: &isStopping, + activeAlert: &state.activeAlert, + activeRequestId: &state.activeRequestId, + task: &task, + queue: &queue, + isPresenting: &isPresenting, + alertHostWindow: &state.alertHostWindow) + } + static func approveRequest( requestId: String, kind: String, diff --git a/apps/macos/Sources/OpenClaw/SelectableRow.swift b/apps/macos/Sources/OpenClaw/SelectableRow.swift new file mode 100644 index 00000000000..e37a741aa08 --- /dev/null +++ b/apps/macos/Sources/OpenClaw/SelectableRow.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct SelectionStateIndicator: View { + let selected: Bool + + var body: some View { + Group { + if self.selected { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(Color.accentColor) + } else { + Image(systemName: "arrow.right.circle") + .foregroundStyle(.secondary) + } + } + } +} + +extension View { + func openClawSelectableRowChrome(selected: Bool, hovered: Bool = false) -> some View { + self + .padding(.horizontal, 10) + .padding(.vertical, 8) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(self.openClawRowBackground(selected: selected, hovered: hovered))) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .strokeBorder( + selected ? Color.accentColor.opacity(0.45) : Color.clear, + lineWidth: 1)) + } + + private func openClawRowBackground(selected: Bool, hovered: Bool) -> Color { + if selected { return Color.accentColor.opacity(0.12) } + if hovered { return Color.secondary.opacity(0.08) } + return Color.clear + } +} diff --git a/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift b/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift index ea9ff79ffa5..26ccdb0e0a6 100644 --- a/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift +++ b/apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift @@ -285,16 +285,12 @@ actor GatewayWizardClient { nonce: connectNonce, platform: platform, deviceFamily: "Mac") - if let signature = DeviceIdentityStore.signPayload(payload, identity: identity), - let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity) + if let device = GatewayDeviceAuthPayload.signedDeviceDictionary( + payload: payload, + identity: identity, + signedAtMs: signedAtMs, + nonce: connectNonce) { - let device: [String: ProtoAnyCodable] = [ - "id": ProtoAnyCodable(identity.deviceId), - "publicKey": ProtoAnyCodable(publicKey), - "signature": ProtoAnyCodable(signature), - "signedAt": ProtoAnyCodable(signedAtMs), - "nonce": ProtoAnyCodable(connectNonce), - ] params["device"] = ProtoAnyCodable(device) } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift index 8bd230e7b7c..08ae3ff2914 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift @@ -70,13 +70,7 @@ private struct ChatBubbleShape: InsettableShape { to: baseBottom, control1: CGPoint(x: bubbleMaxX + self.tailWidth * 0.95, y: midY + baseH * 0.15), control2: CGPoint(x: bubbleMaxX + self.tailWidth * 0.2, y: baseBottomY - baseH * 0.05)) - path.addQuadCurve( - to: CGPoint(x: bubbleMaxX - r, y: bubbleMaxY), - control: CGPoint(x: bubbleMaxX, y: bubbleMaxY)) - path.addLine(to: CGPoint(x: bubbleMinX + r, y: bubbleMaxY)) - path.addQuadCurve( - to: CGPoint(x: bubbleMinX, y: bubbleMaxY - r), - control: CGPoint(x: bubbleMinX, y: bubbleMaxY)) + self.addBottomEdge(path: &path, bubbleMinX: bubbleMinX, bubbleMaxX: bubbleMaxX, bubbleMaxY: bubbleMaxY, radius: r) path.addLine(to: CGPoint(x: bubbleMinX, y: bubbleMinY + r)) path.addQuadCurve( to: CGPoint(x: bubbleMinX + r, y: bubbleMinY), @@ -108,13 +102,7 @@ private struct ChatBubbleShape: InsettableShape { to: CGPoint(x: bubbleMaxX, y: bubbleMinY + r), control: CGPoint(x: bubbleMaxX, y: bubbleMinY)) path.addLine(to: CGPoint(x: bubbleMaxX, y: bubbleMaxY - r)) - path.addQuadCurve( - to: CGPoint(x: bubbleMaxX - r, y: bubbleMaxY), - control: CGPoint(x: bubbleMaxX, y: bubbleMaxY)) - path.addLine(to: CGPoint(x: bubbleMinX + r, y: bubbleMaxY)) - path.addQuadCurve( - to: CGPoint(x: bubbleMinX, y: bubbleMaxY - r), - control: CGPoint(x: bubbleMinX, y: bubbleMaxY)) + self.addBottomEdge(path: &path, bubbleMinX: bubbleMinX, bubbleMaxX: bubbleMaxX, bubbleMaxY: bubbleMaxY, radius: r) path.addLine(to: baseBottom) path.addCurve( to: tip, @@ -131,6 +119,22 @@ private struct ChatBubbleShape: InsettableShape { return path } + + private func addBottomEdge( + path: inout Path, + bubbleMinX: CGFloat, + bubbleMaxX: CGFloat, + bubbleMaxY: CGFloat, + radius: CGFloat) + { + path.addQuadCurve( + to: CGPoint(x: bubbleMaxX - radius, y: bubbleMaxY), + control: CGPoint(x: bubbleMaxX, y: bubbleMaxY)) + path.addLine(to: CGPoint(x: bubbleMinX + radius, y: bubbleMaxY)) + path.addQuadCurve( + to: CGPoint(x: bubbleMinX, y: bubbleMaxY - radius), + control: CGPoint(x: bubbleMinX, y: bubbleMaxY)) + } } @MainActor diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift index 858ef457c7e..9b8e4c2673b 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift @@ -1,4 +1,5 @@ import Foundation +import OpenClawProtocol public enum GatewayDeviceAuthPayload { public static func buildV3( @@ -52,4 +53,24 @@ public enum GatewayDeviceAuthPayload { } return output } + + public static func signedDeviceDictionary( + payload: String, + identity: DeviceIdentity, + signedAtMs: Int, + nonce: String) -> [String: OpenClawProtocol.AnyCodable]? + { + guard let signature = DeviceIdentityStore.signPayload(payload, identity: identity), + let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity) + else { + return nil + } + return [ + "id": OpenClawProtocol.AnyCodable(identity.deviceId), + "publicKey": OpenClawProtocol.AnyCodable(publicKey), + "signature": OpenClawProtocol.AnyCodable(signature), + "signedAt": OpenClawProtocol.AnyCodable(signedAtMs), + "nonce": OpenClawProtocol.AnyCodable(nonce), + ] + } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift index c67559a22bf..3dc5eacee6e 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift @@ -406,15 +406,12 @@ public actor GatewayChannelActor { nonce: connectNonce, platform: platform, deviceFamily: InstanceIdentity.deviceFamily) - if let signature = DeviceIdentityStore.signPayload(payload, identity: identity), - let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity) { - let device: [String: ProtoAnyCodable] = [ - "id": ProtoAnyCodable(identity.deviceId), - "publicKey": ProtoAnyCodable(publicKey), - "signature": ProtoAnyCodable(signature), - "signedAt": ProtoAnyCodable(signedAtMs), - "nonce": ProtoAnyCodable(connectNonce), - ] + if let device = GatewayDeviceAuthPayload.signedDeviceDictionary( + payload: payload, + identity: identity, + signedAtMs: signedAtMs, + nonce: connectNonce) + { params["device"] = ProtoAnyCodable(device) } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/LoopbackHost.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/LoopbackHost.swift index 265e3123303..b090549800a 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/LoopbackHost.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/LoopbackHost.swift @@ -53,7 +53,7 @@ public enum LoopbackHost { return self.isLocalNetworkIPv4(ipv4) } - private static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? { + static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? { let parts = host.split(separator: ".", omittingEmptySubsequences: false) guard parts.count == 4 else { return nil } let bytes: [UInt8] = parts.compactMap { UInt8($0) } @@ -61,7 +61,7 @@ public enum LoopbackHost { return (bytes[0], bytes[1], bytes[2], bytes[3]) } - private static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool { + static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool { let (a, b, _, _) = ip // 10.0.0.0/8 if a == 10 { return true }