Move the app payload under swift/ while keeping git, package.json, and .smartconfig.json at the repo root. This standardizes the Swift app setup so build, test, run, and watch workflows match the other repos.
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
enum AppSection: String, CaseIterable, Identifiable, Hashable, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Codable {
|
||||
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, Equatable {
|
||||
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."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user