import ByreAPI import Foundation import HTTPTypes import OpenAPIRuntime import OpenAPIURLSession import os let log = Logger(subsystem: "run.fc.byre.mac", category: "byre") typealias User = Components.Schemas.User typealias Project = Components.Schemas.Project typealias ByreTask = Components.Schemas.Task typealias AgentSession = Components.Schemas.AgentSession typealias InboxItem = Components.Schemas.InboxItem typealias ByreEvent = Components.Schemas.Event typealias EventPage = Components.Schemas.EventPage typealias Focus = Components.Schemas.Focus typealias TaskTab = Components.Schemas.TaskTab typealias Snapshot = Components.Schemas.Snapshot typealias ServerStatus = Components.Schemas.Status struct ApiError: Error, CustomStringConvertible { let status: Int let message: String var description: String { message } } struct ByreDates: DateTranscoder { private static let style = Date.ISO8601FormatStyle(includingFractionalSeconds: true) func encode(_ date: Date) throws -> String { date.formatted(Self.style) } func decode(_ string: String) throws -> Date { try Self.style.parse(string) } } func jsonDecoder() -> JSONDecoder { let d = JSONDecoder() d.dateDecodingStrategy = .custom { decoder in try ByreDates().decode(try decoder.singleValueContainer().decode(String.self)) } return d } func decodePayload(_ payload: OpenAPIObjectContainer, as type: T.Type) throws -> T { try jsonDecoder().decode(T.self, from: try JSONEncoder().encode(payload)) } private struct ErrorMiddleware: ClientMiddleware { func intercept( _ request: HTTPRequest, body: HTTPBody?, baseURL: URL, operationID: String, next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) ) async throws -> (HTTPResponse, HTTPBody?) { let (response, responseBody) = try await next(request, body, baseURL) let code = response.status.code if code < 400 { return (response, responseBody) } var data = Data() if let responseBody { data = try await Data(collecting: responseBody, upTo: 1 << 20) } if response.headerFields[.contentType]?.hasPrefix("application/json") == true { throw ApiError(status: code, message: try JSONDecoder().decode(Components.Schemas._Error.self, from: data).message) } let text = String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) throw ApiError(status: code, message: "\(code) \(HTTPURLResponse.localizedString(forStatusCode: code))\(text.isEmpty ? "" : ": \(text)")") } } func rootError(_ error: any Error) -> any Error { if let e = error as? ClientError { return rootError(e.underlyingError) } return error } func errorText(_ error: any Error) -> String { let e = rootError(error) if let api = e as? ApiError { return api.message } if let url = e as? URLError { return url.localizedDescription } return String(describing: e) } private let clientID = UUID().uuidString final class API: Sendable { let origin: URL let session: URLSession let client: Client init(origin: URL) { self.origin = origin let config = URLSessionConfiguration.default config.httpAdditionalHeaders = [ "Byre-Client": clientID, "Byre-Commit": Bundle.main.object(forInfoDictionaryKey: "ByreCommit") as! String, "Byre-Host": Host.current().localizedName ?? ProcessInfo.processInfo.hostName, ] session = URLSession(configuration: config) client = Client( serverURL: origin, configuration: Configuration(dateTranscoder: ByreDates()), transport: URLSessionTransport(configuration: .init(session: session)), middlewares: [ErrorMiddleware()] ) } func url(_ path: String, _ query: [URLQueryItem] = [], scheme: String = "https") -> URL { var c = URLComponents(url: origin, resolvingAgainstBaseURL: false)! c.scheme = scheme c.path = path if !query.isEmpty { c.queryItems = query } return c.url! } } func parseOrigin(_ text: String) -> URL? { guard let c = URLComponents(string: text.trimmingCharacters(in: .whitespaces)), c.scheme?.lowercased() == "https", let host = c.host, !host.isEmpty, c.path.isEmpty || c.path == "/", c.query == nil, c.fragment == nil, c.user == nil else { return nil } var origin = URLComponents() origin.scheme = "https" origin.host = host.lowercased() origin.port = c.port return origin.url }