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 }