signer-meta-handler.ts raw
1 /* eslint-disable @typescript-eslint/no-explicit-any */
2 import { Bookmark, EncryptedVault, SyncFlow, ExtensionSettings, VaultSnapshot } from './types';
3 import { v4 as uuidv4 } from 'uuid';
4
5 /**
6 * Handler for extension settings stored outside the encrypted vault.
7 * This includes sync preferences, backups, reckless mode, whitelisted hosts, etc.
8 */
9 export abstract class SignerMetaHandler {
10 get extensionSettings(): ExtensionSettings | undefined {
11 return this.#extensionSettings;
12 }
13
14 /** @deprecated Use extensionSettings instead */
15 get signerMetaData(): ExtensionSettings | undefined {
16 return this.#extensionSettings;
17 }
18
19 #extensionSettings?: ExtensionSettings;
20
21 readonly metaProperties = ['syncFlow', 'vaultSnapshots', 'maxBackups', 'recklessMode', 'whitelistedHosts', 'bookmarks', 'devMode'];
22 readonly DEFAULT_MAX_BACKUPS = 5;
23 /**
24 * Load the full data from the storage. If the storage is used for storing
25 * other data (e.g. browser sync data when the user decided to NOT sync),
26 * make sure to handle the "meta properties" to only load these.
27 *
28 * ATTENTION: Make sure to call "setFullData(..)" afterwards to update the in-memory data.
29 */
30 abstract loadFullData(): Promise<Partial<Record<string, any>>>;
31
32 setFullData(data: ExtensionSettings) {
33 this.#extensionSettings = data;
34 }
35
36 abstract saveFullData(data: ExtensionSettings): Promise<void>;
37
38 /**
39 * Sets the sync flow preference for the user and immediately saves it.
40 */
41 async setSyncFlow(flow: SyncFlow): Promise<void> {
42 if (!this.#extensionSettings) {
43 this.#extensionSettings = {
44 syncFlow: flow,
45 };
46 } else {
47 this.#extensionSettings.syncFlow = flow;
48 }
49
50 await this.saveFullData(this.#extensionSettings);
51 }
52
53 /** @deprecated Use setSyncFlow instead */
54 async setBrowserSyncFlow(flow: SyncFlow): Promise<void> {
55 return this.setSyncFlow(flow);
56 }
57
58 abstract clearData(keep: string[]): Promise<void>;
59
60 /**
61 * Sets the reckless mode and immediately saves it.
62 */
63 async setRecklessMode(enabled: boolean): Promise<void> {
64 if (!this.#extensionSettings) {
65 this.#extensionSettings = {
66 recklessMode: enabled,
67 };
68 } else {
69 this.#extensionSettings.recklessMode = enabled;
70 }
71
72 await this.saveFullData(this.#extensionSettings);
73 }
74
75 /**
76 * Sets dev mode and immediately saves it.
77 */
78 async setDevMode(enabled: boolean): Promise<void> {
79 if (!this.#extensionSettings) {
80 this.#extensionSettings = {
81 devMode: enabled,
82 };
83 } else {
84 this.#extensionSettings.devMode = enabled;
85 }
86
87 await this.saveFullData(this.#extensionSettings);
88 }
89
90 /**
91 * Adds a host to the whitelist and immediately saves it.
92 */
93 async addWhitelistedHost(host: string): Promise<void> {
94 if (!this.#extensionSettings) {
95 this.#extensionSettings = {
96 whitelistedHosts: [host],
97 };
98 } else {
99 const hosts = this.#extensionSettings.whitelistedHosts ?? [];
100 if (!hosts.includes(host)) {
101 hosts.push(host);
102 this.#extensionSettings.whitelistedHosts = hosts;
103 }
104 }
105
106 await this.saveFullData(this.#extensionSettings);
107 }
108
109 /**
110 * Removes a host from the whitelist and immediately saves it.
111 */
112 async removeWhitelistedHost(host: string): Promise<void> {
113 if (!this.#extensionSettings?.whitelistedHosts) {
114 return;
115 }
116
117 this.#extensionSettings.whitelistedHosts = this.#extensionSettings.whitelistedHosts.filter(
118 (h) => h !== host
119 );
120
121 await this.saveFullData(this.#extensionSettings);
122 }
123
124 /**
125 * Sets the bookmarks array and immediately saves it.
126 */
127 async setBookmarks(bookmarks: Bookmark[]): Promise<void> {
128 if (!this.#extensionSettings) {
129 this.#extensionSettings = {
130 bookmarks,
131 };
132 } else {
133 this.#extensionSettings.bookmarks = bookmarks;
134 }
135
136 await this.saveFullData(this.#extensionSettings);
137 }
138
139 /**
140 * Gets the current bookmarks.
141 */
142 getBookmarks(): Bookmark[] {
143 return this.#extensionSettings?.bookmarks ?? [];
144 }
145
146 /**
147 * Gets the maximum number of backups to keep.
148 */
149 getMaxBackups(): number {
150 return this.#extensionSettings?.maxBackups ?? this.DEFAULT_MAX_BACKUPS;
151 }
152
153 /**
154 * Sets the maximum number of backups to keep and immediately saves it.
155 */
156 async setMaxBackups(count: number): Promise<void> {
157 const clampedCount = Math.max(1, Math.min(20, count)); // Clamp between 1-20
158 if (!this.#extensionSettings) {
159 this.#extensionSettings = {
160 maxBackups: clampedCount,
161 };
162 } else {
163 this.#extensionSettings.maxBackups = clampedCount;
164 }
165
166 await this.saveFullData(this.#extensionSettings);
167 }
168
169 /**
170 * Gets all vault backups, sorted newest first.
171 */
172 getBackups(): VaultSnapshot[] {
173 const backups = this.#extensionSettings?.vaultSnapshots ?? [];
174 return [...backups].sort((a, b) =>
175 new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
176 );
177 }
178
179 /**
180 * Gets a specific backup by ID.
181 */
182 getBackupById(id: string): VaultSnapshot | undefined {
183 return this.#extensionSettings?.vaultSnapshots?.find(b => b.id === id);
184 }
185
186 /**
187 * Creates a new backup of the vault data.
188 * Automatically removes old backups if exceeding maxBackups.
189 */
190 async createBackup(
191 encryptedVault: EncryptedVault,
192 reason: 'manual' | 'auto' | 'pre-restore' = 'manual'
193 ): Promise<VaultSnapshot> {
194 const now = new Date();
195 const dateTimeString = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
196 const identityCount = encryptedVault.identities?.length ?? 0;
197
198 const snapshot: VaultSnapshot = {
199 id: uuidv4(),
200 fileName: `Vault Backup - ${dateTimeString}`,
201 createdAt: now.toISOString(),
202 data: JSON.parse(JSON.stringify(encryptedVault)), // Deep clone
203 identityCount,
204 reason,
205 };
206
207 if (!this.#extensionSettings) {
208 this.#extensionSettings = {
209 vaultSnapshots: [snapshot],
210 };
211 } else {
212 const existingBackups = this.#extensionSettings.vaultSnapshots ?? [];
213 existingBackups.push(snapshot);
214
215 // Enforce max backups limit (only for auto backups, keep manual and pre-restore)
216 const maxBackups = this.getMaxBackups();
217 const autoBackups = existingBackups.filter(b => b.reason === 'auto');
218 const otherBackups = existingBackups.filter(b => b.reason !== 'auto');
219
220 // Sort auto backups by date (newest first) and keep only maxBackups
221 autoBackups.sort((a, b) =>
222 new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
223 );
224 const trimmedAutoBackups = autoBackups.slice(0, maxBackups);
225
226 this.#extensionSettings.vaultSnapshots = [...otherBackups, ...trimmedAutoBackups];
227 }
228
229 await this.saveFullData(this.#extensionSettings);
230 return snapshot;
231 }
232
233 /**
234 * Deletes a backup by ID.
235 */
236 async deleteBackup(backupId: string): Promise<boolean> {
237 if (!this.#extensionSettings?.vaultSnapshots) {
238 return false;
239 }
240
241 const initialLength = this.#extensionSettings.vaultSnapshots.length;
242 this.#extensionSettings.vaultSnapshots = this.#extensionSettings.vaultSnapshots.filter(
243 b => b.id !== backupId
244 );
245
246 if (this.#extensionSettings.vaultSnapshots.length < initialLength) {
247 await this.saveFullData(this.#extensionSettings);
248 return true;
249 }
250 return false;
251 }
252
253 /**
254 * Gets the data from a backup for restoration.
255 * Note: The caller should create a pre-restore backup before calling this.
256 */
257 getBackupData(backupId: string): EncryptedVault | undefined {
258 const backup = this.getBackupById(backupId);
259 return backup?.data;
260 }
261 }
262