feat: authorize iOS Hermes accounts and secure session isolation

This commit is contained in:
snowspeeder committed 2026-10-01 12:57:57 -04:00
1 parent 9bdaea2408
commit bebaf59466
23 files changed
+911 -185

No files matched your search

@@ -2,6 +2,7 @@ import SwiftUI
struct ConnectionTestView: View {
@ObservedObject var settings: SettingsStore
var bearerToken: String = ""
@State private var isBusy = false
@State private var result: Result<TokenResponse, Error>?
@State private var urlError: String?
@@ -67,7 +68,7 @@ struct ConnectionTestView: View {
case .success(let config):
isBusy = true
Task {
let client = TokenClient(config: config)
let client = TokenClient(config: config, bearerToken: bearerToken)
do {
let response = try await client.fetchToken()
result = .success(response)
+2 -63
View File
@@ -13,7 +13,6 @@ struct SettingsView: View {
@State private var urlError: String?
@State private var showVoicePicker = false
@State private var savedURLConfirmation: String?
@State private var showModelPicker = false
@FocusState private var urlFocused: Bool
init(settings: SettingsStore, session: VoiceSession) {
self.settings = settings
@@ -46,19 +45,6 @@ struct SettingsView: View {
} footer: {
Text("Voice and typing preferences are saved on this iPhone. You can switch modes from the Talk tab.")
}
Section {
if session.isReady, !session.models.isEmpty {
Button {
showModelPicker = true
} label: {
LabeledContent("Model", value: session.selectedModel ?? settings.modelID ?? "Server default")
}
} else {
LabeledContent("Model", value: settings.modelID ?? "Server default")
Text("Connect from Talk to see the models available on your server.")
.font(.footnote).foregroundStyle(Nearlight.quiet)
}
} header: { Text("Intelligence") }
Section {
Text("Server URL")
.font(.footnote.weight(.semibold))
@@ -106,7 +92,7 @@ struct SettingsView: View {
.foregroundStyle(Nearlight.quiet)
}
NavigationLink("Connection test") {
ConnectionTestView(settings: settings)
ConnectionTestView(settings: settings, bearerToken: session.bootstrapBearerToken)
}
} header: {
Text("Connection")
@@ -120,9 +106,7 @@ struct SettingsView: View {
.sheet(isPresented: $showVoicePicker) {
VoicePickerView(settings: settings, session: session)
}
.sheet(isPresented: $showModelPicker) {
ModelPickerView(settings: settings, session: session)
}
}
/// Validates the draft and persists it only on success. On failure the
@@ -190,51 +174,6 @@ struct VoicePickerView: View {
}
}
struct ModelPickerView: View {
@ObservedObject var settings: SettingsStore
@ObservedObject var session: VoiceSession
@Environment(\.dismiss) private var dismiss
private var activeModelID: String? {
if !session.models.isEmpty,
let selected = session.selectedModel ?? settings.modelID,
session.models.contains(where: { $0.id == selected }) {
return selected
}
return session.models.first?.id
}
var body: some View {
NavigationStack {
List {
ForEach(session.models, id: \.id) { model in
Button {
settings.modelID = model.id
Task { await session.selectModel(model.id) }
dismiss()
} label: {
HStack {
Text(model.name ?? model.id)
if activeModelID == model.id {
Spacer()
Image(systemName: "checkmark")
.foregroundStyle(Nearlight.rosewood)
}
}
}
.accessibilityAddTraits(activeModelID == model.id ? .isSelected : [])
}
}
.navigationTitle("Model")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
}
}
}
}
#Preview {
let previewSettings = SettingsStore()
guard case .success(let previewConfig) = ServerConfig.parse(SettingsStore.defaultServerURL) else {