-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathAppDataWipe.swift
More file actions
112 lines (95 loc) · 4.81 KB
/
Copy pathAppDataWipe.swift
File metadata and controls
112 lines (95 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import Foundation
import Security
/// One-shot total-wipe helper, called from MainView's logout path. Leaves the
/// app in the same state as a fresh install: no keypair, no UserDefaults, no
/// keychain items, no on-disk caches, no in-memory singletons.
///
/// Order matters here: live state (sockets, in-memory caches) is torn down
/// first so nothing tries to write back to ObjectBox / UserDefaults while we're
/// emptying them. Then we wipe persisted state from biggest blast-radius (the
/// keychain catch-all) down to per-pubkey UserDefaults entries.
///
/// We deliberately do NOT delete the ObjectBox store directory — the singleton
/// holds a force-unwrapped `Store!` initialized in `wispApp.init`, and there is
/// no clean way to tear it down + re-init mid-process. Emptying every box has
/// the same observable effect from the user's perspective.
@MainActor
enum AppDataWipe {
static func wipeEverything() async {
// 1. Quiesce live workers so they stop touching state we're about to delete.
RelayPool.authSigner = nil
RelayPool.authApprovalCheck = nil
await GroupRelayPool.shared.shutdownAll()
// 2. In-memory singletons — match the existing logout flow plus the wallet store.
EngagementRepository.shared.clear()
PollTallyRepository.shared.clear()
EmojiRepository.shared.clear()
NoteSourceTracker.shared.clear()
LiveStreamRepository.shared.clear()
LivePlayerStore.shared.releaseAll()
MuteRepository.shared.unbind()
SafetyPreferences.shared.unbind()
ZapAnimationStore.shared.cancelAll()
MissingProfileWatcher.shared.stop()
// 3. Empty the ObjectBox boxes. Both stores are actors; awaits serialise
// against any in-flight persists from the same isolation domain.
await EventStore.shared.removeAll()
await GroupStore.shared.removeAll()
await DmStore.shared.removeAll()
await ExtendedNetworkRepository.shared.unbind()
await SafetyFilter.shared.rebuildSnapshot()
await SpamScorer.shared.clearCache()
// 4. On-disk state under Application Support (per-account SQLite + Spark wallet
// storage) and the wisp-prefixed cache directories.
wipeFilesystem()
// 5. Keychain. A single SecItemDelete on the service nukes every account
// we own — `active`, `account_<pubkey>`, `nwc_<pubkey>`, `spark_seed_<pubkey>`.
wipeKeychain()
// 6. UserDefaults. removePersistentDomain wipes everything in the standard
// suite for this bundle id; no per-key allow-list to maintain.
wipeUserDefaults()
// 7. URLCache for any image/data fetches that bypassed our own caches.
URLCache.shared.removeAllCachedResponses()
}
// MARK: - Filesystem
private static func wipeFilesystem() {
let fm = FileManager.default
// Application Support/wisp/{social_graph_*.db, spark_data/}.
// Leave the objectbox/ subdir alone — its store handle is still live.
if let support = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
let wispDir = support.appendingPathComponent("wisp", isDirectory: true)
if let entries = try? fm.contentsOfDirectory(at: wispDir, includingPropertiesForKeys: nil) {
for entry in entries {
let name = entry.lastPathComponent
if name == "objectbox" { continue }
try? fm.removeItem(at: entry)
}
}
}
// Caches/{wisp_avatars, wisp_emojis}. Image data, no other implications.
if let caches = fm.urls(for: .cachesDirectory, in: .userDomainMask).first {
try? fm.removeItem(at: caches.appendingPathComponent("wisp_avatars", isDirectory: true))
try? fm.removeItem(at: caches.appendingPathComponent("wisp_emojis", isDirectory: true))
}
}
// MARK: - Keychain
private static func wipeKeychain() {
// Service-level wipe: matches NostrKey.service ("com.wisp.nostr") which
// WalletKeychain also uses, so this catches NostrKey + NWC URI + Spark seed.
for service in ["com.wisp.nostr"] {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service
]
SecItemDelete(query as CFDictionary)
}
}
// MARK: - UserDefaults
private static func wipeUserDefaults() {
guard let bundleId = Bundle.main.bundleIdentifier else { return }
UserDefaults.standard.removePersistentDomain(forName: bundleId)
// App-group / shared suites are also wiped if they were registered — none
// exist in this codebase today, but if they're added later they should
// join this list.
}
}