@@ -38,7 +38,9 @@ final class KeychainSecureStore: SecureStore {
3838 cachedAccessGroup = resolved
3939 return resolved
4040 }
41- let fallback = " D7HJ5TFYCU.com.TablePro.shared "
41+ // Use non-shared access group as last resort — credentials won't sync
42+ // across devices but the app still functions
43+ let fallback = " com.TablePro.shared "
4244 cachedAccessGroup = fallback
4345 return fallback
4446 }
@@ -115,6 +117,34 @@ final class KeychainSecureStore: SecureStore {
115117 }
116118 }
117119
120+ /// Remove orphaned test connection credentials that may remain after a SIGKILL.
121+ /// Test credentials use temp UUIDs not associated with any saved connection.
122+ func cleanOrphanedCredentials( validConnectionIds: Set < UUID > ) {
123+ let prefixes = [ " com.TablePro.password. " , " com.TablePro.sshpassword. " , " com.TablePro.keypassphrase. " ]
124+ let query : [ String : Any ] = [
125+ kSecClass as String : kSecClassGenericPassword,
126+ kSecAttrService as String : serviceName,
127+ kSecAttrAccessGroup as String : accessGroup,
128+ kSecReturnAttributes as String : true ,
129+ kSecMatchLimit as String : kSecMatchLimitAll,
130+ kSecAttrSynchronizable as String : kSecAttrSynchronizableAny,
131+ kSecUseDataProtectionKeychain as String : true ,
132+ ]
133+ var result : AnyObject ?
134+ guard SecItemCopyMatching ( query as CFDictionary , & result) == errSecSuccess,
135+ let items = result as? [ [ String : Any ] ] else { return }
136+
137+ for item in items {
138+ guard let account = item [ kSecAttrAccount as String ] as? String else { continue }
139+ for prefix in prefixes {
140+ guard account. hasPrefix ( prefix) else { continue }
141+ let uuidString = String ( account. dropFirst ( prefix. count) )
142+ guard let uuid = UUID ( uuidString: uuidString) ,
143+ !validConnectionIds. contains ( uuid) else { continue }
144+ try ? delete ( forKey: account)
145+ }
146+ }
147+ }
118148}
119149
120150enum KeychainError : Error , LocalizedError {
0 commit comments