import.ts (6760B)
1 import { datastore, db, nostr_keys } from "$lib/utils/app"; 2 import { ls } from "$lib/utils/i18n"; 3 import { get_store, handle_err, parse_file_json } from "@radroots/apps-lib"; 4 import type { I18nPayloadValue } from "@radroots/apps-lib"; 5 import type { ImportableAppState } from "@radroots/apps-lib-pwa/types/app"; 6 import type { IError } from "@radroots/types-bindings"; 7 import type { IdbClientConfig } from "@radroots/utils"; 8 import { err_msg, throw_err } from "@radroots/utils"; 9 import { createStore, set as idb_set } from "idb-keyval"; 10 import { reset_sql_cipher } from "../app/cipher"; 11 import { app_cfg } from "../app/config"; 12 13 const ls_val = get_store(ls); 14 15 export type AppImportStateResult = { 16 pass: boolean; 17 message?: string; 18 } 19 20 const is_record = (value: unknown): value is Record<string, unknown> => 21 typeof value === "object" && value !== null && !Array.isArray(value); 22 23 const i18n_payload_value = (value: unknown): I18nPayloadValue => 24 typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : "unknown"; 25 26 const assert_config_match = ( 27 current: IdbClientConfig, 28 incoming: IdbClientConfig, 29 label: string 30 ): void => { 31 if (current.database !== incoming.database || current.store !== incoming.store) { 32 throw_err( 33 ls_val(`error.configuration.import.storage_mismatch`, { 34 label, 35 app_database: current.database, 36 app_store: current.store, 37 backup_database: incoming.database, 38 backup_store: incoming.store, 39 }) 40 ); 41 } 42 }; 43 44 export const validate_import_file = async (file: File | null): Promise<ImportableAppState> => { 45 const parsed_res = await parse_file_json(file); 46 if (!parsed_res.ok) throw_err(ls_val(`error.configuration.import.invalid_file_contents`)); 47 return await validate_import_state(parsed_res.value); 48 }; 49 50 export const validate_import_state = async (state: unknown): Promise<ImportableAppState> => { 51 if (!is_record(state)) throw_err(ls_val(`error.configuration.import.empty_file`)); 52 if (state.backup_version !== app_cfg.backup.version) { 53 throw_err( 54 ls_val(`error.configuration.import.unsupported_backup_version`, { 55 backup_version: i18n_payload_value(state.backup_version), 56 expected_version: app_cfg.backup.version, 57 }) 58 ); 59 } 60 const versions = state.versions; 61 if (!is_record(versions)) { 62 throw_err(ls_val(`error.configuration.import.missing_version_metadata`)); 63 } 64 const backup_format = versions.backup_format; 65 const replica_db_version = versions.replica_db; 66 if (typeof versions.app !== "string" || typeof replica_db_version !== "string" || typeof backup_format !== "string") { 67 throw_err(ls_val(`error.configuration.import.missing_version_metadata`)); 68 } 69 const database = state.database; 70 if (!state.datastore || !state.nostr_keystore || !is_record(database)) { 71 throw_err(ls_val(`error.configuration.import.missing_required_sections`)); 72 } 73 const backup = database.backup; 74 if (!is_record(backup) || backup.format_version !== backup_format) { 75 throw_err(ls_val(`error.configuration.import.database_format_mismatch`)); 76 } 77 if (backup.replica_db_version !== replica_db_version) { 78 throw_err(ls_val(`error.configuration.import.replica_db_version_mismatch`)); 79 } 80 return { 81 ...state, 82 versions: { ...versions, backup_format }, 83 database: { ...database, backup } 84 } as ImportableAppState; 85 }; 86 87 88 const restore_datastore_state = async (state: ImportableAppState["datastore"]): Promise<void> => { 89 const ds_cfg = datastore.get_config(); 90 assert_config_match(ds_cfg, state.config, ls_val(`common.datastore`)); 91 const reset_res = await datastore.reset(); 92 if (reset_res && "err" in reset_res) throw_err(reset_res.err); 93 const store = createStore(ds_cfg.database, ds_cfg.store); 94 const entries = Object.entries(state.entries); 95 for (const [key, value] of entries) { 96 await idb_set(key, value, store); 97 } 98 }; 99 100 const restore_nostr_keystore_state = async ( 101 state: ImportableAppState["nostr_keystore"] 102 ): Promise<void> => { 103 const nostr_cfg = nostr_keys.get_config(); 104 assert_config_match(nostr_cfg, state.config, ls_val(`common.nostr_keystore`)); 105 const reset_res = await nostr_keys.reset(); 106 if (reset_res && "err" in reset_res) throw_err(reset_res.err); 107 for (const key of state.keys) { 108 const add_res = await nostr_keys.add(key.secret_key); 109 if ("err" in add_res) throw_err(add_res.err); 110 } 111 }; 112 113 const restore_replica_db_state = async (state: ImportableAppState["database"]): Promise<void> => { 114 const current_store_key = db.get_store_key(); 115 if (current_store_key !== state.store_key) { 116 throw_err( 117 ls_val(`error.configuration.import.replica_store_key_mismatch`, { 118 app_store_key: current_store_key, 119 backup_store_key: state.store_key, 120 }) 121 ); 122 } 123 await reset_sql_cipher(current_store_key); 124 await db.reinit(); 125 await db.import_json(state.backup); 126 }; 127 128 export const import_app_state = async (payload: ImportableAppState): Promise<AppImportStateResult | IError<string>> => { 129 try { 130 if (typeof window === "undefined") return err_msg(ls_val(`error.client.undefined_window`)); 131 const import_state = await validate_import_state(payload); 132 assert_config_match(datastore.get_config(), import_state.datastore.config, ls_val(`common.datastore`)); 133 assert_config_match(nostr_keys.get_config(), import_state.nostr_keystore.config, ls_val(`common.nostr_keystore`)); 134 const current_store_key = db.get_store_key(); 135 if (current_store_key !== import_state.database.store_key) { 136 const message = ls_val(`error.configuration.import.replica_store_key_mismatch`, { 137 app_store_key: current_store_key, 138 backup_store_key: import_state.database.store_key, 139 }); 140 console.error(message); 141 return err_msg(message); 142 } 143 await restore_datastore_state(import_state.datastore); 144 await restore_nostr_keystore_state(import_state.nostr_keystore); 145 await restore_replica_db_state(import_state.database); 146 147 return { pass: true } 148 } catch (e) { 149 handle_err(e, `import_app_state`); 150 return { pass: false, message: ls_val(`error.configuration.import.failure`) } 151 } 152 }; 153 154 export const import_app_state_from_file = async (file: File): Promise<ReturnType<typeof import_app_state>> => { 155 const validated = await validate_import_file(file); 156 return await import_app_state(validated); 157 };