Pending request view (#821)

* Splitting out auth context stuff in preparation for batch

* Add uncommitted authhandler rename

* Split out auth/nonauth paths

* Messy auth batching infra WIP

* Restrict connectionAcceptedNotifications to creating filehandle

* WIP

* WIP

* WIP

* WIP

* WIP

* Reenable multilineinfoview

* WIP

* WIP

* Fixing up tests

* WIP

* Return empty bind response on parse throw

* JSON project

* JSON Project

* WIP

* WIP

* Almost done

* Tests

* Cleanup test calls

* Fixme

* Cleanup

* Localized strings
This commit is contained in:
Max Goedjen
2026-09-20 19:22:52 -07:00
committed by GitHub
parent 43ee687232
commit 54e0d00d87
34 changed files with 1390 additions and 527 deletions
+21 -30
View File
@@ -5,6 +5,8 @@ import SecretKit
import SecretAgentKit
import Brief
typealias PersistAction = (@Sendable (AnySecret, TimeInterval) async throws -> Void)
final class Notifier: Sendable {
private let notificationDelegate = NotificationDelegate()
@@ -15,6 +17,12 @@ final class Notifier: Sendable {
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate
}
func registerPersistenceHandler(action: @escaping PersistAction) async {
let rawDurations = [
Measurement(value: 1, unit: UnitDuration.minutes),
Measurement(value: 5, unit: UnitDuration.minutes),
@@ -24,11 +32,9 @@ final class Notifier: Sendable {
let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: .persistAuthenticationDeclineButton), options: [])
var allPersistenceActions = [doNotPersistAction]
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
var identifiers: [String: TimeInterval] = [:]
for duration in rawDurations {
let seconds = duration.converted(to: .seconds).value
@@ -43,16 +49,11 @@ final class Notifier: Sendable {
if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) {
persistAuthenticationCategory.setValue(String(localized: .persistAuthenticationAcceptButton), forKey: "_actionsMenuTitle")
}
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate
Task {
await notificationDelegate.state.setPersistenceState(options: identifiers) { secret, store, duration in
guard let duration = duration else { return }
try? await store.persistAuthentication(secret: secret, forDuration: duration)
}
}
var categories = await UNUserNotificationCenter.current().notificationCategories()
categories.insert(persistAuthenticationCategory)
UNUserNotificationCenter.current().setNotificationCategories(categories)
await notificationDelegate.state.setPersistenceState(options: identifiers, action: action)
}
func prompt() {
@@ -60,7 +61,7 @@ final class Notifier: Sendable {
notificationCenter.requestAuthorization(options: .alert) { _, _ in }
}
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?) async {
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?, offerPersistence: Bool) async {
await notificationDelegate.state.setPending(secret: secret, store: store)
let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent()
@@ -76,7 +77,7 @@ final class Notifier: Sendable {
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
notificationContent.interruptionLevel = .timeSensitive
if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.authenticationRequirement.required {
if offerPersistence {
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
}
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
@@ -115,13 +116,8 @@ extension Notifier: SigningWitness {
) async throws {
}
func witness(
accessTo secret: AnySecret,
from store: AnySecretStore,
by provenance: SigningRequestProvenance,
target: SigningRequestTarget?
) async throws {
await notify(accessTo: secret, from: store, by: provenance, target: target)
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?, offerPersistence: Bool) async throws {
await notify(accessTo: secret, from: store, by: provenance, target: target, offerPersistence: offerPersistence)
}
}
@@ -150,28 +146,24 @@ extension Notifier {
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
fileprivate actor State {
typealias PersistAction = (@Sendable (AnySecret, AnySecretStore, TimeInterval?) async -> Void)
typealias IgnoreAction = (@Sendable (Release) async -> Void)
fileprivate var release: Release?
fileprivate var ignoreAction: IgnoreAction?
fileprivate var persistAction: PersistAction?
fileprivate var persistOptions: [String: TimeInterval] = [:]
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
func setPending(secret: AnySecret, store: AnySecretStore) {
pendingPersistableSecrets[secret.id.description] = secret
pendingPersistableStores[store.id.description] = store
}
func retrievePending(secretID: String, storeID: String, optionID: String) -> (AnySecret, AnySecretStore, TimeInterval)? {
func retrievePending(secretID: String, optionID: String) -> (AnySecret, TimeInterval)? {
guard let secret = pendingPersistableSecrets[secretID],
let store = pendingPersistableStores[storeID],
let options = persistOptions[optionID] else {
return nil
}
pendingPersistableSecrets.removeValue(forKey: secretID)
return (secret, store, options)
return (secret, options)
}
func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) {
@@ -219,13 +211,12 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se
}
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String,
let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String else {
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String else {
return
}
let optionID = response.actionIdentifier
guard let (secret, store, persistOptions) = await state.retrievePending(secretID: secretID, storeID: storeID, optionID: optionID) else { return }
await state.persistAction?(secret, store, persistOptions)
guard let (secret, persistOptions) = await state.retrievePending(secretID: secretID, optionID: optionID) else { return }
try? await state.persistAction?(secret, persistOptions)
}