import Foundation protocol IDPServicing { func bootstrap() async throws -> BootstrapContext func signIn(withQRCode payload: String) async throws -> SignInResult 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 } actor MockIDPService: IDPServicing { 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 var requests: [ApprovalRequest] = [] private var notifications: [AppNotification] = [] init() { requests = Self.seedRequests() notifications = Self.seedNotifications() } func bootstrap() async throws -> BootstrapContext { try await Task.sleep(for: .milliseconds(120)) return BootstrapContext( suggestedQRCodePayload: "idp.global://pair?token=swiftapp-demo-berlin&origin=code.foss.global&device=Safari%20on%20Berlin%20MBP" ) } func signIn(withQRCode payload: String) async throws -> SignInResult { try await Task.sleep(for: .milliseconds(260)) let session = try parseSession(from: payload) notifications.insert( AppNotification( title: "New device paired", message: "\(session.deviceName) completed a QR pairing against \(session.originHost).", sentAt: .now, kind: .security, isUnread: true ), at: 0 ) return SignInResult( session: session, snapshot: snapshot() ) } func refreshDashboard() async throws -> DashboardSnapshot { try await Task.sleep(for: .milliseconds(180)) return snapshot() } func approveRequest(id: UUID) async throws -> DashboardSnapshot { 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: "Request approved", message: "\(requests[index].title) was approved for \(requests[index].source).", sentAt: .now, kind: .approval, isUnread: true ), at: 0 ) return snapshot() } func rejectRequest(id: UUID) async throws -> DashboardSnapshot { 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: "Request rejected", message: "\(requests[index].title) was rejected before token issuance.", sentAt: .now, kind: .security, isUnread: true ), at: 0 ) return snapshot() } func simulateIncomingRequest() async throws -> DashboardSnapshot { try await Task.sleep(for: .milliseconds(120)) let syntheticRequest = ApprovalRequest( title: "Approve SSH certificate issue", subtitle: "CI runner wants a short-lived signing certificate for a deployment pipeline.", source: "deploy.idp.global", createdAt: .now, kind: .elevatedAction, risk: .elevated, scopes: ["sign:ssh", "ttl:10m", "environment:staging"], status: .pending ) requests.insert(syntheticRequest, at: 0) notifications.insert( AppNotification( title: "Fresh approval request", message: "A staging deployment is waiting for your approval.", sentAt: .now, kind: .approval, isUnread: true ), at: 0 ) return snapshot() } func markNotificationRead(id: UUID) async throws -> DashboardSnapshot { try await Task.sleep(for: .milliseconds(80)) guard let index = notifications.firstIndex(where: { $0.id == id }) else { return snapshot() } notifications[index].isUnread = false return snapshot() } private func snapshot() -> DashboardSnapshot { DashboardSnapshot( profile: profile, requests: requests, notifications: notifications ) } private func parseSession(from payload: String) throws -> AuthSession { if let components = URLComponents(string: payload), components.scheme == "idp.global", components.host == "pair" { let queryItems = components.queryItems ?? [] let token = queryItems.first(where: { $0.name == "token" })?.value ?? "demo-token" let origin = queryItems.first(where: { $0.name == "origin" })?.value ?? "code.foss.global" let device = queryItems.first(where: { $0.name == "device" })?.value ?? "Web Session" return AuthSession( deviceName: device, originHost: origin, pairedAt: .now, tokenPreview: String(token.suffix(6)), pairingCode: payload ) } if payload.contains("token") || payload.contains("pair") { return AuthSession( deviceName: "Manual Pairing", originHost: "code.foss.global", pairedAt: .now, tokenPreview: String(payload.suffix(6)), pairingCode: payload ) } throw AppError.invalidQRCode } private static func seedRequests() -> [ApprovalRequest] { [ ApprovalRequest( title: "Approve Safari sign-in", subtitle: "A browser session from Berlin wants an SSO token for the portal.", source: "code.foss.global", createdAt: .now.addingTimeInterval(-60 * 12), kind: .signIn, risk: .routine, scopes: ["openid", "profile", "groups:read"], status: .pending ), ApprovalRequest( title: "Grant package publish access", subtitle: "The release bot is asking for a scoped publish token.", source: "registry.foss.global", createdAt: .now.addingTimeInterval(-60 * 42), kind: .accessGrant, risk: .elevated, scopes: ["packages:write", "ttl:30m"], status: .pending ), ApprovalRequest( title: "Approve CLI login", subtitle: "A terminal session completed QR pairing earlier today.", source: "cli.idp.global", createdAt: .now.addingTimeInterval(-60 * 180), kind: .signIn, risk: .routine, scopes: ["openid", "profile"], status: .approved ) ] } private static func seedNotifications() -> [AppNotification] { [ AppNotification( title: "Two requests are waiting", message: "The queue includes one routine sign-in and one elevated access grant.", 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: "Quiet hours active on mobile", message: "Routine notifications will be delivered silently until the morning.", sentAt: .now.addingTimeInterval(-60 * 220), kind: .security, isUnread: false ) ] } }