import Foundation import CryptoKit #if canImport(UIKit) import UIKit #endif #if canImport(CoreLocation) import CoreLocation #endif #if canImport(Security) import Security #endif protocol IDPServicing { func bootstrap() async throws -> BootstrapContext func signIn(with request: PairingAuthenticationRequest) async throws -> SignInResult func identify(with request: PairingAuthenticationRequest) async throws -> DashboardSnapshot func refreshDashboard() async throws -> DashboardSnapshot func approveRequest(id: UUID) async throws -> DashboardSnapshot func rejectRequest(id: UUID) async throws -> DashboardSnapshot func simulateIncomingRequest() async throws -> DashboardSnapshot func markNotificationRead(id: UUID) async throws -> DashboardSnapshot } enum DefaultIDPService { static let shared: IDPServicing = LiveIDPService.shared } actor LiveIDPService: IDPServicing { static let shared = LiveIDPService() private let appStateStore: AppStateStoring private let keyStore: PassportKeyStoring init( appStateStore: AppStateStoring = UserDefaultsAppStateStore(), keyStore: PassportKeyStoring = DefaultPassportKeyStore() ) { self.appStateStore = appStateStore self.keyStore = keyStore } func bootstrap() async throws -> BootstrapContext { let suggestedPayload = appStateStore.load()?.session.pairingCode ?? "" return BootstrapContext(suggestedPairingPayload: suggestedPayload) } func signIn(with request: PairingAuthenticationRequest) async throws -> SignInResult { try validateSignedGPSPosition(in: request) let context = try PairingPayloadParser.parse(request.pairingPayload) let baseURL = try serverURL(from: context) let client = LiveTypedRequestClient(baseURL: baseURL) let keyMaterial = try loadOrCreateKeyMaterial(for: context.originHost) let signingPayload = try buildEnrollmentSigningPayload(from: context) let signatureBase64 = try signPayload(signingPayload, with: keyMaterial.privateKeyData) let enrollmentResponse = try await client.fire( method: "completePassportEnrollment", request: CompletePassportEnrollmentRequest( pairingToken: context.pairingToken, deviceLabel: DeviceEnvironment.currentDeviceLabel, platform: DeviceEnvironment.currentPlatform, publicKeyX963Base64: keyMaterial.publicKeyBase64, signatureBase64: signatureBase64, signatureFormat: "der", appVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, capabilities: DeviceEnvironment.capabilities ), responseType: CompletePassportEnrollmentResponse.self ) let session = AuthSession( deviceName: enrollmentResponse.device.data.label, originHost: context.originHost, serverURL: baseURL.absoluteString, passportDeviceID: enrollmentResponse.device.id, pairedAt: .now, tokenPreview: context.tokenPreview, pairingCode: request.pairingPayload, pairingTransport: request.transport, signedGPSPosition: request.signedGPSPosition ) let snapshot = try await dashboardSnapshot(for: session) return SignInResult(session: session, snapshot: snapshot) } func identify(with request: PairingAuthenticationRequest) async throws -> DashboardSnapshot { guard let state = appStateStore.load() else { throw AppError.invalidPairingPayload } _ = request return try await dashboardSnapshot(for: state.session) } func refreshDashboard() async throws -> DashboardSnapshot { guard let state = appStateStore.load() else { throw AppError.invalidPairingPayload } return try await dashboardSnapshot(for: state.session) } func approveRequest(id: UUID) async throws -> DashboardSnapshot { guard let state = appStateStore.load(), let request = state.requests.first(where: { $0.id == id }) else { throw AppError.requestNotFound } guard let signingPayload = request.signingPayload else { throw AppError.requestNotFound } let client = LiveTypedRequestClient(baseURL: URL(string: state.session.serverURL)!) let signatureBase64 = try signPayload(signingPayload, with: try privateKeyData(for: state.session)) let locationEvidence = request.requiresLocation ? try await CurrentLocationEvidenceProvider.currentLocationEvidenceIfAvailable() : nil _ = try await client.fire( method: "approvePassportChallenge", request: ApprovePassportChallengeRequest( challengeId: request.serverID, deviceId: state.session.passportDeviceID, signatureBase64: signatureBase64, signatureFormat: "der", location: locationEvidence, nfc: nil ), responseType: ApprovePassportChallengeResponse.self ) return try await dashboardSnapshot(for: state.session) } func rejectRequest(id: UUID) async throws -> DashboardSnapshot { guard let state = appStateStore.load(), let request = state.requests.first(where: { $0.id == id }) else { throw AppError.requestNotFound } let client = LiveTypedRequestClient(baseURL: URL(string: state.session.serverURL)!) let signedRequest = try signedDeviceRequest( session: state.session, action: "rejectPassportChallenge", signedFields: ["challenge_id=\(request.serverID)"] ) _ = try await client.fire( method: "rejectPassportChallenge", request: RejectPassportChallengeRequest( deviceId: signedRequest.deviceId, timestamp: signedRequest.timestamp, nonce: signedRequest.nonce, signatureBase64: signedRequest.signatureBase64, signatureFormat: signedRequest.signatureFormat, challengeId: request.serverID ), responseType: RejectPassportChallengeResponse.self ) return try await dashboardSnapshot(for: state.session) } func simulateIncomingRequest() async throws -> DashboardSnapshot { try await refreshDashboard() } func markNotificationRead(id: UUID) async throws -> DashboardSnapshot { guard let state = appStateStore.load(), let notification = state.notifications.first(where: { $0.id == id }) else { return try await refreshDashboard() } let client = LiveTypedRequestClient(baseURL: URL(string: state.session.serverURL)!) let signedRequest = try signedDeviceRequest( session: state.session, action: "markPassportAlertSeen", signedFields: ["hint_id=\(notification.hintID)"] ) _ = try await client.fire( method: "markPassportAlertSeen", request: MarkPassportAlertSeenRequest( deviceId: signedRequest.deviceId, timestamp: signedRequest.timestamp, nonce: signedRequest.nonce, signatureBase64: signedRequest.signatureBase64, signatureFormat: signedRequest.signatureFormat, hintId: notification.hintID ), responseType: SimpleSuccessResponse.self ) return try await dashboardSnapshot(for: state.session) } private func dashboardSnapshot(for session: AuthSession) async throws -> DashboardSnapshot { let client = LiveTypedRequestClient(baseURL: URL(string: session.serverURL)!) let signedRequest = try signedDeviceRequest(session: session, action: "getPassportDashboard", signedFields: []) let response = try await client.fire( method: "getPassportDashboard", request: signedRequest, responseType: PassportDashboardResponse.self ) return DashboardSnapshot( profile: MemberProfile( name: response.profile.name, handle: response.profile.handle, organization: response.profile.organizations.first?.name ?? "No organization", deviceCount: response.profile.deviceCount, recoverySummary: response.profile.recoverySummary ), requests: response.challenges.map { challengeItem in ApprovalRequest( serverID: challengeItem.challenge.id, hintID: challengeItem.challenge.data.notification?.hintId ?? challengeItem.challenge.id, title: approvalTitle(for: challengeItem.challenge), subtitle: approvalSubtitle(for: challengeItem.challenge), source: challengeItem.challenge.data.metadata.audience ?? challengeItem.challenge.data.metadata.originHost ?? "idp.global", createdAt: Date(millisecondsSince1970: challengeItem.challenge.data.createdAt), kind: approvalKind(for: challengeItem.challenge), risk: approvalRisk(for: challengeItem.challenge), scopes: approvalScopes(for: challengeItem.challenge), requiresLocation: challengeItem.challenge.data.metadata.requireLocation, challenge: challengeItem.challenge.data.challenge, signingPayload: challengeItem.signingPayload, deviceSummaryText: challengeItem.challenge.data.metadata.deviceLabel, locationSummaryText: locationSummary(for: challengeItem.challenge), networkSummaryText: challengeItem.challenge.data.metadata.originHost, ipSummaryText: nil, expiresAtDate: Date(millisecondsSince1970: challengeItem.challenge.data.expiresAt), status: .pending ) }, notifications: response.alerts.map { alert in AppNotification( serverID: alert.id, hintID: alert.data.notification.hintId, title: alert.data.title, message: alert.data.body, sentAt: Date(millisecondsSince1970: alert.data.createdAt), kind: notificationKind(for: alert.data.category), isUnread: alert.data.seenAt == nil ) }, devices: response.devices.map { device in PassportDeviceRecord( id: device.id, label: device.data.label, platform: device.data.platform, lastSeenAt: device.data.lastSeenAt.map(Date.init(millisecondsSince1970:)), isCurrent: device.id == session.passportDeviceID ) } ) } private func validateSignedGPSPosition(in request: PairingAuthenticationRequest) throws { if request.transport == .nfc, request.signedGPSPosition == nil { throw AppError.missingSignedGPSPosition } if let signedGPSPosition = request.signedGPSPosition, !signedGPSPosition.verified(for: request.pairingPayload) { throw AppError.invalidSignedGPSPosition } } private func serverURL(from context: PairingPayloadContext) throws -> URL { guard let url = URL(string: "https://\(context.originHost)") else { throw AppError.invalidPairingPayload } return url } private func buildEnrollmentSigningPayload(from context: PairingPayloadContext) throws -> String { guard let challenge = context.challenge, let challengeID = context.challengeID else { throw AppError.invalidPairingPayload } return [ "purpose=passport-enrollment", "origin=\(context.originHost)", "token=\(context.pairingToken)", "challenge=\(challenge)", "challenge_id=\(challengeID)" ].joined(separator: "\n") } private func loadOrCreateKeyMaterial(for originHost: String) throws -> StoredPassportKeyMaterial { if let existing = try keyStore.load(key: originHost) { return existing } let privateKey = P256.Signing.PrivateKey() let material = StoredPassportKeyMaterial( privateKeyBase64: privateKey.rawRepresentation.base64EncodedString(), publicKeyBase64: privateKey.publicKey.x963Representation.base64EncodedString() ) try keyStore.save(material, key: originHost) return material } private func privateKeyData(for session: AuthSession) throws -> Data { guard let stored = try keyStore.load(key: session.originHost), let data = Data(base64Encoded: stored.privateKeyBase64) else { throw AppError.invalidPairingPayload } return data } private func signPayload(_ payload: String, with privateKeyData: Data) throws -> String { let privateKey = try P256.Signing.PrivateKey(rawRepresentation: privateKeyData) let signature = try privateKey.signature(for: Data(payload.utf8)) return signature.derRepresentation.base64EncodedString() } private func signedDeviceRequest( session: AuthSession, action: String, signedFields: [String] ) throws -> SignedDeviceRequest { let nonce = UUID().uuidString.lowercased() let timestamp = Int(Date().timeIntervalSince1970 * 1000) let payload = ([ "purpose=passport-device-request", "origin=\(session.originHost)", "action=\(action)", "device_id=\(session.passportDeviceID)", "timestamp=\(timestamp)", "nonce=\(nonce)" ] + signedFields).joined(separator: "\n") return SignedDeviceRequest( deviceId: session.passportDeviceID, timestamp: timestamp, nonce: nonce, signatureBase64: try signPayload(payload, with: try privateKeyData(for: session)), signatureFormat: "der" ) } private func approvalKind(for challenge: ServerPassportChallenge) -> ApprovalRequestKind { switch challenge.data.type { case "authentication": return .signIn case "physical_access": return .accessGrant default: return .elevatedAction } } private func approvalRisk(for challenge: ServerPassportChallenge) -> ApprovalRisk { if challenge.data.metadata.requireLocation || challenge.data.metadata.requireNfc || challenge.data.metadata.locationPolicy != nil || challenge.data.type != "authentication" { return .elevated } return .routine } private func approvalTitle(for challenge: ServerPassportChallenge) -> String { challenge.data.metadata.notificationTitle ?? challenge.data.type.replacingOccurrences(of: "_", with: " ").capitalized } private func approvalSubtitle(for challenge: ServerPassportChallenge) -> String { if let audience = challenge.data.metadata.audience { return "Approve this proof for \(audience)." } return "Approve this passport challenge on your trusted device." } private func approvalScopes(for challenge: ServerPassportChallenge) -> [String] { var scopes = ["passport:device-proof"] if challenge.data.metadata.requireLocation { scopes.append("context:location") } if challenge.data.metadata.requireNfc { scopes.append("context:nfc") } if let label = challenge.data.metadata.locationPolicy?.label { scopes.append("policy:\(label)") } return scopes } private func locationSummary(for challenge: ServerPassportChallenge) -> String { if let locationPolicy = challenge.data.metadata.locationPolicy { return locationPolicy.label ?? "Location within required area" } if challenge.data.metadata.requireLocation { return "Current location required" } return "Location not required" } private func notificationKind(for category: String) -> AppNotificationKind { switch category { case "security": return .security case "admin": return .approval default: return .system } } } actor MockIDPService: IDPServicing { static let shared = MockIDPService() private let profile = MemberProfile( name: "Phil Kunz", handle: "phil@idp.global", organization: "idp.global", deviceCount: 4, recoverySummary: "Recovery kit healthy with 2 of 3 backup paths verified." ) private let appStateStore: AppStateStoring private var requests: [ApprovalRequest] = [] private var notifications: [AppNotification] = [] private var devices: [PassportDeviceRecord] = [] init(appStateStore: AppStateStoring = UserDefaultsAppStateStore()) { self.appStateStore = appStateStore if let state = appStateStore.load() { requests = state.requests.sorted { $0.createdAt > $1.createdAt } notifications = state.notifications.sorted { $0.sentAt > $1.sentAt } devices = state.devices } else { requests = Self.seedRequests() notifications = Self.seedNotifications() devices = Self.seedDevices() } } func bootstrap() async throws -> BootstrapContext { restoreSharedState() try await Task.sleep(for: .milliseconds(120)) return BootstrapContext( suggestedPairingPayload: "idp.global://pair?token=swiftapp-demo-berlin&origin=code.foss.global&device=Safari%20on%20Berlin%20MBP" ) } func signIn(with request: PairingAuthenticationRequest) async throws -> SignInResult { restoreSharedState() try await Task.sleep(for: .milliseconds(260)) try validateSignedGPSPosition(in: request) let session = try parseSession(from: request) notifications.insert( AppNotification( title: "Passport activated", message: pairingMessage(for: session), sentAt: .now, kind: .security, isUnread: true ), at: 0 ) persistSharedStateIfAvailable() return SignInResult( session: session, snapshot: snapshot() ) } func identify(with request: PairingAuthenticationRequest) async throws -> DashboardSnapshot { restoreSharedState() try await Task.sleep(for: .milliseconds(180)) try validateSignedGPSPosition(in: request) let context = try PairingPayloadParser.parse(request.pairingPayload) notifications.insert( AppNotification( title: "Identity proof completed", message: identificationMessage(for: context, signedGPSPosition: request.signedGPSPosition), sentAt: .now, kind: .security, isUnread: true ), at: 0 ) persistSharedStateIfAvailable() return snapshot() } func refreshDashboard() async throws -> DashboardSnapshot { restoreSharedState() try await Task.sleep(for: .milliseconds(180)) return snapshot() } func approveRequest(id: UUID) async throws -> DashboardSnapshot { restoreSharedState() try await Task.sleep(for: .milliseconds(150)) guard let index = requests.firstIndex(where: { $0.id == id }) else { throw AppError.requestNotFound } requests[index].status = .approved notifications.insert( AppNotification( title: "Identity verified", message: "\(requests[index].title) was completed for \(requests[index].source).", sentAt: .now, kind: .approval, isUnread: true ), at: 0 ) persistSharedStateIfAvailable() return snapshot() } func rejectRequest(id: UUID) async throws -> DashboardSnapshot { restoreSharedState() try await Task.sleep(for: .milliseconds(150)) guard let index = requests.firstIndex(where: { $0.id == id }) else { throw AppError.requestNotFound } requests[index].status = .rejected notifications.insert( AppNotification( title: "Identity proof declined", message: "\(requests[index].title) was declined before the session could continue.", sentAt: .now, kind: .security, isUnread: true ), at: 0 ) persistSharedStateIfAvailable() return snapshot() } func simulateIncomingRequest() async throws -> DashboardSnapshot { restoreSharedState() try await Task.sleep(for: .milliseconds(120)) let syntheticRequest = ApprovalRequest( title: "Prove identity for web sign-in", subtitle: "A browser session is asking this passport to prove that it is really you.", source: "auth.idp.global", createdAt: .now, kind: .signIn, risk: .routine, scopes: ["proof:basic", "client:web", "method:qr"], status: .pending ) requests.insert(syntheticRequest, at: 0) notifications.insert( AppNotification( title: "Fresh identity proof request", message: "A new relying party is waiting for your identity proof.", sentAt: .now, kind: .approval, isUnread: true ), at: 0 ) persistSharedStateIfAvailable() return snapshot() } func markNotificationRead(id: UUID) async throws -> DashboardSnapshot { restoreSharedState() try await Task.sleep(for: .milliseconds(80)) guard let index = notifications.firstIndex(where: { $0.id == id }) else { return snapshot() } notifications[index].isUnread = false persistSharedStateIfAvailable() return snapshot() } private func snapshot() -> DashboardSnapshot { DashboardSnapshot( profile: profile, requests: requests, notifications: notifications, devices: devices ) } private func validateSignedGPSPosition(in request: PairingAuthenticationRequest) throws { if request.transport == .nfc, request.signedGPSPosition == nil { throw AppError.missingSignedGPSPosition } if let signedGPSPosition = request.signedGPSPosition, !signedGPSPosition.verified(for: request.pairingPayload) { throw AppError.invalidSignedGPSPosition } } private func parseSession(from request: PairingAuthenticationRequest) throws -> AuthSession { let context = try PairingPayloadParser.parse(request.pairingPayload) return AuthSession( deviceName: context.deviceName, originHost: context.originHost, serverURL: "https://\(context.originHost)", passportDeviceID: UUID().uuidString, pairedAt: .now, tokenPreview: context.tokenPreview, pairingCode: request.pairingPayload, pairingTransport: request.transport, signedGPSPosition: request.signedGPSPosition ) } private func pairingMessage(for session: AuthSession) -> String { let transportSummary: String switch session.pairingTransport { case .qr: transportSummary = "activated via QR" case .nfc: transportSummary = "activated via NFC with a signed GPS position" case .manual: transportSummary = "activated via manual payload" case .preview: transportSummary = "activated via preview payload" } if let signedGPSPosition = session.signedGPSPosition { return "\(session.deviceName) is now acting as a passport, \(transportSummary) against \(session.originHost) from \(signedGPSPosition.coordinateSummary) \(signedGPSPosition.accuracySummary)." } return "\(session.deviceName) is now acting as a passport, \(transportSummary) against \(session.originHost)." } private func identificationMessage(for context: PairingPayloadContext, signedGPSPosition: SignedGPSPosition?) -> String { if let signedGPSPosition { return "A signed GPS proof was sent for \(context.deviceName) on \(context.originHost) from \(signedGPSPosition.coordinateSummary) \(signedGPSPosition.accuracySummary)." } return "An identity proof was completed for \(context.deviceName) on \(context.originHost)." } private func restoreSharedState() { guard let state = appStateStore.load() else { requests = Self.seedRequests() notifications = Self.seedNotifications() devices = Self.seedDevices() return } requests = state.requests.sorted { $0.createdAt > $1.createdAt } notifications = state.notifications.sorted { $0.sentAt > $1.sentAt } devices = state.devices } private func persistSharedStateIfAvailable() { guard let state = appStateStore.load() else { return } appStateStore.save( PersistedAppState( session: state.session, profile: state.profile, requests: requests, notifications: notifications, devices: devices ) ) } private static func seedRequests() -> [ApprovalRequest] { [ ApprovalRequest( title: "Prove identity for Safari sign-in", subtitle: "The portal wants this passport to prove that the browser session is really you.", source: "code.foss.global", createdAt: .now.addingTimeInterval(-60 * 12), kind: .signIn, risk: .routine, scopes: ["proof:basic", "client:web", "origin:trusted"], status: .pending ), ApprovalRequest( title: "Prove identity for workstation unlock", subtitle: "Your secure workspace is asking for a stronger proof before it unlocks.", source: "berlin-mbp.idp.global", createdAt: .now.addingTimeInterval(-60 * 42), kind: .elevatedAction, risk: .elevated, scopes: ["proof:high", "client:desktop", "presence:required"], status: .pending ), ApprovalRequest( title: "Prove identity for CLI session", subtitle: "The CLI session asked for proof earlier and was completed from this passport.", source: "cli.idp.global", createdAt: .now.addingTimeInterval(-60 * 180), kind: .signIn, risk: .routine, scopes: ["proof:basic", "client:cli"], status: .approved ) ] } private static func seedNotifications() -> [AppNotification] { [ AppNotification( title: "Two identity checks are waiting", message: "One routine web proof and one stronger workstation proof are waiting for this passport.", sentAt: .now.addingTimeInterval(-60 * 8), kind: .approval, isUnread: true ), AppNotification( title: "Recovery health check passed", message: "Backup recovery channels were verified in the last 24 hours.", sentAt: .now.addingTimeInterval(-60 * 95), kind: .system, isUnread: false ), AppNotification( title: "Passport quiet hours active", message: "Routine identity checks will be delivered silently until the morning.", sentAt: .now.addingTimeInterval(-60 * 220), kind: .security, isUnread: false ) ] } private static func seedDevices() -> [PassportDeviceRecord] { [ PassportDeviceRecord(id: UUID().uuidString, label: "Phil's iPhone", platform: "ios", lastSeenAt: .now, isCurrent: true), PassportDeviceRecord(id: UUID().uuidString, label: "Phil's iPad Pro", platform: "ipados", lastSeenAt: .now.addingTimeInterval(-60 * 18), isCurrent: false), PassportDeviceRecord(id: UUID().uuidString, label: "Berlin MacBook Pro", platform: "macos", lastSeenAt: .now.addingTimeInterval(-60 * 74), isCurrent: false) ] } } private struct StoredPassportKeyMaterial: Codable, Equatable { let privateKeyBase64: String let publicKeyBase64: String } private protocol PassportKeyStoring { func load(key: String) throws -> StoredPassportKeyMaterial? func save(_ value: StoredPassportKeyMaterial, key: String) throws } private final class DefaultPassportKeyStore: PassportKeyStoring { private let service = "global.idp.swiftapp.passport-keys" private let fallbackDefaults = UserDefaults.standard func load(key: String) throws -> StoredPassportKeyMaterial? { #if canImport(Security) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] var result: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecSuccess, let data = result as? Data { return try JSONDecoder().decode(StoredPassportKeyMaterial.self, from: data) } if status == errSecItemNotFound { return nil } #endif guard let data = fallbackDefaults.data(forKey: "passport-key-\(key)") else { return nil } return try JSONDecoder().decode(StoredPassportKeyMaterial.self, from: data) } func save(_ value: StoredPassportKeyMaterial, key: String) throws { let data = try JSONEncoder().encode(value) #if canImport(Security) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key ] SecItemDelete(query as CFDictionary) let attributes: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, kSecValueData as String: data ] SecItemAdd(attributes as CFDictionary, nil) #endif fallbackDefaults.set(data, forKey: "passport-key-\(key)") } } private enum DeviceEnvironment { static var currentPlatform: String { #if os(iOS) return "ios" #elseif os(watchOS) return "watchos" #elseif os(macOS) return "macos" #else return "unknown" #endif } static var currentDeviceLabel: String { #if canImport(UIKit) return UIDevice.current.name #elseif os(macOS) return Host.current().localizedName ?? "Mac Passport" #elseif os(watchOS) return "Apple Watch Passport" #else return "Swift Passport Device" #endif } static var capabilities: CapabilityRequest { CapabilityRequest( gps: true, nfc: { #if os(iOS) return true #else return false #endif }(), push: false ) } } private struct LiveTypedRequestClient { let endpoint: URL private let session: URLSession = .shared private let encoder = JSONEncoder() private let decoder = JSONDecoder() init(baseURL: URL) { self.endpoint = baseURL.appending(path: "typedrequest") } func fire( method: String, request: Request, responseType: Response.Type ) async throws -> Response { let envelope = TypedRequestEnvelope(method: method, request: request, response: nil, correlation: TypedCorrelation(phase: "request")) return try await send(envelope: envelope, method: method, responseType: responseType) } private func send( envelope: TypedRequestEnvelope, method: String, responseType: Response.Type ) async throws -> Response { var urlRequest = URLRequest(url: endpoint) urlRequest.httpMethod = "POST" urlRequest.httpBody = try encoder.encode(envelope) urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") let (data, response) = try await session.data(for: urlRequest) guard let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else { throw TypedRequestTransportError(message: "Typed request transport failed") } let typedResponse = try decoder.decode(TypedResponseEnvelope.self, from: data) if let error = typedResponse.error { throw TypedRequestTransportError(message: error.text) } if let retry = typedResponse.retry { try await Task.sleep(for: .milliseconds(retry.waitForMs)) return try await send(envelope: envelope, method: method, responseType: responseType) } guard let responsePayload = typedResponse.response else { throw TypedRequestTransportError(message: "Typed request returned no response payload") } return responsePayload } } private struct TypedCorrelation: Codable { let id: String let phase: String init(id: String = UUID().uuidString, phase: String) { self.id = id self.phase = phase } } private struct TypedRequestEnvelope: Encodable { let method: String let request: Request let response: EmptyJSONValue? let correlation: TypedCorrelation } private struct TypedResponseEnvelope: Decodable { let response: Response? let error: TypedResponseErrorPayload? let retry: TypedRetryInstruction? } private struct TypedResponseErrorPayload: Decodable { let text: String } private struct TypedRetryInstruction: Decodable { let waitForMs: Int } private struct EmptyJSONValue: Codable {} private struct TypedRequestTransportError: LocalizedError { let message: String var errorDescription: String? { message } } private struct SignedDeviceRequest: Encodable { let deviceId: String let timestamp: Int let nonce: String let signatureBase64: String let signatureFormat: String } private struct CapabilityRequest: Encodable { let gps: Bool let nfc: Bool let push: Bool } private struct CompletePassportEnrollmentRequest: Encodable { let pairingToken: String let deviceLabel: String let platform: String let publicKeyX963Base64: String let signatureBase64: String let signatureFormat: String let appVersion: String? let capabilities: CapabilityRequest } private struct ApprovePassportChallengeRequest: Encodable { let challengeId: String let deviceId: String let signatureBase64: String let signatureFormat: String let location: LocationEvidenceRequest? let nfc: NFCEvidenceRequest? } private struct RejectPassportChallengeRequest: Encodable { let deviceId: String let timestamp: Int let nonce: String let signatureBase64: String let signatureFormat: String let challengeId: String } private struct MarkPassportAlertSeenRequest: Encodable { let deviceId: String let timestamp: Int let nonce: String let signatureBase64: String let signatureFormat: String let hintId: String } private struct LocationEvidenceRequest: Encodable { let latitude: Double let longitude: Double let accuracyMeters: Double let capturedAt: Int } private struct NFCEvidenceRequest: Encodable { let tagId: String? let readerId: String? } private struct SimpleSuccessResponse: Decodable { let success: Bool } private struct CompletePassportEnrollmentResponse: Decodable { let device: ServerPassportDevice } private struct ApprovePassportChallengeResponse: Decodable { let success: Bool } private struct RejectPassportChallengeResponse: Decodable { let success: Bool } private struct PassportDashboardResponse: Decodable { struct Profile: Decodable { let userId: String let name: String let handle: String let organizations: [Organization] let deviceCount: Int let recoverySummary: String } struct Organization: Decodable { let id: String let name: String } let profile: Profile let devices: [ServerPassportDevice] let challenges: [ServerChallengeItem] let alerts: [ServerAlert] } private struct ServerChallengeItem: Decodable { let challenge: ServerPassportChallenge let signingPayload: String } private struct ServerPassportDevice: Decodable { struct DataPayload: Decodable { let userId: String let label: String let platform: String let status: String let lastSeenAt: Int? } let id: String let data: DataPayload } private struct ServerPassportChallenge: Decodable { struct DataPayload: Decodable { struct MetadataPayload: Decodable { struct LocationPolicyPayload: Decodable { let mode: String let label: String? let latitude: Double let longitude: Double let radiusMeters: Double let maxAccuracyMeters: Double? } let originHost: String? let audience: String? let notificationTitle: String? let deviceLabel: String? let requireLocation: Bool let requireNfc: Bool let locationPolicy: LocationPolicyPayload? } struct NotificationPayload: Decodable { let hintId: String } let challenge: String let type: String let status: String let metadata: MetadataPayload let notification: NotificationPayload? let createdAt: Int let expiresAt: Int } let id: String let data: DataPayload } private struct ServerAlert: Decodable { struct DataPayload: Decodable { struct NotificationPayload: Decodable { let hintId: String } let category: String let title: String let body: String let createdAt: Int let seenAt: Int? let notification: NotificationPayload } let id: String let data: DataPayload } #if canImport(CoreLocation) @MainActor private final class CurrentLocationEvidenceProvider: NSObject, @preconcurrency CLLocationManagerDelegate { private var manager: CLLocationManager? private var authorizationContinuation: CheckedContinuation? private var locationContinuation: CheckedContinuation? static func currentLocationEvidenceIfAvailable() async throws -> LocationEvidenceRequest { let provider = CurrentLocationEvidenceProvider() return try await provider.currentLocationEvidence() } private func currentLocationEvidence() async throws -> LocationEvidenceRequest { let manager = CLLocationManager() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters self.manager = manager switch manager.authorizationStatus { case .authorizedAlways, .authorizedWhenInUse: break case .notDetermined: let status = await requestAuthorization(using: manager) guard status == .authorizedAlways || status == .authorizedWhenInUse else { throw AppError.locationPermissionDenied } case .denied, .restricted: throw AppError.locationPermissionDenied @unknown default: throw AppError.locationUnavailable } let location = try await requestLocation(using: manager) return LocationEvidenceRequest( latitude: location.coordinate.latitude, longitude: location.coordinate.longitude, accuracyMeters: location.horizontalAccuracy, capturedAt: Int(location.timestamp.timeIntervalSince1970 * 1000) ) } private func requestAuthorization(using manager: CLLocationManager) async -> CLAuthorizationStatus { manager.requestWhenInUseAuthorization() return await withCheckedContinuation { continuation in authorizationContinuation = continuation } } private func requestLocation(using manager: CLLocationManager) async throws -> CLLocation { try await withCheckedThrowingContinuation { continuation in locationContinuation = continuation manager.requestLocation() } } func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { guard let continuation = authorizationContinuation else { return } let status = manager.authorizationStatus guard status != .notDetermined else { return } authorizationContinuation = nil continuation.resume(returning: status) } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let continuation = locationContinuation, let location = locations.last else { return } authorizationContinuation = nil locationContinuation = nil self.manager = nil continuation.resume(returning: location) } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { guard let continuation = locationContinuation else { return } authorizationContinuation = nil locationContinuation = nil self.manager = nil continuation.resume(throwing: AppError.locationUnavailable) } } #else private enum CurrentLocationEvidenceProvider { static func currentLocationEvidenceIfAvailable() async throws -> LocationEvidenceRequest { throw AppError.locationUnavailable } } #endif private extension Date { init(millisecondsSince1970: Int) { self = Date(timeIntervalSince1970: TimeInterval(millisecondsSince1970) / 1000) } }