krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

cc69cbd7e589ec4b065ce74d8c5e7714a040cfc5

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-26T05:22:01Z

DomainDig v4.2.0: Add a local-only HTTP API layer for DomainDig

Expose DomainDig as a programmable local domain intelligence engine over
localhost with explicit user opt-in and token-based authentication.

Highlights:
- add a localhost-only API server with safe start/stop lifecycle
- require a local token for every request and store it in Keychain
- add read endpoints for portfolio, domains, history, events, and monitoring
- add inspection endpoints that reuse the existing inspection engine
- add monitoring enable/disable control endpoints
- add structured JSON response envelopes with API versioning
- add lightweight capped request logging with clear/reset support
- add a Settings UI for Local API enablement, token management, status, and logs
- start the server on launch only when enabled
- include Local API secrets in local data reset cleanup

This is the first programmability release for DomainDig and establishes the
foundation for Shortcuts, scripts, and other local automation workflows.
 DomainDig.xcodeproj/project.pbxproj |   20 +-
 DomainDig/ContentView.swift         |  121 ++++
 DomainDig/DataResetService.swift    |    3 +-
 DomainDig/DomainDigApp.swift        |    4 +
 LocalAPIModels.swift                |   45 ++
 LocalAPIService.swift               | 1029 +++++++++++++++++++++++++++++++++++
 6 files changed, 1217 insertions(+), 5 deletions(-)

diff --git a/DomainDig.xcodeproj/project.pbxproj b/DomainDig.xcodeproj/project.pbxproj
index 2de9125..02634ff 100644
--- a/DomainDig.xcodeproj/project.pbxproj
+++ b/DomainDig.xcodeproj/project.pbxproj
@@ -16,6 +16,10 @@
 		8BBFEFCB2F987E8700E8E144 /* DomainReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */; };
 		8BBFEFCC2F987E8700E8E144 /* DomainReportExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF052F9874AE00E8E144 /* DomainReportExporter.swift */; };
 		8BBFEFCD2F987E8700E8E144 /* LookupSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */; };
+		8BCA3CBE2F9C8D57004B742C /* LocalAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */; };
+		8BCA3CBF2F9C8D57004B742C /* LocalAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */; };
+		8BCA3CC02F9C8D57004B742C /* LocalAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */; };
+		8BCA3CC12F9C8D57004B742C /* LocalAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */; };
 		8BF9DA862F9B13FB00EF41D5 /* DomainDataPortabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */; };
 		8BF9DA872F9B13FB00EF41D5 /* DomainDataPortabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */; };
 /* End PBXBuildFile section */
@@ -27,6 +31,8 @@
 		8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainReportBuilder.swift; sourceTree = "<group>"; };
 		8BBFEF052F9874AE00E8E144 /* DomainReportExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainReportExporter.swift; sourceTree = "<group>"; };
 		8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LookupSnapshot.swift; sourceTree = "<group>"; };
+		8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIModels.swift; sourceTree = "<group>"; };
+		8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIService.swift; sourceTree = "<group>"; };
 		8BF124F92F70000100933221 /* domaindig */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = domaindig; sourceTree = BUILT_PRODUCTS_DIR; };
 		8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityService.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
@@ -99,6 +105,8 @@
 				8BBFEF052F9874AE00E8E144 /* DomainReportExporter.swift */,
 				8BBFEF022F9874AE00E8E144 /* DomainDigCLI.swift */,
 				8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */,
+				8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */,
+				8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */,
 			);
 			sourceTree = "<group>";
 		};
@@ -212,6 +220,8 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				8BCA3CC02F9C8D57004B742C /* LocalAPIModels.swift in Sources */,
+				8BCA3CC12F9C8D57004B742C /* LocalAPIService.swift in Sources */,
 				8BBFEF092F9874AE00E8E144 /* DomainInspectionService.swift in Sources */,
 				8BBFEF0A2F9874AE00E8E144 /* DomainReportBuilder.swift in Sources */,
 				8BBFEF0B2F9874AE00E8E144 /* DomainReportExporter.swift in Sources */,
@@ -230,6 +240,8 @@
 				8BBFEFCB2F987E8700E8E144 /* DomainReportBuilder.swift in Sources */,
 				8BBFEFCC2F987E8700E8E144 /* DomainReportExporter.swift in Sources */,
 				8BBFEFCD2F987E8700E8E144 /* LookupSnapshot.swift in Sources */,
+				8BCA3CBE2F9C8D57004B742C /* LocalAPIModels.swift in Sources */,
+				8BCA3CBF2F9C8D57004B742C /* LocalAPIService.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -366,7 +378,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 33;
+				CURRENT_PROJECT_VERSION = 34;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -383,7 +395,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 4.1.0;
+				MARKETING_VERSION = 4.2.0;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -403,7 +415,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 33;
+				CURRENT_PROJECT_VERSION = 34;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -420,7 +432,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 4.1.0;
+				MARKETING_VERSION = 4.2.0;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
index c5c8e50..3716f88 100644
--- a/DomainDig/ContentView.swift
+++ b/DomainDig/ContentView.swift
@@ -2560,6 +2560,10 @@ struct SettingsView: View {
                     IntegrationsSettingsView()
                 }
 
+                NavigationLink("Local API") {
+                    LocalAPISettingsView()
+                }
+
                 NavigationLink("iCloud Sync") {
                     CloudSyncSettingsView()
                 }
@@ -2749,6 +2753,123 @@ private struct CloudSyncSettingsView: View {
     }
 }
 
+private struct LocalAPISettingsView: View {
+    @Environment(\.appDensity) private var appDensity
+    @State private var localAPIService = LocalAPIService.shared
+    @State private var portText = ""
+
+    var body: some View {
+        Form {
+            Section("Local API") {
+                Toggle(
+                    "Enable Local API",
+                    isOn: Binding(
+                        get: { localAPIService.config.isEnabled },
+                        set: { localAPIService.setEnabled($0) }
+                    )
+                )
+
+                TextField(
+                    "Port",
+                    text: Binding(
+                        get: { portText },
+                        set: { newValue in
+                            portText = newValue
+                            if let port = Int(newValue) {
+                                localAPIService.setPort(port)
+                            }
+                        }
+                    )
+                )
+                .keyboardType(.numberPad)
+
+                LabeledContent("Address", value: localAPIService.address)
+                LabeledContent("Status", value: localAPIService.isRunning ? "Running" : (localAPIService.config.isEnabled ? "Stopped" : "Disabled"))
+                LabeledContent("Token", value: localAPIService.maskedToken)
+
+                if let statusMessage = localAPIService.statusMessage {
+                    Text(statusMessage)
+                        .font(appDensity.font(.caption, design: .default))
+                        .foregroundStyle(.secondary)
+                }
+            }
+
+            Section("Authentication") {
+                Button("Copy Token") {
+                    localAPIService.copyToken()
+                }
+
+                Button("Rotate Token") {
+                    localAPIService.rotateToken()
+                }
+
+                Text("Every request requires either `Authorization: Bearer <token>` or `X-API-Token`. DomainDig stores the token in Keychain and only binds the server to localhost.")
+                    .font(appDensity.font(.caption, design: .default))
+                    .foregroundStyle(.secondary)
+            }
+
+            Section("Request Logging") {
+                Toggle(
+                    "Log Requests",
+                    isOn: Binding(
+                        get: { localAPIService.config.requestLoggingEnabled },
+                        set: { localAPIService.setRequestLoggingEnabled($0) }
+                    )
+                )
+
+                if localAPIService.requestLogs.isEmpty {
+                    Text("No local API requests logged yet.")
+                        .font(appDensity.font(.caption, design: .default))
+                        .foregroundStyle(.secondary)
+                } else {
+                    ForEach(localAPIService.requestLogs.prefix(25)) { log in
+                        VStack(alignment: .leading, spacing: 4) {
+                            HStack {
+                                Text("\(log.method) \(log.path)")
+                                    .font(appDensity.font(.callout, design: .monospaced))
+                                Spacer()
+                                Text("\(log.statusCode)")
+                                    .font(appDensity.font(.caption, design: .default))
+                                    .foregroundStyle(log.statusCode >= 400 ? .red : .secondary)
+                            }
+
+                            Text(log.timestamp.formatted(date: .abbreviated, time: .standard))
+                                .font(appDensity.font(.caption2, design: .default))
+                                .foregroundStyle(.secondary)
+
+                            Text("\(Int(log.duration * 1000)) ms")
+                                .font(appDensity.font(.caption2, design: .default))
+                                .foregroundStyle(.secondary)
+                        }
+                    }
+                }
+
+                Button("Clear Logs", role: .destructive) {
+                    localAPIService.clearRequestLogs()
+                }
+            }
+
+            Section("Control") {
+                Button("Restart Server") {
+                    localAPIService.setEnabled(false)
+                    localAPIService.setEnabled(true)
+                }
+                .disabled(!localAPIService.config.isEnabled)
+
+                Button("Stop Server") {
+                    localAPIService.stopServer()
+                }
+                .disabled(!localAPIService.isRunning)
+            }
+        }
+        .navigationTitle("Local API")
+        .onAppear {
+            portText = String(localAPIService.config.port)
+            localAPIService.refresh()
+        }
+    }
+}
+
 private struct MonitoringSettingsView: View {
     @Environment(\.appDensity) private var appDensity
     @Bindable var viewModel: DomainViewModel
diff --git a/DomainDig/DataResetService.swift b/DomainDig/DataResetService.swift
index e993eba..d4c5bfe 100644
--- a/DomainDig/DataResetService.swift
+++ b/DomainDig/DataResetService.swift
@@ -15,7 +15,7 @@ enum DataResetService {
 
     static func wipeAllLocalData(viewModel: DomainViewModel) async throws {
         let secretReferences = await MainActor.run {
-            IntegrationService.shared.localSecretReferences()
+            IntegrationService.shared.localSecretReferences() + LocalAPIService.shared.localSecretReferences()
         }
 
         try await Task.detached(priority: .userInitiated) {
@@ -28,6 +28,7 @@ enum DataResetService {
 
         await MainActor.run {
             IntegrationService.shared.resetAfterLocalWipe()
+            LocalAPIService.shared.resetAfterLocalWipe()
             CloudSyncService.shared.resetLocalStateAfterWipe()
             PurchaseService.shared.resetCachedStateAfterLocalWipe()
             _ = DomainMonitoringScheduler.shared.syncSchedule()
diff --git a/DomainDig/DomainDigApp.swift b/DomainDig/DomainDigApp.swift
index 2071213..ff40caf 100644
--- a/DomainDig/DomainDigApp.swift
+++ b/DomainDig/DomainDigApp.swift
@@ -15,6 +15,7 @@ struct DomainDigApp: App {
     @State private var viewModel = DomainViewModel()
     @State private var purchaseService = PurchaseService.shared
     @State private var cloudSyncService = CloudSyncService.shared
+    @State private var localAPIService = LocalAPIService.shared
 
     init() {
         LocalNotificationService.shared.configureForegroundPresentation()
@@ -28,11 +29,13 @@ struct DomainDigApp: App {
                 .task {
                     let _ = purchaseService.currentTier
                     let _ = cloudSyncService.status
+                    let _ = localAPIService.isRunning
                     let _ = IntegrationService.shared.targets.count
                     await purchaseService.refreshEntitlements()
                     viewModel.refreshMonitoringState()
                     await viewModel.refreshMonitoringAuthorizationStatus()
                     await cloudSyncService.refreshAvailability()
+                    localAPIService.refresh()
                     cloudSyncService.scheduleSyncIfNeeded(trigger: .launch)
                     viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
                     IntegrationService.shared.processQueueNow()
@@ -50,6 +53,7 @@ struct DomainDigApp: App {
                 await viewModel.refreshMonitoringAuthorizationStatus()
                 await cloudSyncService.refreshAvailability()
             }
+            localAPIService.refresh()
             cloudSyncService.scheduleSyncIfNeeded(trigger: .launch)
             viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule()
             IntegrationService.shared.processQueueNow()
diff --git a/LocalAPIModels.swift b/LocalAPIModels.swift
new file mode 100644
index 0000000..5a18f11
--- /dev/null
+++ b/LocalAPIModels.swift
@@ -0,0 +1,45 @@
+import Foundation
+
+struct LocalAPIConfig: Codable, Equatable, Sendable {
+    var isEnabled: Bool
+    var port: Int
+    var token: String
+    var requestLoggingEnabled: Bool
+
+    init(
+        isEnabled: Bool = false,
+        port: Int = 47821,
+        token: String = "",
+        requestLoggingEnabled: Bool = true
+    ) {
+        self.isEnabled = isEnabled
+        self.port = port
+        self.token = token
+        self.requestLoggingEnabled = requestLoggingEnabled
+    }
+}
+
+struct APIRequestLog: Codable, Identifiable, Equatable, Sendable {
+    let id: UUID
+    let timestamp: Date
+    let method: String
+    let path: String
+    let statusCode: Int
+    let duration: TimeInterval
+
+    init(
+        id: UUID = UUID(),
+        timestamp: Date,
+        method: String,
+        path: String,
+        statusCode: Int,
+        duration: TimeInterval
+    ) {
+        self.id = id
+        self.timestamp = timestamp
+        self.method = method
+        self.path = path
+        self.statusCode = statusCode
+        self.duration = duration
+    }
+}
diff --git a/LocalAPIService.swift b/LocalAPIService.swift
new file mode 100644
index 0000000..ad1e07d
--- /dev/null
+++ b/LocalAPIService.swift
@@ -0,0 +1,1029 @@
+import Foundation
+import Network
+import Observation
+import Security
+
+private let localAPIVersion = "v1"
+
+private enum LocalAPIServerError: LocalizedError {
+    case missingSecret
+    case secretPersistenceFailed
+    case serverStartFailed(String)
+
+    var errorDescription: String? {
+        switch self {
+        case .missingSecret:
+            return "Local API token is unavailable."
+        case .secretPersistenceFailed:
+            return "Could not store the Local API token in Keychain."
+        case .serverStartFailed(let message):
+            return message
+        }
+    }
+}
+
+private final class ListenerResumeState: @unchecked Sendable {
+    private let lock = NSLock()
+    nonisolated(unsafe) private var hasResumed = false
+
+    nonisolated func beginResume() -> Bool {
+        lock.lock()
+        defer { lock.unlock() }
+
+        guard !hasResumed else { return false }
+        hasResumed = true
+        return true
+    }
+}
+
+@MainActor
+@Observable
+final class LocalAPIService {
+    static let shared = LocalAPIService()
+
+    private static let configDefaultsKey = "localAPI.config"
+    private static let logsDefaultsKey = "localAPI.requestLogs"
+    private static let maxRequestLogs = 120
+    private(set) var config: LocalAPIConfig
+    private(set) var requestLogs: [APIRequestLog]
+    private(set) var isRunning = false
+    private(set) var boundPort: Int?
+    private(set) var statusMessage: String?
+
+    private var server: LocalAPIServer?
+
+    private init() {
+        config = Self.loadConfig()
+        requestLogs = Self.loadRequestLogs()
+    }
+
+    var address: String {
+        "http://127.0.0.1:\(boundPort ?? config.port)"
+    }
+
+    var maskedToken: String {
+        guard !config.token.isEmpty else { return "Unavailable" }
+        let suffix = String(config.token.suffix(8))
+        return "••••••••\(suffix)"
+    }
+
+    func refresh() {
+        config = Self.loadConfig()
+        requestLogs = Self.loadRequestLogs()
+
+        Task {
+            if config.isEnabled {
+                await startServerIfNeeded()
+            } else {
+                stopServer()
+            }
+        }
+    }
+
+    func setEnabled(_ isEnabled: Bool) {
+        config.isEnabled = isEnabled
+        persistConfig()
+
+        Task {
+            if isEnabled {
+                await startServer(forceRestart: true)
+            } else {
+                stopServer()
+            }
+        }
+    }
+
+    func setPort(_ port: Int) {
+        let sanitizedPort = Self.sanitizedPort(port)
+        guard config.port != sanitizedPort else { return }
+        config.port = sanitizedPort
+        persistConfig()
+
+        Task {
+            guard config.isEnabled else { return }
+            await startServer(forceRestart: true)
+        }
+    }
+
+    func setRequestLoggingEnabled(_ isEnabled: Bool) {
+        config.requestLoggingEnabled = isEnabled
+        persistConfig()
+    }
+
+    func rotateToken() {
+        config.token = Self.generateToken()
+        persistConfig()
+
+        Task {
+            guard config.isEnabled else { return }
+            await startServer(forceRestart: true)
+        }
+    }
+
+    func stopServer() {
+        server?.stop()
+        server = nil
+        isRunning = false
+        boundPort = nil
+        if config.isEnabled {
+            statusMessage = "Stopped"
+        } else {
+            statusMessage = "Disabled"
+        }
+    }
+
+    func clearRequestLogs() {
+        requestLogs.removeAll()
+        Self.saveRequestLogs([])
+    }
+
+    func copyToken() {
+        guard !config.token.isEmpty else { return }
+        AppClipboard.copy(config.token)
+    }
+
+    func localSecretReferences() -> [String] {
+        [LocalAPISecretStore.reference]
+    }
+
+    func resetAfterLocalWipe() {
+        stopServer()
+        config = LocalAPIConfig(token: Self.generateToken())
+        requestLogs = []
+        persistConfig()
+        clearRequestLogs()
+    }
+
+    private func startServerIfNeeded() async {
+        guard config.isEnabled else {
+            stopServer()
+            return
+        }
+        guard server == nil else { return }
+        await startServer(forceRestart: false)
+    }
+
+    private func startServer(forceRestart: Bool) async {
+        if forceRestart {
+            stopServer()
+        }
+
+        guard config.isEnabled else {
+            stopServer()
+            return
+        }
+
+        let token = config.token.isEmpty ? Self.generateToken() : config.token
+        if token != config.token {
+            config.token = token
+            persistConfig()
+        }
+
+        let server = LocalAPIServer(
+            port: Self.sanitizedPort(config.port),
+            token: token,
+            requestLogger: { log in
+                Task { @MainActor [weak self] in
+                    self?.record(log)
+                }
+            },
+            stateLogger: { stateMessage in
+                Task { @MainActor [weak self] in
+                    self?.statusMessage = stateMessage
+                }
+            }
+        )
+
+        do {
+            let activePort = try await server.start()
+            self.server = server
+            boundPort = activePort
+            isRunning = true
+            statusMessage = "Listening on localhost:\(activePort)"
+        } catch {
+            self.server = nil
+            boundPort = nil
+            isRunning = false
+            statusMessage = error.localizedDescription
+        }
+    }
+
+    private func record(_ log: APIRequestLog) {
+        guard config.requestLoggingEnabled else { return }
+        requestLogs.insert(log, at: 0)
+        requestLogs = Array(requestLogs.prefix(Self.maxRequestLogs))
+        Self.saveRequestLogs(requestLogs)
+    }
+
+    private func persistConfig() {
+        config.port = Self.sanitizedPort(config.port)
+        if config.token.isEmpty {
+            config.token = Self.generateToken()
+        }
+
+        Self.saveConfig(config)
+    }
+
+    private static func loadConfig(defaults: UserDefaults = .standard) -> LocalAPIConfig {
+        let persisted: PersistedLocalAPIConfig
+        if let data = defaults.data(forKey: configDefaultsKey),
+           let decoded = try? JSONDecoder().decode(PersistedLocalAPIConfig.self, from: data) {
+            persisted = decoded
+        } else {
+            persisted = PersistedLocalAPIConfig()
+        }
+
+        let token = (try? LocalAPISecretStore.secret(reference: LocalAPISecretStore.reference)) ?? generateToken()
+        if (try? LocalAPISecretStore.secret(reference: LocalAPISecretStore.reference)) == nil {
+            try? LocalAPISecretStore.save(secret: token, reference: LocalAPISecretStore.reference)
+        }
+
+        return LocalAPIConfig(
+            isEnabled: persisted.isEnabled,
+            port: sanitizedPort(persisted.port),
+            token: token,
+            requestLoggingEnabled: persisted.requestLoggingEnabled
+        )
+    }
+
+    private static func saveConfig(_ config: LocalAPIConfig, defaults: UserDefaults = .standard) {
+        let persisted = PersistedLocalAPIConfig(
+            isEnabled: config.isEnabled,
+            port: sanitizedPort(config.port),
+            requestLoggingEnabled: config.requestLoggingEnabled
+        )
+
+        if let data = try? JSONEncoder().encode(persisted) {
+            defaults.set(data, forKey: configDefaultsKey)
+        }
+
+        try? LocalAPISecretStore.save(secret: config.token, reference: LocalAPISecretStore.reference)
+    }
+
+    private static func loadRequestLogs(defaults: UserDefaults = .standard) -> [APIRequestLog] {
+        guard let data = defaults.data(forKey: logsDefaultsKey),
+              let decoded = try? JSONDecoder().decode([APIRequestLog].self, from: data) else {
+            return []
+        }
+        return decoded
+    }
+
+    private static func saveRequestLogs(_ logs: [APIRequestLog], defaults: UserDefaults = .standard) {
+        if let data = try? JSONEncoder().encode(Array(logs.prefix(maxRequestLogs))) {
+            defaults.set(data, forKey: logsDefaultsKey)
+        }
+    }
+
+    private static func sanitizedPort(_ port: Int) -> Int {
+        min(max(port, 1024), 65535)
+    }
+
+    private static func generateToken() -> String {
+        let bytes = (0..<24).map { _ in UInt8.random(in: .min ... .max) }
+        let data = Data(bytes)
+        return data
+            .base64EncodedString()
+            .replacingOccurrences(of: "+", with: "-")
+            .replacingOccurrences(of: "/", with: "_")
+            .replacingOccurrences(of: "=", with: "")
+    }
+
+    private struct PersistedLocalAPIConfig: Codable {
+        var isEnabled: Bool = false
+        var port: Int = 47821
+        var requestLoggingEnabled: Bool = true
+    }
+
+    private enum LocalAPISecretStore {
+        static let reference = "DomainDig.LocalAPI.Token"
+
+        static func save(secret: String, reference: String) throws {
+            let data = Data(secret.utf8)
+            try? delete(reference: reference)
+
+            let query: [String: Any] = [
+                kSecClass as String: kSecClassGenericPassword,
+                kSecAttrAccount as String: reference,
+                kSecValueData as String: data,
+                kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
+            ]
+
+            let status = SecItemAdd(query as CFDictionary, nil)
+            guard status == errSecSuccess else {
+                throw LocalAPIServerError.secretPersistenceFailed
+            }
+        }
+
+        static func secret(reference: String) throws -> String {
+            let query: [String: Any] = [
+                kSecClass as String: kSecClassGenericPassword,
+                kSecAttrAccount as String: reference,
+                kSecReturnData as String: true,
+                kSecMatchLimit as String: kSecMatchLimitOne
+            ]
+
+            var result: CFTypeRef?
+            let status = SecItemCopyMatching(query as CFDictionary, &result)
+            guard status == errSecSuccess,
+                  let data = result as? Data,
+                  let secret = String(data: data, encoding: .utf8) else {
+                throw LocalAPIServerError.missingSecret
+            }
+
+            return secret
+        }
+
+        static func delete(reference: String) throws {
+            let query: [String: Any] = [
+                kSecClass as String: kSecClassGenericPassword,
+                kSecAttrAccount as String: reference
+            ]
+            SecItemDelete(query as CFDictionary)
+        }
+    }
+
+}
+
+private final class LocalAPIServer: @unchecked Sendable {
+    private let port: Int
+    private let token: String
+    private let requestLogger: @Sendable (APIRequestLog) -> Void
+    private let stateLogger: @Sendable (String) -> Void
+    private let handler = LocalAPIRequestHandler()
+    private let queue = DispatchQueue(label: "DomainDig.LocalAPIServer")
+
+    private var listener: NWListener?
+
+    init(
+        port: Int,
+        token: String,
+        requestLogger: @escaping @Sendable (APIRequestLog) -> Void,
+        stateLogger: @escaping @Sendable (String) -> Void
+    ) {
+        self.port = port
+        self.token = token
+        self.requestLogger = requestLogger
+        self.stateLogger = stateLogger
+    }
+
+    func start() async throws -> Int {
+        let parameters = NWParameters.tcp
+        parameters.acceptLocalOnly = true
+        parameters.allowLocalEndpointReuse = true
+        parameters.requiredLocalEndpoint = .hostPort(
+            host: .ipv4(IPv4Address.loopback),
+            port: NWEndpoint.Port(rawValue: UInt16(port)) ?? .any
+        )
+
+        let listener: NWListener
+        do {
+            listener = try NWListener(
+                using: parameters,
+                on: NWEndpoint.Port(rawValue: UInt16(port)) ?? .any
+            )
+        } catch {
+            throw LocalAPIServerError.serverStartFailed("Could not start Local API on port \(port).")
+        }
+
+        listener.newConnectionHandler = { [weak self] connection in
+            Task {
+                await self?.handle(connection: connection)
+            }
+        }
+
+        return try await withCheckedThrowingContinuation { continuation in
+            let resumeState = ListenerResumeState()
+
+            listener.stateUpdateHandler = { state in
+                switch state {
+                case .ready:
+                    guard resumeState.beginResume() else { return }
+                    continuation.resume(returning: Int(listener.port?.rawValue ?? UInt16(self.port)))
+                case .failed(let error):
+                    self.stateLogger("Failed: \(error.localizedDescription)")
+                    guard resumeState.beginResume() else { return }
+                    continuation.resume(throwing: LocalAPIServerError.serverStartFailed(error.localizedDescription))
+                case .cancelled:
+                    self.stateLogger("Stopped")
+                default:
+                    break
+                }
+            }
+
+            self.listener = listener
+            listener.start(queue: self.queue)
+        }
+    }
+
+    func stop() {
+        listener?.cancel()
+        listener = nil
+    }
+
+    private func handle(connection: NWConnection) async {
+        connection.start(queue: queue)
+        let startedAt = Date()
+
+        do {
+            let requestData = try await readCompleteRequest(from: connection)
+            let request = try LocalAPIHTTPParser.parseRequest(from: requestData)
+            let response = await handler.handle(request: request, expectedToken: token)
+            let log = APIRequestLog(
+                timestamp: startedAt,
+                method: request.method,
+                path: request.pathWithQuery,
+                statusCode: response.statusCode,
+                duration: Date().timeIntervalSince(startedAt)
+            )
+            requestLogger(log)
+            try await send(response.serialized(), on: connection)
+        } catch let error as LocalAPIHTTPParser.ParserError {
+            let response = LocalAPIHTTPResponse.error(statusCode: 400, code: "bad_request", message: error.localizedDescription)
+            let log = APIRequestLog(
+                timestamp: startedAt,
+                method: "INVALID",
+                path: "/",
+                statusCode: response.statusCode,
+                duration: Date().timeIntervalSince(startedAt)
+            )
+            requestLogger(log)
+            try? await send(response.serialized(), on: connection)
+        } catch {
+            let response = LocalAPIHTTPResponse.error(statusCode: 500, code: "internal_error", message: "The Local API request failed.")
+            let log = APIRequestLog(
+                timestamp: startedAt,
+                method: "ERROR",
+                path: "/",
+                statusCode: response.statusCode,
+                duration: Date().timeIntervalSince(startedAt)
+            )
+            requestLogger(log)
+            try? await send(response.serialized(), on: connection)
+        }
+
+        connection.cancel()
+    }
+
+    private func readCompleteRequest(from connection: NWConnection, accumulated: Data = Data()) async throws -> Data {
+        let chunk = try await receiveChunk(from: connection)
+        let combined = accumulated + chunk.data
+
+        if LocalAPIHTTPParser.isCompleteRequest(combined) || chunk.isComplete {
+            return combined
+        }
+
+        return try await readCompleteRequest(from: connection, accumulated: combined)
+    }
+
+    private func receiveChunk(from connection: NWConnection) async throws -> (data: Data, isComplete: Bool) {
+        try await withCheckedThrowingContinuation { continuation in
+            connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in
+                if let error {
+                    continuation.resume(throwing: error)
+                    return
+                }
+
+                continuation.resume(returning: (data ?? Data(), isComplete))
+            }
+        }
+    }
+
+    private func send(_ data: Data, on connection: NWConnection) async throws {
+        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
+            connection.send(content: data, completion: .contentProcessed { error in
+                if let error {
+                    continuation.resume(throwing: error)
+                } else {
+                    continuation.resume(returning: ())
+                }
+            })
+        }
+    }
+}
+
+private struct LocalAPIRequestHandler {
+    private let encoder: JSONEncoder = {
+        let encoder = JSONEncoder()
+        encoder.dateEncodingStrategy = .iso8601
+        encoder.outputFormatting = [.sortedKeys]
+        return encoder
+    }()
+
+    private let decoder: JSONDecoder = {
+        let decoder = JSONDecoder()
+        decoder.dateDecodingStrategy = .iso8601
+        return decoder
+    }()
+
+    private let inspectionService = DomainInspectionService()
+    private let reportBuilder = DomainReportBuilder()
+
+    func handle(request: LocalAPIHTTPRequest, expectedToken: String) async -> LocalAPIHTTPResponse {
+        guard isAuthorized(request: request, expectedToken: expectedToken) else {
+            return .error(statusCode: 401, code: "unauthorized", message: "A valid local API token is required.")
+        }
+
+        if request.method == "GET", request.pathComponents == ["portfolio"] {
+            return successResponse(PortfolioPayload(summary: buildPortfolioSummary()))
+        }
+
+        if request.method == "GET", request.pathComponents == ["domains"] {
+            return successResponse(DomainListPayload(domains: DomainDataPortabilityService.loadTrackedDomains()))
+        }
+
+        if request.method == "GET",
+           request.pathComponents.count == 2,
+           request.pathComponents.first == "domains" {
+            let domain = request.pathComponents[1]
+            return domainDetailsResponse(for: domain)
+        }
+
+        if request.method == "GET",
+           request.pathComponents.count == 3,
+           request.pathComponents.first == "domains",
+           request.pathComponents.last == "history" {
+            let domain = request.pathComponents[1]
+            return successResponse(DomainHistoryPayload(domain: normalizedDomain(domain), history: historyEntries(for: domain)))
+        }
+
+        if request.method == "GET", request.pathComponents == ["events"] {
+            return successResponse(RecentEventsPayload(events: recentEvents()))
+        }
+
+        if request.method == "GET", request.pathComponents == ["monitoring"] {
+            return successResponse(monitoringPayload())
+        }
+
+        if request.method == "POST", request.pathComponents == ["inspect"] {
+            return await inspectBodyResponse(request.body)
+        }
+
+        if request.method == "POST",
+           request.pathComponents.count == 2,
+           request.pathComponents.first == "inspect" {
+            let domain = request.pathComponents[1]
+            return await inspectResponse(for: domain)
+        }
+
+        if request.method == "POST",
+           request.pathComponents.count == 3,
+           request.pathComponents.first == "monitoring",
+           request.pathComponents.last == "enable" {
+            let domain = request.pathComponents[1]
+            return await setMonitoring(enabled: true, for: domain)
+        }
+
+        if request.method == "POST",
+           request.pathComponents.count == 3,
+           request.pathComponents.first == "monitoring",
+           request.pathComponents.last == "disable" {
+            let domain = request.pathComponents[1]
+            return await setMonitoring(enabled: false, for: domain)
+        }
+
+        return .error(statusCode: 404, code: "not_found", message: "The requested Local API route does not exist.")
+    }
+
+    private func isAuthorized(request: LocalAPIHTTPRequest, expectedToken: String) -> Bool {
+        guard !expectedToken.isEmpty else { return false }
+
+        if request.headers["x-api-token"] == expectedToken {
+            return true
+        }
+
+        if let authorization = request.headers["authorization"],
+           authorization == "Bearer \(expectedToken)" {
+            return true
+        }
+
+        return false
+    }
+
+    private func successResponse<Value: Encodable>(_ value: Value) -> LocalAPIHTTPResponse {
+        let envelope = LocalAPIEnvelope(success: true, data: value, error: nil, version: localAPIVersion)
+        guard let body = try? encoder.encode(envelope) else {
+            return .error(statusCode: 500, code: "encoding_failed", message: "Could not encode the Local API response.")
+        }
+
+        return LocalAPIHTTPResponse(statusCode: 200, body: body)
+    }
+
+    private func domainDetailsResponse(for domain: String) -> LocalAPIHTTPResponse {
+        let normalized = normalizedDomain(domain)
+        let trackedDomain = DomainDataPortabilityService.loadTrackedDomains().first {
+            $0.domain.caseInsensitiveCompare(normalized) == .orderedSame
+        }
+        let history = historyEntries(for: normalized)
+        let latestEntry = history.first
+
+        guard trackedDomain != nil || latestEntry != nil else {
+            return .error(statusCode: 404, code: "domain_not_found", message: "No local data exists for \(normalized).")
+        }
+
+        let latestReport = latestEntry.map { buildReport(for: $0, from: history) }
+        return successResponse(
+            DomainDetailPayload(
+                domain: normalized,
+                trackedDomain: trackedDomain,
+                latestReport: latestReport
+            )
+        )
+    }
+
+    private func inspectBodyResponse(_ body: Data) async -> LocalAPIHTTPResponse {
+        guard let payload = try? decoder.decode(InspectRequestPayload.self, from: body) else {
+            return .error(statusCode: 400, code: "invalid_body", message: "Expected JSON body: {\"domain\":\"example.com\"}.")
+        }
+
+        return await inspectResponse(for: payload.domain)
+    }
+
+    private func inspectResponse(for domain: String) async -> LocalAPIHTTPResponse {
+        let normalized = normalizedDomain(domain)
+        guard !normalized.isEmpty else {
+            return .error(statusCode: 400, code: "invalid_domain", message: "A valid domain is required.")
+        }
+
+        let previousSnapshot = latestHistoryEntry(for: normalized)?.snapshot
+        let report = await inspectionService.inspect(domain: normalized, previousSnapshot: previousSnapshot)
+        return successResponse(InspectResponsePayload(report: report))
+    }
+
+    private func setMonitoring(enabled: Bool, for domain: String) async -> LocalAPIHTTPResponse {
+        let normalized = normalizedDomain(domain)
+        guard !normalized.isEmpty else {
+            return .error(statusCode: 400, code: "invalid_domain", message: "A valid domain is required.")
+        }
+
+        var trackedDomains = DomainDataPortabilityService.loadTrackedDomains()
+        guard let index = trackedDomains.firstIndex(where: { $0.domain.caseInsensitiveCompare(normalized) == .orderedSame }) else {
+            return .error(statusCode: 404, code: "domain_not_found", message: "Tracked domain \(normalized) was not found.")
+        }
+
+        trackedDomains[index].monitoringEnabled = enabled
+        trackedDomains[index].updatedAt = Date()
+        DomainDataPortabilityService.saveTrackedDomains(trackedDomains)
+
+        let sanitizedSettings = MonitoringStorage.sanitizeSettings(
+            MonitoringStorage.loadSettings(),
+            trackedDomains: trackedDomains
+        )
+        MonitoringStorage.saveSettings(sanitizedSettings)
+
+        await MainActor.run {
+            _ = DomainMonitoringScheduler.shared.syncSchedule()
+        }
+
+        return successResponse(
+            MonitoringMutationPayload(
+                domain: trackedDomains[index].domain,
+                monitoringEnabled: trackedDomains[index].monitoringEnabled
+            )
+        )
+    }
+
+    private func buildPortfolioSummary() -> PortfolioSummary {
+        let trackedDomains = DomainDataPortabilityService.loadTrackedDomains()
+        let history = DomainDataPortabilityService.loadHistoryEntries()
+
+        var healthyCount = 0
+        var warningCount = 0
+        var criticalCount = 0
+        var changedLast24h = 0
+        var unreachableCount = 0
+
+        for trackedDomain in trackedDomains {
+            guard let latestEntry = latestHistoryEntry(for: trackedDomain.domain, history: history) else { continue }
+            let report = buildReport(for: latestEntry, from: history)
+
+            switch report.health {
+            case .healthy:
+                healthyCount += 1
+            case .warning:
+                warningCount += 1
+            case .critical:
+                criticalCount += 1
+            }
+
+            if latestEntry.changeSummary?.hasChanges == true,
+               Date().timeIntervalSince(latestEntry.timestamp) <= 24 * 60 * 60 {
+                changedLast24h += 1
+            }
+
+            if report.network.reachabilityError != nil || report.lastMonitoringFailure != nil {
+                unreachableCount += 1
+            }
+        }
+
+        return PortfolioSummary(
+            totalDomains: trackedDomains.count,
+            healthyCount: healthyCount,
+            warningCount: warningCount,
+            criticalCount: criticalCount,
+            changedLast24h: changedLast24h,
+            expiringSoonCount: trackedDomains.filter { $0.certificateWarningLevel != .none }.count,
+            unreachableCount: unreachableCount
+        )
+    }
+
+    private func monitoringPayload() -> MonitoringPayload {
+        let trackedDomains = DomainDataPortabilityService.loadTrackedDomains()
+        let settings = MonitoringStorage.sanitizeSettings(MonitoringStorage.loadSettings(), trackedDomains: trackedDomains)
+
+        return MonitoringPayload(
+            isEnabled: settings.isEnabled,
+            scope: settings.scope,
+            alertsEnabled: settings.alertsEnabled,
+            monitoredDomains: trackedDomains.map {
+                MonitoringDomainPayload(
+                    domain: $0.domain,
+                    monitoringEnabled: $0.monitoringEnabled,
+                    lastMonitoredAt: $0.lastMonitoredAt,
+                    lastAlertAt: $0.lastAlertAt,
+                    certificateWarningLevel: $0.certificateWarningLevel
+                )
+            }
+        )
+    }
+
+    private func recentEvents() -> [RecentEventPayload] {
+        MonitoringStorage.loadLogs()
+            .prefix(25)
+            .flatMap { log in
+                log.checkedDomains.map { result in
+                    RecentEventPayload(
+                        timestamp: result.checkedAt,
+                        domain: result.domain,
+                        summary: result.summaryMessage,
+                        status: result.errorMessage == nil ? "ok" : "error",
+                        severity: result.alertSeverity?.title ?? "Info"
+                    )
+                }
+            }
+    }
+
+    private func historyEntries(for domain: String) -> [HistoryEntry] {
+        let normalized = normalizedDomain(domain)
+        return DomainDataPortabilityService.loadHistoryEntries().filter {
+            $0.domain.caseInsensitiveCompare(normalized) == .orderedSame
+        }
+    }
+
+    private func latestHistoryEntry(for domain: String, history: [HistoryEntry]? = nil) -> HistoryEntry? {
+        let entries = history ?? DomainDataPortabilityService.loadHistoryEntries()
+        let normalized = normalizedDomain(domain)
+        return entries.first { $0.domain.caseInsensitiveCompare(normalized) == .orderedSame }
+    }
+
+    private func buildReport(for entry: HistoryEntry, from history: [HistoryEntry]) -> DomainReport {
+        let previousSnapshot = history
+            .dropFirst(history.firstIndex(where: { $0.id == entry.id }).map { $0 + 1 } ?? history.count)
+            .first { $0.domain.caseInsensitiveCompare(entry.domain) == .orderedSame }?
+            .snapshot
+        return reportBuilder.build(from: entry, previousSnapshot: previousSnapshot)
+    }
+
+    private func normalizedDomain(_ value: String) -> String {
+        value
+            .trimmingCharacters(in: .whitespacesAndNewlines)
+            .replacingOccurrences(of: "https://", with: "")
+            .replacingOccurrences(of: "http://", with: "")
+            .components(separatedBy: "/")
+            .first?
+            .lowercased() ?? ""
+    }
+}
+
+private struct LocalAPIHTTPRequest {
+    let method: String
+    let path: String
+    let queryItems: [URLQueryItem]
+    let headers: [String: String]
+    let body: Data
+
+    var pathComponents: [String] {
+        path.split(separator: "/").map(String.init)
+    }
+
+    var pathWithQuery: String {
+        guard !queryItems.isEmpty else { return path }
+        let query = queryItems.compactMap { item in
+            guard let value = item.value else { return item.name }
+            return "\(item.name)=\(value)"
+        }
+        .joined(separator: "&")
+        return "\(path)?\(query)"
+    }
+}
+
+private struct LocalAPIHTTPResponse {
+    let statusCode: Int
+    let body: Data
+
+    init(statusCode: Int, body: Data) {
+        self.statusCode = statusCode
+        self.body = body
+    }
+
+    static func error(statusCode: Int, code: String, message: String) -> LocalAPIHTTPResponse {
+        let payload = LocalAPIEnvelope<EmptyPayload>(
+            success: false,
+            data: nil,
+            error: LocalAPIErrorPayload(code: code, message: message),
+            version: localAPIVersion
+        )
+
+        let encoder = JSONEncoder()
+        encoder.outputFormatting = [.sortedKeys]
+        let body = (try? encoder.encode(payload)) ?? Data()
+        return LocalAPIHTTPResponse(statusCode: statusCode, body: body)
+    }
+
+    func serialized() -> Data {
+        var response = Data()
+        response.append("HTTP/1.1 \(statusCode) \(Self.reasonPhrase(for: statusCode))\r\n".data(using: .utf8) ?? Data())
+        response.append("Content-Type: application/json\r\n".data(using: .utf8) ?? Data())
+        response.append("Content-Length: \(body.count)\r\n".data(using: .utf8) ?? Data())
+        response.append("Connection: close\r\n\r\n".data(using: .utf8) ?? Data())
+        response.append(body)
+        return response
+    }
+
+    private static func reasonPhrase(for statusCode: Int) -> String {
+        switch statusCode {
+        case 200:
+            return "OK"
+        case 400:
+            return "Bad Request"
+        case 401:
+            return "Unauthorized"
+        case 404:
+            return "Not Found"
+        default:
+            return "Internal Server Error"
+        }
+    }
+}
+
+private enum LocalAPIHTTPParser {
+    enum ParserError: LocalizedError {
+        case invalidRequestLine
+        case invalidPath
+
+        var errorDescription: String? {
+            switch self {
+            case .invalidRequestLine:
+                return "The HTTP request line is invalid."
+            case .invalidPath:
+                return "The HTTP request path is invalid."
+            }
+        }
+    }
+
+    static func isCompleteRequest(_ data: Data) -> Bool {
+        guard let separatorRange = data.range(of: Data("\r\n\r\n".utf8)) else {
+            return false
+        }
+
+        let headerData = data[..<separatorRange.lowerBound]
+        guard let headerString = String(data: headerData, encoding: .utf8) else {
+            return false
+        }
+
+        let contentLength = headerString
+            .split(separator: "\r\n")
+            .first { $0.lowercased().hasPrefix("content-length:") }
+            .flatMap { Int($0.split(separator: ":", maxSplits: 1).last?.trimmingCharacters(in: .whitespaces) ?? "") }
+            ?? 0
+
+        let bodyLength = data.count - separatorRange.upperBound
+        return bodyLength >= contentLength
+    }
+
+    static func parseRequest(from data: Data) throws -> LocalAPIHTTPRequest {
+        guard let separatorRange = data.range(of: Data("\r\n\r\n".utf8)) else {
+            throw ParserError.invalidRequestLine
+        }
+
+        let headerData = data[..<separatorRange.lowerBound]
+        let bodyData = data[separatorRange.upperBound...]
+
+        guard let headerString = String(data: headerData, encoding: .utf8) else {
+            throw ParserError.invalidRequestLine
+        }
+
+        let lines = headerString.split(separator: "\r\n", omittingEmptySubsequences: false)
+        guard let requestLine = lines.first else {
+            throw ParserError.invalidRequestLine
+        }
+
+        let requestParts = requestLine.split(separator: " ")
+        guard requestParts.count >= 2 else {
+            throw ParserError.invalidRequestLine
+        }
+
+        let method = String(requestParts[0]).uppercased()
+        let rawPath = String(requestParts[1])
+        guard let components = URLComponents(string: rawPath) else {
+            throw ParserError.invalidPath
+        }
+
+        var headers: [String: String] = [:]
+        for line in lines.dropFirst() {
+            let parts = line.split(separator: ":", maxSplits: 1)
+            guard parts.count == 2 else { continue }
+            headers[String(parts[0]).lowercased()] = String(parts[1]).trimmingCharacters(in: .whitespaces)
+        }
+
+        return LocalAPIHTTPRequest(
+            method: method,
+            path: components.path,
+            queryItems: components.queryItems ?? [],
+            headers: headers,
+            body: Data(bodyData)
+        )
+    }
+}
+
+private struct LocalAPIEnvelope<DataPayload: Encodable>: Encodable {
+    let success: Bool
+    let data: DataPayload?
+    let error: LocalAPIErrorPayload?
+    let version: String
+}
+
+private struct LocalAPIErrorPayload: Encodable {
+    let code: String
+    let message: String
+}
+
+private struct EmptyPayload: Encodable {}
+
+private struct PortfolioPayload: Encodable {
+    let summary: PortfolioSummary
+}
+
+private struct PortfolioSummary: Encodable {
+    let totalDomains: Int
+    let healthyCount: Int
+    let warningCount: Int
+    let criticalCount: Int
+    let changedLast24h: Int
+    let expiringSoonCount: Int
+    let unreachableCount: Int
+}
+
+private struct DomainListPayload: Encodable {
+    let domains: [TrackedDomain]
+}
+
+private struct DomainDetailPayload: Encodable {
+    let domain: String
+    let trackedDomain: TrackedDomain?
+    let latestReport: DomainReport?
+}
+
+private struct DomainHistoryPayload: Encodable {
+    let domain: String
+    let history: [HistoryEntry]
+}
+
+private struct RecentEventsPayload: Encodable {
+    let events: [RecentEventPayload]
+}
+
+private struct RecentEventPayload: Encodable {
+    let timestamp: Date
+    let domain: String
+    let summary: String
+    let status: String
+    let severity: String
+}
+
+private struct MonitoringPayload: Encodable {
+    let isEnabled: Bool
+    let scope: MonitoringScope
+    let alertsEnabled: Bool
+    let monitoredDomains: [MonitoringDomainPayload]
+}
+
+private struct MonitoringDomainPayload: Encodable {
+    let domain: String
+    let monitoringEnabled: Bool
+    let lastMonitoredAt: Date?
+    let lastAlertAt: Date?
+    let certificateWarningLevel: CertificateWarningLevel
+}
+
+private struct MonitoringMutationPayload: Encodable {
+    let domain: String
+    let monitoringEnabled: Bool
+}
+
+private struct InspectRequestPayload: Decodable {
+    let domain: String
+}
+
+private struct InspectResponsePayload: Encodable {
+    let report: DomainReport
+}