// // RequesterOAuthBridge.swift // AT-IRC // // The embedded socialweb requester signs in with its own OAuth client (the // MarketIdentity). It redirects to a loopback URI; this bridge keeps one // loopback listener for the app's lifetime and forwards each callback to the // in-process requester (CompleteOAuth), like the page's // /oauth/callback/requester route. // import Foundation import OSLog @MainActor final class RequesterOAuthBridge { private var server: LoopbackServer? private var task: Task? private let kcp = KCPClient() private let log = Logger(subsystem: "com.fedfork.irc.AT-IRC", category: "market-oauth") /// Start the redirect listener and return the redirect URI for the /// requester config. Idempotent. func start() async throws -> String { if let server, server.port != 0 { return "http://127.0.0.1:\(server.port)/" } let server = LoopbackServer() let port = try await server.start() self.server = server task?.cancel() task = Task { @MainActor [weak self] in while !Task.isCancelled { guard let self, let server = self.server else { return } guard let query = try? await server.waitForCallback() else { return } server.resetCallback() log.notice("callback received keys=\(query.keys.sorted().joined(separator: ","), privacy: .public)") var components = URLComponents() components.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) } let path = "/oauth/callback/requester?" + (components.percentEncodedQuery ?? "") do { // This route belongs to the requester, not the // apiserver: do not send X-Atirc-Direct. let response = try await self.kcp.request("GET", path, direct: false) log.notice("callback forwarded status=\(response.status, privacy: .public)") } catch { log.error("callback forward failed: \(error.localizedDescription, privacy: .public)") } } } return "http://127.0.0.1:\(port)/" } func stop() { task?.cancel() task = nil server?.stop() server = nil } }