import Foundation public struct SessionMetadata: Equatable, Sendable { public let instanceID: UUID public let authenticated: Bool public let checkedAt: Date public init(instanceID: UUID, authenticated: Bool, checkedAt: Date) { self.instanceID = instanceID self.authenticated = authenticated self.checkedAt = checkedAt } } public struct InstanceSessionService: Sendable { private let store: InstanceSessionStore private let client: any SimAdminClientProtocol public init(store: InstanceSessionStore, client: any SimAdminClientProtocol) { self.store = store self.client = client } /// Logs in directly and stores the session cookie. /// /// Only meaningful in direct mode: a relay instance has no origin to dial and no cookie to keep, /// because the node's agent holds that session itself. Passing one here returns the explicit /// unauthenticated state rather than dialing something arbitrary. public func login(_ instance: Instance, password: String) async throws -> SessionMetadata { guard let origin = instance.endpoint.directOrigin else { return SessionMetadata(instanceID: instance.id, authenticated: false, checkedAt: Date()) } do { let response = try await client.send( .authLogin(password: password), baseURL: origin, cookie: nil ) if let cookie = Self.sessionCookie(response.setCookie) { await store.set(instance.id, origin: origin, cookie: cookie) return SessionMetadata(instanceID: instance.id, authenticated: true, checkedAt: Date()) } } catch { // Rejected credentials fall through to the same explicit unauthenticated state. } await store.clear(instance.id) return SessionMetadata(instanceID: instance.id, authenticated: false, checkedAt: Date()) } public func logout(_ instance: Instance) async throws -> SessionMetadata { guard let origin = instance.endpoint.directOrigin else { return SessionMetadata(instanceID: instance.id, authenticated: false, checkedAt: Date()) } if let cookie = await store.cookie(for: instance.id, origin: origin) { _ = try? await client.send(.authLogout, baseURL: origin, cookie: cookie) } await store.clear(instance.id) return SessionMetadata(instanceID: instance.id, authenticated: false, checkedAt: Date()) } public func cookie(for instance: Instance) async -> String? { guard let origin = instance.endpoint.directOrigin else { return nil } return await store.cookie(for: instance.id, origin: origin) } static func sessionCookie(_ header: String?) -> String? { guard let header, let range = header.range(of: "simadmin_session=([^;\\s,]+)", options: .regularExpression) else { return nil } return String(header[range]) } }