TeraSessionRemoval.swift (4289B)
1 import Foundation 2 3 extension TeraSessionStore { 4 func applySettingsReconfiguration() async -> TeraSessionPhase { 5 guard !removalInProgress else { return phase } 6 generation = generation.invalidated() 7 let requestedGeneration = generation 8 phase = .starting 9 do { 10 let identity = try await identityStore.loadAndMigrate() 11 try ensureCurrent(requestedGeneration) 12 guard identity.state == .unlocked else { 13 return await start() 14 } 15 let configuration = try await configurationStore.load() 16 try ensureCurrent(requestedGeneration) 17 phase = try await startRuntime( 18 configuration: configuration, 19 identity: identity, 20 generation: requestedGeneration.requireActive(), 21 forceReconfiguration: true, 22 adoptBootstrapSettings: false 23 ) 24 } catch { 25 guard generation == requestedGeneration else { return phase } 26 phase = .failed( 27 .local( 28 operation: "session.settings_reconfiguration", 29 code: "ios.session.settings_reconfiguration_failed", 30 safeMessage: "Tera could not apply the saved settings." 31 ) 32 ) 33 } 34 return phase 35 } 36 37 func keyRemovalPage(author: String, cursor: String?) async throws -> TeraLegacyDraftPage { 38 guard !removalInProgress, case let .running(snapshot) = phase, 39 snapshot.identity.publicKeyHex == author else { throw TeraIdentityStoreError.unavailable } 40 let page = try await runtimeClient.legacyDraftPage(limit: 50, cursor: cursor) 41 guard page.authorPublicKey == author else { throw TeraIdentityStoreError.unavailable } 42 return page 43 } 44 45 func removeSigningKey(author: String, requests: [TeraKeyRemovalRequest]) async -> TeraSessionPhase { 46 guard !removalInProgress, requests.count <= 100, 47 case let .running(snapshot) = phase, snapshot.identity.publicKeyHex == author 48 else { return phase } 49 removalInProgress = true 50 defer { removalInProgress = false } 51 generation = generation.invalidated() 52 let requestedGeneration = generation 53 do { 54 try ensureCurrent(requestedGeneration) 55 let identity = try await identityStore.loadAndMigrate() 56 guard identity.state == .unlocked, identity.publicKeyHex == author else { 57 throw TeraIdentityStoreError.unavailable 58 } 59 for request in requests { 60 try ensureCurrent(requestedGeneration) 61 try await runtimeClient.prepareRetractionForKeyRemoval(request) 62 } 63 try ensureCurrent(requestedGeneration) 64 _ = try await runtimeClient.stop() 65 try ensureCurrent(requestedGeneration) 66 let removed = try await identityStore.removeSigningKey(expected: identity) 67 // Do not discard an actual completed deletion because cancellation or 68 // lifecycle invalidation arrived after the last reversible boundary. 69 if removed.state == .absent { 70 phase = .identityRequired 71 } 72 } catch { 73 // All local data and already signed requests remain with their owner. 74 // Re-read custody on retry, including a recoverable partial transaction. 75 guard generation == requestedGeneration else { return phase } 76 phase = .failed(.local(operation: "identity.key_removal", code: "ios.identity.key_removal_incomplete", 77 safeMessage: "Key removal did not finish. Local data and any signed requests are retained. Retry to check identity recovery before continuing.")) 78 } 79 return phase 80 } 81 82 func stop() async -> TeraSessionPhase { 83 generation = generation.invalidated() 84 let requestedGeneration = generation 85 do { 86 _ = try await runtimeClient.stop() 87 try ensureCurrent(requestedGeneration) 88 await identityStore.lock() 89 try ensureCurrent(requestedGeneration) 90 try qualificationEvidenceStore?.cleanup() 91 phase = .stopped 92 } catch let TeraRuntimeClientError.shutdown(failure) { 93 guard isCurrent(requestedGeneration) else { return phase } 94 phase = .failed(failure) 95 } catch { 96 guard isCurrent(requestedGeneration) else { return phase } 97 phase = .failed( 98 .local( 99 operation: "session.stop", 100 code: "ios.session.stop_failed", 101 safeMessage: "Tera could not finish shutting down." 102 ) 103 ) 104 } 105 return phase 106 } 107 }