import Foundation struct BackendClient { let baseURL: URL func register(username: String, password: String, displayName: String) async throws -> AuthResponse { try await post( path: "/api/auth/register", body: RegisterRequest( username: username, password: password, displayName: displayName.isEmpty ? nil : displayName ), token: nil ) } func login(username: String, password: String) async throws -> AuthResponse { try await post( path: "/api/auth/login", body: LoginRequest(username: username, password: password), token: nil ) } func restoreSession(token: String) async throws -> SessionResponse { try await get(path: "/api/auth/session", token: token) } func logout(token: String) async throws { let _: EmptyResponse = try await post(path: "/api/auth/logout", body: EmptyBody(), token: token) } func listAccessKeys(token: String) async throws -> [AccessKeySummary] { let response: AccessKeyListResponse = try await get(path: "/api/webauthn/credentials", token: token) return response.credentials } func startAccessKeyRegistration(label: String?, token: String) async throws -> RegistrationOptionsResponse { try await post( path: "/api/webauthn/register/options", body: RegisterAccessKeyRequest(label: label?.isEmpty == false ? label : nil), token: token ) } func finishAccessKeyRegistration(payload: PasskeyRegistrationPayload, token: String) async throws { let _: EmptyResponse = try await post( path: "/api/webauthn/register/verify", body: VerifyRegistrationRequest(credential: payload), token: token ) } func startAccessKeyAuthentication() async throws -> AuthenticationOptionsResponse { try await post(path: "/api/webauthn/authenticate/options", body: EmptyBody(), token: nil) } func finishAccessKeyAuthentication( attemptId: String, payload: PasskeyAuthenticationPayload ) async throws -> AuthResponse { try await post( path: "/api/webauthn/authenticate/verify", body: VerifyAuthenticationRequest(attemptId: attemptId, credential: payload), token: nil ) } private func get(path: String, token: String?) async throws -> Response { var request = URLRequest(url: baseURL.appending(path: path)) request.httpMethod = "GET" request.setValue("application/json", forHTTPHeaderField: "Accept") if let token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } return try await perform(request) } private func post( path: String, body: Body, token: String? ) async throws -> Response { var request = URLRequest(url: baseURL.appending(path: path)) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") if let token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } request.httpBody = try JSONEncoder().encode(body) return try await perform(request) } private func perform(_ request: URLRequest) async throws -> Response { let (data, rawResponse) = try await URLSession.shared.data(for: request) let response = rawResponse as? HTTPURLResponse guard let response else { throw APIErrorResponse(message: "The backend did not return a valid HTTP response.") } let decoder = JSONDecoder() if (200 ..< 300).contains(response.statusCode) { if Response.self == EmptyResponse.self { return EmptyResponse() as! Response } return try decoder.decode(Response.self, from: data) } if let apiError = try? decoder.decode(APIErrorResponse.self, from: data) { throw apiError } throw APIErrorResponse(message: "The backend returned HTTP \(response.statusCode).") } } private struct EmptyBody: Encodable {} private struct EmptyResponse: Decodable {} private struct RegisterRequest: Encodable { let username: String let password: String let displayName: String? } private struct LoginRequest: Encodable { let username: String let password: String } private struct RegisterAccessKeyRequest: Encodable { let label: String? } private struct VerifyRegistrationRequest: Encodable { let credential: PasskeyRegistrationPayload } private struct VerifyAuthenticationRequest: Encodable { let attemptId: String let credential: PasskeyAuthenticationPayload }