Adopt root-level tsswift app layout
CI / test (push) Has been cancelled

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:
2026-04-19 01:21:43 +02:00
parent d534964601
commit a6939453f8
61 changed files with 2341 additions and 3 deletions
+467
View File
@@ -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."
}
}
}
@@ -0,0 +1,46 @@
import Foundation
struct PersistedAppState: Codable, Equatable {
let session: AuthSession
let profile: MemberProfile
let requests: [ApprovalRequest]
let notifications: [AppNotification]
}
protocol AppStateStoring {
func load() -> PersistedAppState?
func save(_ state: PersistedAppState)
func clear()
}
final class UserDefaultsAppStateStore: AppStateStoring {
private let defaults: UserDefaults
private let storageKey: String
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
init(defaults: UserDefaults = .standard, storageKey: String = "persisted-app-state") {
self.defaults = defaults
self.storageKey = storageKey
}
func load() -> PersistedAppState? {
guard let data = defaults.data(forKey: storageKey) else {
return nil
}
return try? decoder.decode(PersistedAppState.self, from: data)
}
func save(_ state: PersistedAppState) {
guard let data = try? encoder.encode(state) else {
return
}
defaults.set(data, forKey: storageKey)
}
func clear() {
defaults.removeObject(forKey: storageKey)
}
}
@@ -0,0 +1,290 @@
import Foundation
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
}
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(
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 {
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
)
return SignInResult(
session: session,
snapshot: snapshot()
)
}
func identify(with request: PairingAuthenticationRequest) async throws -> DashboardSnapshot {
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
)
return 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: "Identity verified",
message: "\(requests[index].title) was completed 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: "Identity proof declined",
message: "\(requests[index].title) was declined before the session could continue.",
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: "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
)
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 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,
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 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
)
]
}
}
@@ -0,0 +1,54 @@
import Foundation
import UserNotifications
protocol NotificationCoordinating {
func authorizationStatus() async -> NotificationPermissionState
func requestAuthorization() async throws -> NotificationPermissionState
func scheduleTestNotification(title: String, body: String) async throws
}
final class NotificationCoordinator: NotificationCoordinating {
private let center = UNUserNotificationCenter.current()
func authorizationStatus() async -> NotificationPermissionState {
let settings = await center.notificationSettings()
return NotificationPermissionState(settings.authorizationStatus)
}
func requestAuthorization() async throws -> NotificationPermissionState {
_ = try await center.requestAuthorization(options: [.alert, .badge, .sound])
return await authorizationStatus()
}
func scheduleTestNotification(title: String, body: String) async throws {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false)
)
try await center.add(request)
}
}
private extension NotificationPermissionState {
init(_ status: UNAuthorizationStatus) {
switch status {
case .authorized:
self = .allowed
case .provisional, .ephemeral:
self = .provisional
case .denied:
self = .denied
case .notDetermined:
self = .unknown
@unknown default:
self = .unknown
}
}
}
@@ -0,0 +1,19 @@
import CryptoKit
import Foundation
enum OneTimePasscodeGenerator {
static func code(for pairingCode: String, at date: Date) -> String {
let timeSlot = Int(date.timeIntervalSince1970 / 30)
let digest = SHA256.hash(data: Data("\(pairingCode)|\(timeSlot)".utf8))
let value = digest.prefix(4).reduce(UInt32(0)) { partialResult, byte in
(partialResult << 8) | UInt32(byte)
}
return String(format: "%06d", locale: Locale(identifier: "en_US_POSIX"), Int(value % 1_000_000))
}
static func renewalCountdown(at date: Date) -> Int {
let elapsed = Int(date.timeIntervalSince1970) % 30
return elapsed == 0 ? 30 : 30 - elapsed
}
}
@@ -0,0 +1,38 @@
import Foundation
struct PairingPayloadContext: Equatable {
let deviceName: String
let originHost: String
let tokenPreview: String
}
enum PairingPayloadParser {
static func parse(_ payload: String) throws -> PairingPayloadContext {
let trimmedPayload = payload.trimmingCharacters(in: .whitespacesAndNewlines)
if let components = URLComponents(string: trimmedPayload),
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 PairingPayloadContext(
deviceName: device,
originHost: origin,
tokenPreview: String(token.suffix(6))
)
}
if trimmedPayload.contains("token") || trimmedPayload.contains("pair") {
return PairingPayloadContext(
deviceName: "Manual Session",
originHost: "code.foss.global",
tokenPreview: String(trimmedPayload.suffix(6))
)
}
throw AppError.invalidPairingPayload
}
}