(feat): add cache package
All checks were successful
/ build-web (push) Successful in 7m26s
/ build-desktop (linux) (push) Successful in 11m29s
/ build-mobile (push) Successful in 19m15s
/ release (push) Successful in 3m3s

(feat): improve local storage security
(feat): move settings to dedicated settings package
This commit is contained in:
Alois 2026-07-11 22:08:01 +02:00
commit 790a1db788
54 changed files with 1984 additions and 947 deletions

View file

@ -423,6 +423,8 @@ export interface Storage extends SettingsStorageDefaults {
legal_docs: z.infer<typeof legalDocsSchema>;
cached_contacts: Contacts;
cached_communities: Communities;
cache_contacts: number;
cache_messages_per_chat: number;
omega_url: string;
forced_omikron_url: string | undefined;
forced_omikron_public_key: string | undefined;
@ -475,6 +477,8 @@ export const storageDefaults: Storage = {
},
cached_contacts: [],
cached_communities: [],
cache_contacts: 5,
cache_messages_per_chat: 20,
omega_url: "https://omega.tensamin.net",
forced_omikron_url: undefined,
forced_omikron_public_key: undefined,

View file

@ -38,6 +38,13 @@ type ElectronDesktopApi = {
iconDataUrl?: string;
}) => Promise<void>;
};
secureStorage?: {
getStatus?: () => Promise<{ available: boolean; backend: string | null }>;
load?: (key: string) => Promise<string | null>;
save?: (key: string, value: string) => Promise<void>;
delete?: (key: string) => Promise<void>;
clear?: () => Promise<void>;
};
};
declare global {

View file

@ -0,0 +1,98 @@
export const TENSAMIN_DB_NAME = "tensamin";
export const TENSAMIN_DB_VERSION = 1;
export type TensaminStore = "storage" | "cache" | "keys";
const stores: TensaminStore[] = ["storage", "cache", "keys"];
let databasePromise: Promise<IDBDatabase> | undefined;
export function openTensaminDatabase(
indexedDb: IDBFactory = globalThis.indexedDB,
) {
databasePromise ??= new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDb.open(TENSAMIN_DB_NAME, TENSAMIN_DB_VERSION);
request.onupgradeneeded = () => {
for (const store of stores) {
if (!request.result.objectStoreNames.contains(store)) {
request.result.createObjectStore(store);
}
}
};
request.onsuccess = () => {
request.result.onversionchange = () => request.result.close();
resolve(request.result);
};
request.onerror = () => {
databasePromise = undefined;
reject(request.error);
};
request.onblocked = () => {
databasePromise = undefined;
reject(new Error("The Tensamin database upgrade is blocked."));
};
});
return databasePromise;
}
export async function getDatabaseEntry<T>(store: TensaminStore, key: string) {
const database = await openTensaminDatabase();
return new Promise<T | undefined>((resolve, reject) => {
const request = database
.transaction(store, "readonly")
.objectStore(store)
.get(key);
request.onsuccess = () => resolve(request.result as T | undefined);
request.onerror = () => reject(request.error);
});
}
export async function setDatabaseEntry(
store: TensaminStore,
key: string,
value: unknown,
) {
const database = await openTensaminDatabase();
return new Promise<void>((resolve, reject) => {
const transaction = database.transaction(store, "readwrite");
transaction.objectStore(store).put(value, key);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
}
export async function deleteDatabaseEntry(store: TensaminStore, key: string) {
const database = await openTensaminDatabase();
return new Promise<void>((resolve, reject) => {
const transaction = database.transaction(store, "readwrite");
transaction.objectStore(store).delete(key);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
}
export async function listDatabaseEntries(
store: TensaminStore,
keyPrefix: string,
) {
const database = await openTensaminDatabase();
return new Promise<Array<[string, unknown]>>((resolve, reject) => {
const entries: Array<[string, unknown]> = [];
const cursor = database
.transaction(store, "readonly")
.objectStore(store)
.openCursor();
cursor.onsuccess = () => {
const value = cursor.result;
if (!value) {
resolve(entries);
return;
}
if (typeof value.key === "string" && value.key.startsWith(keyPrefix)) {
entries.push([value.key, value.value]);
}
value.continue();
};
cursor.onerror = () => reject(cursor.error);
});
}

View file

@ -46,6 +46,7 @@ const settings = {
},
},
application: {
cache: {},
theme: {},
licenses: {},
},