import Foundation import OpenClawKit /// Single source of truth for "how we connect" to the current gateway. /// /// The iOS app maintains two WebSocket sessions to the same gateway: /// - a `role=node` session for device capabilities (`node.invoke.*`) /// - a `role=operator` session for chat/talk/config (`chat.*`, `talk.*`, etc.) /// /// Both sessions should derive all connection inputs from this config so we /// don't accidentally persist gateway-scoped state under different keys. struct GatewayConnectConfig { let url: URL let stableID: String let tls: GatewayTLSParams? let token: String? let bootstrapToken: String? let password: String? let nodeOptions: GatewayConnectOptions /// Stable, non-empty identifier used for gateway-scoped persistence keys. /// If the caller doesn't provide a stableID, fall back to URL identity. var effectiveStableID: String { let trimmed = self.stableID.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return self.url.absoluteString } return trimmed } func hasSameConnectionInputs(as other: GatewayConnectConfig) -> Bool { self.url == other.url && self.stableID == other.stableID && Self.sameTLS(self.tls, other.tls) && self.token == other.token && self.bootstrapToken == other.bootstrapToken && self.password == other.password && Self.sameOptions(self.nodeOptions, other.nodeOptions) } private static func sameTLS(_ lhs: GatewayTLSParams?, _ rhs: GatewayTLSParams?) -> Bool { switch (lhs, rhs) { case (nil, nil): true case let (lhs?, rhs?): lhs.required == rhs.required && lhs.expectedFingerprint == rhs.expectedFingerprint && lhs.allowTOFU == rhs.allowTOFU && lhs.storeKey == rhs.storeKey default: false } } private static func sameOptions(_ lhs: GatewayConnectOptions, _ rhs: GatewayConnectOptions) -> Bool { let lhsScopes = Self.normalizedValues(lhs.scopes) let rhsScopes = Self.normalizedValues(rhs.scopes) let lhsCaps = Self.normalizedValues(lhs.caps) let rhsCaps = Self.normalizedValues(rhs.caps) let lhsCommands = Self.normalizedValues(lhs.commands) let rhsCommands = Self.normalizedValues(rhs.commands) return lhs.role == rhs.role && lhs.scopesAreExplicit == rhs.scopesAreExplicit && lhs.clientId == rhs.clientId && lhs.clientMode == rhs.clientMode && lhs.clientDisplayName == rhs.clientDisplayName && lhs.includeDeviceIdentity == rhs.includeDeviceIdentity && lhsScopes == rhsScopes && lhsCaps == rhsCaps && lhsCommands == rhsCommands && lhs.permissions == rhs.permissions } private static func normalizedValues(_ values: [String]) -> [String] { values.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } .sorted() } }