Files
swiftapp/Sources/Core/Models/AppModels.swift

468 lines
12 KiB
Swift
Raw Normal View History

import CryptoKit
import Foundation
enum AppSection: String, CaseIterable, Identifiable, Hashable {
case overview
case requests
case activity
case account
var id: String { rawValue }
var title: String {
switch self {
case .overview: "Passport"
case .requests: "Requests"
case .activity: "Activity"
case .account: "Account"
}
}
var systemImage: String {
switch self {
case .overview: "person.crop.square.fill"
case .requests: "checklist.checked"
case .activity: "clock.arrow.trianglehead.counterclockwise.rotate.90"
case .account: "person.crop.circle.fill"
}
}
}
enum NotificationPermissionState: String, CaseIterable, Identifiable {
case unknown
case allowed
case provisional
case denied
var id: String { rawValue }
var title: String {
switch self {
case .unknown: "Not Asked Yet"
case .allowed: "Enabled"
case .provisional: "Delivered Quietly"
case .denied: "Disabled"
}
}
var systemImage: String {
switch self {
case .unknown: "bell"
case .allowed: "bell.badge.fill"
case .provisional: "bell.badge"
case .denied: "bell.slash.fill"
}
}
var summary: String {
switch self {
case .unknown:
"The app has not asked for notification delivery yet."
case .allowed:
"Identity proof alerts can break through immediately when a check arrives."
case .provisional:
"Identity proof alerts can be delivered quietly until the user promotes them."
case .denied:
"Identity proof events stay in-app until the user re-enables notifications."
}
}
}
struct BootstrapContext {
let suggestedPairingPayload: String
}
enum PairingTransport: String, Hashable {
case qr
case nfc
case manual
case preview
var title: String {
switch self {
case .qr:
"QR"
case .nfc:
"NFC"
case .manual:
"Manual"
case .preview:
"Preview"
}
}
}
struct PairingAuthenticationRequest: Hashable {
let pairingPayload: String
let transport: PairingTransport
let signedGPSPosition: SignedGPSPosition?
}
struct SignedGPSPosition: Hashable {
let latitude: Double
let longitude: Double
let horizontalAccuracyMeters: Double
let capturedAt: Date
let signatureBase64: String
let publicKeyBase64: String
init(
latitude: Double,
longitude: Double,
horizontalAccuracyMeters: Double,
capturedAt: Date,
signatureBase64: String = "",
publicKeyBase64: String = ""
) {
self.latitude = latitude
self.longitude = longitude
self.horizontalAccuracyMeters = horizontalAccuracyMeters
self.capturedAt = capturedAt
self.signatureBase64 = signatureBase64
self.publicKeyBase64 = publicKeyBase64
}
var coordinateSummary: String {
"\(Self.normalized(latitude, precision: 5)), \(Self.normalized(longitude, precision: 5))"
}
var accuracySummary: String {
"±\(Int(horizontalAccuracyMeters.rounded())) m"
}
func signingPayload(for pairingPayload: String) -> Data {
let lines = [
"payload=\(pairingPayload)",
"latitude=\(Self.normalized(latitude, precision: 6))",
"longitude=\(Self.normalized(longitude, precision: 6))",
"accuracy=\(Self.normalized(horizontalAccuracyMeters, precision: 2))",
"captured_at=\(Self.timestampFormatter.string(from: capturedAt))"
]
return Data(lines.joined(separator: "\n").utf8)
}
func verified(for pairingPayload: String) -> Bool {
guard let signatureData = Data(base64Encoded: signatureBase64),
let publicKeyData = Data(base64Encoded: publicKeyBase64),
let publicKey = try? P256.Signing.PublicKey(x963Representation: publicKeyData),
let signature = try? P256.Signing.ECDSASignature(derRepresentation: signatureData) else {
return false
}
return publicKey.isValidSignature(signature, for: signingPayload(for: pairingPayload))
}
func signed(signatureData: Data, publicKeyData: Data) -> SignedGPSPosition {
SignedGPSPosition(
latitude: latitude,
longitude: longitude,
horizontalAccuracyMeters: horizontalAccuracyMeters,
capturedAt: capturedAt,
signatureBase64: signatureData.base64EncodedString(),
publicKeyBase64: publicKeyData.base64EncodedString()
)
}
private static let timestampFormatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
private static func normalized(_ value: Double, precision: Int) -> String {
String(format: "%.\(precision)f", locale: Locale(identifier: "en_US_POSIX"), value)
}
}
struct DashboardSnapshot {
let profile: MemberProfile
let requests: [ApprovalRequest]
let notifications: [AppNotification]
}
struct SignInResult {
let session: AuthSession
let snapshot: DashboardSnapshot
}
struct MemberProfile: Identifiable, Hashable {
let id: UUID
let name: String
let handle: String
let organization: String
let deviceCount: Int
let recoverySummary: String
init(
id: UUID = UUID(),
name: String,
handle: String,
organization: String,
deviceCount: Int,
recoverySummary: String
) {
self.id = id
self.name = name
self.handle = handle
self.organization = organization
self.deviceCount = deviceCount
self.recoverySummary = recoverySummary
}
}
struct AuthSession: Identifiable, Hashable {
let id: UUID
let deviceName: String
let originHost: String
let pairedAt: Date
let tokenPreview: String
let pairingCode: String
let pairingTransport: PairingTransport
let signedGPSPosition: SignedGPSPosition?
init(
id: UUID = UUID(),
deviceName: String,
originHost: String,
pairedAt: Date,
tokenPreview: String,
pairingCode: String,
pairingTransport: PairingTransport = .manual,
signedGPSPosition: SignedGPSPosition? = nil
) {
self.id = id
self.deviceName = deviceName
self.originHost = originHost
self.pairedAt = pairedAt
self.tokenPreview = tokenPreview
self.pairingCode = pairingCode
self.pairingTransport = pairingTransport
self.signedGPSPosition = signedGPSPosition
}
}
enum ApprovalRequestKind: String, CaseIterable, Hashable {
case signIn
case accessGrant
case elevatedAction
var title: String {
switch self {
case .signIn: "Identity Check"
case .accessGrant: "Strong Proof"
case .elevatedAction: "Sensitive Proof"
}
}
var systemImage: String {
switch self {
case .signIn: "qrcode.viewfinder"
case .accessGrant: "person.badge.shield.checkmark.fill"
case .elevatedAction: "shield.checkered"
}
}
}
enum ApprovalRisk: String, Hashable {
case routine
case elevated
var title: String {
switch self {
case .routine: "Routine"
case .elevated: "Elevated"
}
}
var summary: String {
switch self {
case .routine:
"A familiar identity proof for a normal sign-in or check."
case .elevated:
"A higher-assurance identity proof for a sensitive check."
}
}
var guidance: String {
switch self {
case .routine:
"Review the origin and continue only if it matches the proof you started."
case .elevated:
"Only continue if you initiated this proof and trust the origin asking for it."
}
}
}
enum ApprovalStatus: String, Hashable {
case pending
case approved
case rejected
var title: String {
switch self {
case .pending: "Pending"
case .approved: "Verified"
case .rejected: "Declined"
}
}
var systemImage: String {
switch self {
case .pending: "clock.badge"
case .approved: "checkmark.circle.fill"
case .rejected: "xmark.circle.fill"
}
}
}
struct ApprovalRequest: Identifiable, Hashable {
let id: UUID
let title: String
let subtitle: String
let source: String
let createdAt: Date
let kind: ApprovalRequestKind
let risk: ApprovalRisk
let scopes: [String]
var status: ApprovalStatus
init(
id: UUID = UUID(),
title: String,
subtitle: String,
source: String,
createdAt: Date,
kind: ApprovalRequestKind,
risk: ApprovalRisk,
scopes: [String],
status: ApprovalStatus
) {
self.id = id
self.title = title
self.subtitle = subtitle
self.source = source
self.createdAt = createdAt
self.kind = kind
self.risk = risk
self.scopes = scopes
self.status = status
}
var scopeSummary: String {
if scopes.isEmpty {
return "No proof details listed"
}
let suffix = scopes.count == 1 ? "" : "s"
return "\(scopes.count) proof detail\(suffix)"
}
var trustHeadline: String {
switch (kind, risk) {
case (.signIn, .routine):
"Standard identity proof"
case (.signIn, .elevated):
"High-assurance sign-in proof"
case (.accessGrant, _):
"Cross-device identity proof"
case (.elevatedAction, _):
"Sensitive identity proof"
}
}
var trustDetail: String {
switch kind {
case .signIn:
"This request proves that the person at the browser, CLI, or device is really you."
case .accessGrant:
"This request asks for a stronger proof so the relying party can trust the session with higher confidence."
case .elevatedAction:
"This request asks for the highest confidence proof before continuing with a sensitive flow."
}
}
}
enum AppNotificationKind: String, Hashable {
case approval
case security
case system
var title: String {
switch self {
case .approval: "Proof"
case .security: "Security"
case .system: "System"
}
}
var systemImage: String {
switch self {
case .approval: "checkmark.seal.fill"
case .security: "shield.fill"
case .system: "sparkles"
}
}
var summary: String {
switch self {
case .approval:
"Identity proof activity"
case .security:
"Passport and security posture updates"
case .system:
"Product and environment status messages"
}
}
}
struct AppNotification: Identifiable, Hashable {
let id: UUID
let title: String
let message: String
let sentAt: Date
let kind: AppNotificationKind
var isUnread: Bool
init(
id: UUID = UUID(),
title: String,
message: String,
sentAt: Date,
kind: AppNotificationKind,
isUnread: Bool
) {
self.id = id
self.title = title
self.message = message
self.sentAt = sentAt
self.kind = kind
self.isUnread = isUnread
}
}
enum AppError: LocalizedError {
case invalidPairingPayload
case missingSignedGPSPosition
case invalidSignedGPSPosition
case locationPermissionDenied
case locationUnavailable
case requestNotFound
var errorDescription: String? {
switch self {
case .invalidPairingPayload:
"That idp.global payload is not valid for this action."
case .missingSignedGPSPosition:
"Tap NFC requires a signed GPS position."
case .invalidSignedGPSPosition:
"The signed GPS position attached to this NFC proof could not be verified."
case .locationPermissionDenied:
"Location access is required so Tap NFC can attach a signed GPS position."
case .locationUnavailable:
"Unable to determine the current GPS position for Tap NFC."
case .requestNotFound:
"The selected identity check could not be found."
}
}
}