add astra draft

This commit is contained in:
2026-09-17 17:59:34 +02:00
parent 88588dcfcc
commit 41458afc76
38 changed files with 2392 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
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<T: Decodable>(_ 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)
}
}