web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

web.ts (13704B)


      1 import { err_msg, handle_err, text_dec, text_enc, type IdbClientConfig, type ResolveError, type ResultObj, type ResultPass, type ResultsList } from "@radroots/utils";
      2 import { clear as idb_clear, del as idb_del, get as idb_get, keys as idb_keys, set as idb_set, type UseStore } from "idb-keyval";
      3 import type { BackupDatastorePayload } from "../backup/types.js";
      4 import { crypto_registry_clear_key_entry, crypto_registry_clear_store_index, crypto_registry_get_store_index } from "../crypto/registry.js";
      5 import { WebEncryptedStore } from "../idb/encrypted_store.js";
      6 import { IDB_CONFIG_DATASTORE } from "../idb/config.js";
      7 import { idb_value_as_bytes } from "../idb/value.js";
      8 import { is_error } from "../utils/resolve.js";
      9 import { cl_datastore_error } from "./error.js";
     10 import type {
     11     IClientDatastore,
     12     IClientDatastoreDelPrefResolve,
     13     IClientDatastoreDelResolve,
     14     IClientDatastoreKeyMap,
     15     IClientDatastoreKeyParamMap
     16 } from "./types.js";
     17 
     18 const DEFAULT_IDB_CONFIG: IdbClientConfig = IDB_CONFIG_DATASTORE;
     19 
     20 const is_record = (value: unknown): value is Record<string, unknown> =>
     21     typeof value === "object" && value !== null && !Array.isArray(value);
     22 
     23 export interface IWebDatastore<
     24     Tk extends IClientDatastoreKeyMap,
     25     Tp extends IClientDatastoreKeyParamMap,
     26     TkO extends IClientDatastoreKeyMap,
     27 > extends IClientDatastore<Tk, Tp, TkO> { }
     28 
     29 export class WebDatastore<
     30     Tk extends IClientDatastoreKeyMap,
     31     Tp extends IClientDatastoreKeyParamMap,
     32     TkO extends IClientDatastoreKeyMap,
     33 > implements IWebDatastore<Tk, Tp, TkO> {
     34     private readonly encrypted_store: WebEncryptedStore;
     35     private readonly store_id: string;
     36     private _key_map: Tk;
     37     private _key_param_map: Tp;
     38     private _key_obj_map: TkO;
     39 
     40     constructor(key_map: Tk, key_param_map: Tp, key_obj_map: TkO, config?: Partial<IdbClientConfig>) {
     41         const idb_config: IdbClientConfig = {
     42             database: config?.database ?? DEFAULT_IDB_CONFIG.database,
     43             store: config?.store ?? DEFAULT_IDB_CONFIG.store
     44         };
     45         this.store_id = `datastore:${idb_config.database}:${idb_config.store}`;
     46         this.encrypted_store = new WebEncryptedStore({
     47             idb_config,
     48             store_id: this.store_id,
     49             idb_error: cl_datastore_error.idb_undefined,
     50             iv_length: 12
     51         });
     52         this._key_map = key_map;
     53         this._key_param_map = key_param_map;
     54         this._key_obj_map = key_obj_map;
     55     }
     56 
     57     private async get_store(): Promise<ResolveError<UseStore>> {
     58         return await this.encrypted_store.get_store();
     59     }
     60 
     61     private async decrypt_value(store_key: string, stored: unknown): Promise<ResolveError<ResultObj<string>>> {
     62         if (typeof stored === "string") {
     63             const encrypted = await this.encrypted_store.encrypt_bytes(text_enc(stored));
     64             if (is_error(encrypted)) return encrypted;
     65             const store = await this.get_store();
     66             if (is_error(store)) return store;
     67             await idb_set(store_key, encrypted, store);
     68             return { result: stored };
     69         }
     70         const bytes = idb_value_as_bytes(stored);
     71         if (!bytes) return err_msg(cl_datastore_error.no_result);
     72         const outcome = await this.encrypted_store.decrypt_record(bytes);
     73         if (is_error(outcome)) return outcome;
     74         if (outcome.reencrypted) {
     75             const store = await this.get_store();
     76             if (is_error(store)) return store;
     77             await idb_set(store_key, outcome.reencrypted, store);
     78         }
     79         return { result: text_dec(outcome.plaintext) };
     80     }
     81 
     82     public get_config(): IdbClientConfig {
     83         return this.encrypted_store.get_config();
     84     }
     85 
     86     public get_store_id(): string {
     87         return this.store_id;
     88     }
     89 
     90     public async init(): Promise<ResolveError<void>> {
     91         try {
     92             const store = await this.get_store();
     93             if (is_error(store)) return store;
     94             return;
     95         } catch (e) {
     96             return handle_err(e);
     97         }
     98     }
     99 
    100     public async set(key: keyof Tk, value: string): Promise<ResolveError<ResultObj<string>>> {
    101         try {
    102             const encrypted = await this.encrypted_store.encrypt_bytes(text_enc(value));
    103             if (is_error(encrypted)) return encrypted;
    104             const store = await this.get_store();
    105             if (is_error(store)) return store;
    106             await idb_set(this._key_map[key], encrypted, store);
    107             return { result: value };
    108         } catch (e) {
    109             return handle_err(e);
    110         }
    111     }
    112 
    113     public async get(key: keyof Tk): Promise<ResolveError<ResultObj<string>>> {
    114         try {
    115             const store_key = this._key_map[key];
    116             const store = await this.get_store();
    117             if (is_error(store)) return store;
    118             const value = await idb_get(store_key, store);
    119             if (!value) return err_msg(cl_datastore_error.no_result);
    120             return await this.decrypt_value(store_key, value);
    121         } catch (e) {
    122             return handle_err(e);
    123         }
    124     }
    125 
    126     public async del(key: keyof Tk): Promise<IClientDatastoreDelResolve> {
    127         try {
    128             const store = await this.get_store();
    129             if (is_error(store)) return store;
    130             await idb_del(this._key_map[key], store);
    131             return { result: key.toString() };
    132         } catch (e) {
    133             return handle_err(e);
    134         }
    135     }
    136 
    137     public async set_obj<T>(key: keyof TkO, value: T): Promise<ResolveError<ResultObj<T>>> {
    138         try {
    139             const serialized = JSON.stringify(value);
    140             const encrypted = await this.encrypted_store.encrypt_bytes(text_enc(serialized));
    141             if (is_error(encrypted)) return encrypted;
    142             const store = await this.get_store();
    143             if (is_error(store)) return store;
    144             await idb_set(this._key_obj_map[key], encrypted, store);
    145             return { result: value };
    146         } catch (e) {
    147             return handle_err(e);
    148         }
    149     }
    150 
    151     public async update_obj<T extends Record<string, unknown>>(key: keyof TkO, value: Partial<T>): Promise<ResolveError<ResultObj<T>>> {
    152         try {
    153             const store = await this.get_store();
    154             if (is_error(store)) return store;
    155             const k = this._key_obj_map[key];
    156             const obj_curr: Record<string, unknown> = {};
    157             const curr = await idb_get(k, store);
    158             if (curr) {
    159                 const decrypted = await this.decrypt_value(k, curr);
    160                 if (is_error(decrypted)) return decrypted;
    161                 const parsed: unknown = JSON.parse(decrypted.result);
    162                 if (is_record(parsed)) for (const [curr_key, curr_val] of Object.entries(parsed)) obj_curr[curr_key] = curr_val;
    163             }
    164             const obj: T = { ...obj_curr, ...value } as T;
    165             const serialized = JSON.stringify(obj);
    166             const encrypted = await this.encrypted_store.encrypt_bytes(text_enc(serialized));
    167             if (is_error(encrypted)) return encrypted;
    168             await idb_set(k, encrypted, store);
    169             return { result: obj };
    170         } catch (e) {
    171             return handle_err(e);
    172         }
    173     }
    174 
    175     public async get_obj<T>(key: keyof TkO): Promise<ResolveError<ResultObj<T>>> {
    176         try {
    177             const store_key = this._key_obj_map[key];
    178             const store = await this.get_store();
    179             if (is_error(store)) return store;
    180             const value = await idb_get(store_key, store);
    181             if (!value) return err_msg(cl_datastore_error.no_result);
    182             const decrypted = await this.decrypt_value(store_key, value);
    183             if (is_error(decrypted)) return decrypted;
    184             return { result: JSON.parse(decrypted.result) };
    185         } catch (e) {
    186             return handle_err(e);
    187         }
    188     }
    189 
    190     public async del_obj(key: keyof TkO): Promise<ResolveError<ResultObj<string>>> {
    191         try {
    192             const store = await this.get_store();
    193             if (is_error(store)) return store;
    194             await idb_del(this._key_obj_map[key], store);
    195             return { result: key.toString() };
    196         } catch (e) {
    197             return handle_err(e);
    198         }
    199     }
    200 
    201     public async setp<K extends keyof Tp>(
    202         key: K,
    203         key_param: Parameters<Tp[K]>[0],
    204         value: string
    205     ): Promise<ResolveError<ResultObj<string>>> {
    206         try {
    207             const store_key = this._key_param_map[key](key_param);
    208             const encrypted = await this.encrypted_store.encrypt_bytes(text_enc(value));
    209             if (is_error(encrypted)) return encrypted;
    210             const store = await this.get_store();
    211             if (is_error(store)) return store;
    212             await idb_set(store_key, encrypted, store);
    213             return { result: value };
    214         } catch (e) {
    215             return handle_err(e);
    216         }
    217     }
    218 
    219     public async getp<K extends keyof Tp>(
    220         key: K,
    221         key_param: Parameters<Tp[K]>[0]
    222     ): Promise<ResolveError<ResultObj<string>>> {
    223         try {
    224             const store_key = this._key_param_map[key](key_param);
    225             const store = await this.get_store();
    226             if (is_error(store)) return store;
    227             const value = await idb_get(store_key, store);
    228             if (!value) return err_msg(cl_datastore_error.no_result);
    229             return await this.decrypt_value(store_key, value);
    230         } catch (e) {
    231             return handle_err(e);
    232         }
    233     }
    234 
    235     public async keys(): Promise<ResolveError<ResultsList<string>>> {
    236         try {
    237             const store = await this.get_store();
    238             if (is_error(store)) return store;
    239             const all_keys = await idb_keys(store);
    240             return { results: all_keys.filter((k): k is string => typeof k === "string") };
    241         } catch (e) {
    242             return handle_err(e);
    243         }
    244     }
    245 
    246     public async del_pref(key_prefix: string): Promise<IClientDatastoreDelPrefResolve> {
    247         try {
    248             const store = await this.get_store();
    249             if (is_error(store)) return store;
    250             const all_keys = await idb_keys(store);
    251             const pref = all_keys.filter((k): k is string => typeof k === "string" && k.startsWith(key_prefix));
    252             for (const key of pref) await idb_del(key, store);
    253             return { results: pref };
    254         } catch (e) {
    255             return handle_err(e);
    256         }
    257     }
    258 
    259     public async entries(): Promise<ResolveError<ResultsList<[string, string | null]>>> {
    260         try {
    261             const store = await this.get_store();
    262             if (is_error(store)) return store;
    263             const all_keys = await idb_keys(store);
    264             const out: [string, string | null][] = [];
    265             for (const key of all_keys) {
    266                 if (typeof key !== "string") continue;
    267                 const value = await idb_get(key, store);
    268                 if (!value) {
    269                     out.push([key, null]);
    270                     continue;
    271                 }
    272                 if (typeof value === "string") {
    273                     out.push([key, value]);
    274                     continue;
    275                 }
    276                 const decrypted = await this.decrypt_value(key, value);
    277                 if (is_error(decrypted)) return decrypted;
    278                 out.push([key, decrypted.result]);
    279             }
    280             return { results: out };
    281         } catch (e) {
    282             return handle_err(e);
    283         }
    284     }
    285 
    286     public async reset(): Promise<ResolveError<ResultPass>> {
    287         try {
    288             const store = await this.get_store();
    289             if (is_error(store)) return store;
    290             await idb_clear(store);
    291             const index = await crypto_registry_get_store_index(this.store_id);
    292             if (is_error(index)) return index;
    293             if (index) {
    294                 const cleared = await crypto_registry_clear_store_index(this.store_id);
    295                 if (is_error(cleared)) return cleared;
    296                 for (const key_id of index.key_ids) {
    297                     const res = await crypto_registry_clear_key_entry(key_id);
    298                     if (is_error(res)) return res;
    299                 }
    300             }
    301             return { pass: true } as const;
    302         } catch (e) {
    303             return handle_err(e);
    304         }
    305     }
    306 
    307     public async export_backup(): Promise<ResolveError<BackupDatastorePayload>> {
    308         try {
    309             const store = await this.get_store();
    310             if (is_error(store)) return store;
    311             const all_keys = await idb_keys(store);
    312             const entries: BackupDatastorePayload["entries"] = [];
    313             for (const key of all_keys) {
    314                 if (typeof key !== "string") continue;
    315                 const stored = await idb_get(key, store);
    316                 if (!stored) return err_msg(cl_datastore_error.no_result);
    317                 const decrypted = await this.decrypt_value(key, stored);
    318                 if (is_error(decrypted)) return decrypted;
    319                 entries.push({ key, value: decrypted.result });
    320             }
    321             return { entries };
    322         } catch (e) {
    323             return handle_err(e);
    324         }
    325     }
    326 
    327     public async import_backup(payload: BackupDatastorePayload): Promise<ResolveError<void>> {
    328         try {
    329             const store = await this.get_store();
    330             if (is_error(store)) return store;
    331             for (const entry of payload.entries) {
    332                 const encrypted = await this.encrypted_store.encrypt_bytes(text_enc(entry.value));
    333                 if (is_error(encrypted)) return encrypted;
    334                 await idb_set(entry.key, encrypted, store);
    335             }
    336             return;
    337         } catch (e) {
    338             return handle_err(e);
    339         }
    340     }
    341 }