import Foundation import PolicyCore struct Credentials: Codable { var server: String var token: String var deviceId: String var childId: String } struct PairResponse: Decodable { let deviceId: String; let childId: String; let token: String } struct Acknowledgement: Decodable { let received: Bool } struct ConfigurationGrant: Decodable { let authorized: Bool } struct MathChallenge: Decodable, Identifiable { struct Question: Decodable, Identifiable { let id: String; let prompt: String } let id: String let expiresAt: Double let questions: [Question] } struct SubmittedAnswer: Encodable { let questionId: String; let answer: Int } struct Submission: Encodable { let answers: [SubmittedAnswer] } struct ChallengeResult: Decodable { let passed: Bool let correctCount: Int let requiredCount: Int let awardedMinutes: Int } private struct APIError: Decodable { let detail: String } private final class NoRedirects: NSObject, URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) } } struct APIClient { let origin: URL let token: String? init(server: String, token: String? = nil) throws { guard let parts = URLComponents(string: server.trimmingCharacters(in: .whitespacesAndNewlines)), let host = parts.host, parts.user == nil, parts.password == nil, parts.query == nil, parts.fragment == nil, ["", "/"].contains(parts.path), let url = parts.url else { throw LocalError.message("Enter the server origin, without a path") } var allowed = parts.scheme == "https" #if DEBUG allowed = allowed || (parts.scheme == "http" && ["localhost", "127.0.0.1", "::1"].contains(host)) #endif guard allowed else { throw LocalError.message("Use HTTPS with a trusted certificate; plain LAN HTTP is not supported") } origin = url self.token = token } func request(_ path: String, method: String = "GET", body: Data? = nil) async throws -> T { guard let url = URL(string: path, relativeTo: origin)?.absoluteURL, url.host == origin.host else { throw LocalError.message("Invalid API path") } var request = URLRequest(url: url) request.httpMethod = method; request.httpBody = body; request.timeoutInterval = 20 request.setValue("application/json", forHTTPHeaderField: "Content-Type") if let token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } let config = URLSessionConfiguration.ephemeral config.httpShouldSetCookies = false; config.urlCache = nil let session = URLSession(configuration: config, delegate: NoRedirects(), delegateQueue: nil) defer { session.finishTasksAndInvalidate() } let (data, response) = try await session.data(for: request) guard let http = response as? HTTPURLResponse else { throw LocalError.message("Invalid server response") } guard (200...299).contains(http.statusCode) else { let detail = (try? JSONDecoder().decode(APIError.self, from: data).detail) ?? "HTTP \(http.statusCode)" throw LocalError.message(detail) } return try WireCoding.decoder().decode(T.self, from: data) } }