web


git clone https://radroots.dev/git/web.git
Log | Files | Refs | Submodules | README | LICENSE

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 };