summaryrefslogtreecommitdiff
path: root/DomainDig
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-26 00:22:01 -0500
committerChristian Cleberg <[email protected]>2026-04-26 00:22:01 -0500
commitcc69cbd7e589ec4b065ce74d8c5e7714a040cfc5 (patch)
tree653eda7655bba21df43f4039915d452110591094 /DomainDig
parent535bb0ff0f64d57be1074e33ceb42f598f80205b (diff)
downloaddomain-dig-cc69cbd7e589ec4b065ce74d8c5e7714a040cfc5.tar.gz
domain-dig-cc69cbd7e589ec4b065ce74d8c5e7714a040cfc5.tar.bz2
domain-dig-cc69cbd7e589ec4b065ce74d8c5e7714a040cfc5.zip
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.
Diffstat (limited to 'DomainDig')
-rw-r--r--DomainDig/ContentView.swift121
-rw-r--r--DomainDig/DataResetService.swift3
-rw-r--r--DomainDig/DomainDigApp.swift4
3 files changed, 127 insertions, 1 deletions
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()