web_lib

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

registry.ts (8560B)


      1 import { createStore, del as idb_del, get as idb_get, keys as idb_keys, set as idb_set, type UseStore } from "idb-keyval";
      2 import { err_msg, handle_err, type IdbClientConfig, type ResolveError } from "@radroots/utils";
      3 import { IDB_CONFIG_CRYPTO_REGISTRY } from "../idb/config.js";
      4 import { idb_store_ensure } from "../idb/store.js";
      5 import { is_error } from "../utils/resolve.js";
      6 import { cl_crypto_error } from "./error.js";
      7 import type { CryptoKeyEntry, CryptoRegistryExport, CryptoStoreIndex } from "./types.js";
      8 
      9 const CRYPTO_IDB_CONFIG: IdbClientConfig = IDB_CONFIG_CRYPTO_REGISTRY;
     10 
     11 let crypto_store: UseStore | null = null;
     12 const STORE_INDEX_PREFIX = "store:";
     13 const KEY_ENTRY_PREFIX = "key:";
     14 const DEVICE_MATERIAL_KEY = "device:material";
     15 
     16 const ensure_idb = async (): Promise<ResolveError<void>> => {
     17     if (typeof indexedDB === "undefined") return err_msg(cl_crypto_error.idb_undefined);
     18     try {
     19         await idb_store_ensure(CRYPTO_IDB_CONFIG.database, CRYPTO_IDB_CONFIG.store);
     20         return;
     21     } catch (e) {
     22         return handle_err(e);
     23     }
     24 };
     25 
     26 const get_crypto_store = async (): Promise<ResolveError<UseStore>> => {
     27     const ensured = await ensure_idb();
     28     if (is_error(ensured)) return ensured;
     29     if (!crypto_store) crypto_store = createStore(CRYPTO_IDB_CONFIG.database, CRYPTO_IDB_CONFIG.store);
     30     return crypto_store;
     31 };
     32 
     33 const store_index_key = (store_id: string): string => `${STORE_INDEX_PREFIX}${store_id}`;
     34 const key_entry_key = (key_id: string): string => `${KEY_ENTRY_PREFIX}${key_id}`;
     35 
     36 const is_string_array = (value: unknown): value is string[] =>
     37     Array.isArray(value) && value.every((item) => typeof item === "string");
     38 
     39 const is_record = (value: unknown): value is Record<string, unknown> =>
     40     typeof value === "object" && value !== null && !Array.isArray(value);
     41 
     42 const is_crypto_store_index = (value: unknown): value is CryptoStoreIndex => {
     43     if (!is_record(value)) return false;
     44     return typeof value.store_id === "string"
     45         && typeof value.active_key_id === "string"
     46         && typeof value.created_at === "number"
     47         && is_string_array(value.key_ids);
     48 };
     49 
     50 const is_crypto_key_entry = (value: unknown): value is CryptoKeyEntry => {
     51     if (!is_record(value)) return false;
     52     return typeof value.key_id === "string"
     53         && typeof value.store_id === "string"
     54         && typeof value.created_at === "number"
     55         && typeof value.status === "string"
     56         && value.wrapped_key instanceof Uint8Array
     57         && value.wrap_iv instanceof Uint8Array
     58         && value.kdf_salt instanceof Uint8Array
     59         && typeof value.kdf_iterations === "number"
     60         && typeof value.iv_length === "number"
     61         && typeof value.algorithm === "string"
     62         && typeof value.provider_id === "string";
     63 };
     64 
     65 export const crypto_registry_get_store_index = async (store_id: string): Promise<ResolveError<CryptoStoreIndex | null>> => {
     66     try {
     67         const store = await get_crypto_store();
     68         if (is_error(store)) return store;
     69         const record = await idb_get(store_index_key(store_id), store);
     70         if (!record) return null;
     71         if (!is_crypto_store_index(record)) return err_msg(cl_crypto_error.registry_failure);
     72         return record;
     73     } catch (e) {
     74         return handle_err(e);
     75     }
     76 };
     77 
     78 export const crypto_registry_set_store_index = async (index: CryptoStoreIndex): Promise<ResolveError<void>> => {
     79     try {
     80         const store = await get_crypto_store();
     81         if (is_error(store)) return store;
     82         await idb_set(store_index_key(index.store_id), index, store);
     83         return;
     84     } catch (e) {
     85         return handle_err(e);
     86     }
     87 };
     88 
     89 export const crypto_registry_get_key_entry = async (key_id: string): Promise<ResolveError<CryptoKeyEntry | null>> => {
     90     try {
     91         const store = await get_crypto_store();
     92         if (is_error(store)) return store;
     93         const record = await idb_get(key_entry_key(key_id), store);
     94         if (!record) return null;
     95         if (!is_crypto_key_entry(record)) return err_msg(cl_crypto_error.registry_failure);
     96         return record;
     97     } catch (e) {
     98         return handle_err(e);
     99     }
    100 };
    101 
    102 export const crypto_registry_set_key_entry = async (entry: CryptoKeyEntry): Promise<ResolveError<void>> => {
    103     try {
    104         const store = await get_crypto_store();
    105         if (is_error(store)) return store;
    106         await idb_set(key_entry_key(entry.key_id), entry, store);
    107         return;
    108     } catch (e) {
    109         return handle_err(e);
    110     }
    111 };
    112 
    113 export const crypto_registry_list_store_indices = async (): Promise<ResolveError<CryptoStoreIndex[]>> => {
    114     try {
    115         const store = await get_crypto_store();
    116         if (is_error(store)) return store;
    117         const keys = await idb_keys(store);
    118         const store_keys = keys.filter((key): key is string => typeof key === "string" && key.startsWith(STORE_INDEX_PREFIX));
    119         const out: CryptoStoreIndex[] = [];
    120         for (const key of store_keys) {
    121             const record = await idb_get(key, store);
    122             if (!record) continue;
    123             if (!is_crypto_store_index(record)) return err_msg(cl_crypto_error.registry_failure);
    124             out.push(record);
    125         }
    126         return out;
    127     } catch (e) {
    128         return handle_err(e);
    129     }
    130 };
    131 
    132 export const crypto_registry_list_key_entries = async (): Promise<ResolveError<CryptoKeyEntry[]>> => {
    133     try {
    134         const store = await get_crypto_store();
    135         if (is_error(store)) return store;
    136         const keys = await idb_keys(store);
    137         const entry_keys = keys.filter((key): key is string => typeof key === "string" && key.startsWith(KEY_ENTRY_PREFIX));
    138         const out: CryptoKeyEntry[] = [];
    139         for (const key of entry_keys) {
    140             const record = await idb_get(key, store);
    141             if (!record) continue;
    142             if (!is_crypto_key_entry(record)) return err_msg(cl_crypto_error.registry_failure);
    143             out.push(record);
    144         }
    145         return out;
    146     } catch (e) {
    147         return handle_err(e);
    148     }
    149 };
    150 
    151 export const crypto_registry_export = async (): Promise<ResolveError<CryptoRegistryExport>> => {
    152     const stores = await crypto_registry_list_store_indices();
    153     if (is_error(stores)) return stores;
    154     const keys = await crypto_registry_list_key_entries();
    155     if (is_error(keys)) return keys;
    156     return { stores, keys };
    157 };
    158 
    159 export const crypto_registry_import = async (registry: CryptoRegistryExport): Promise<ResolveError<void>> => {
    160     const store = await get_crypto_store();
    161     if (is_error(store)) return store;
    162     for (const store_index of registry.stores) {
    163         const res = await crypto_registry_set_store_index(store_index);
    164         if (is_error(res)) return res;
    165     }
    166     for (const entry of registry.keys) {
    167         const res = await crypto_registry_set_key_entry(entry);
    168         if (is_error(res)) return res;
    169     }
    170     return;
    171 };
    172 
    173 export const crypto_registry_get_device_material = async (): Promise<ResolveError<Uint8Array | null>> => {
    174     try {
    175         const store = await get_crypto_store();
    176         if (is_error(store)) return store;
    177         const record = await idb_get(DEVICE_MATERIAL_KEY, store);
    178         if (!record) return null;
    179         if (record instanceof Uint8Array) return record;
    180         if (record instanceof ArrayBuffer) return new Uint8Array(record);
    181         if (ArrayBuffer.isView(record)) return new Uint8Array(record.buffer, record.byteOffset, record.byteLength);
    182         return err_msg(cl_crypto_error.registry_failure);
    183     } catch (e) {
    184         return handle_err(e);
    185     }
    186 };
    187 
    188 export const crypto_registry_set_device_material = async (material: Uint8Array): Promise<ResolveError<void>> => {
    189     try {
    190         const store = await get_crypto_store();
    191         if (is_error(store)) return store;
    192         await idb_set(DEVICE_MATERIAL_KEY, material, store);
    193         return;
    194     } catch (e) {
    195         return handle_err(e);
    196     }
    197 };
    198 
    199 export const crypto_registry_clear_store_index = async (store_id: string): Promise<ResolveError<void>> => {
    200     try {
    201         const store = await get_crypto_store();
    202         if (is_error(store)) return store;
    203         await idb_del(store_index_key(store_id), store);
    204         return;
    205     } catch (e) {
    206         return handle_err(e);
    207     }
    208 };
    209 
    210 export const crypto_registry_clear_key_entry = async (key_id: string): Promise<ResolveError<void>> => {
    211     try {
    212         const store = await get_crypto_store();
    213         if (is_error(store)) return store;
    214         await idb_del(key_entry_key(key_id), store);
    215         return;
    216     } catch (e) {
    217         return handle_err(e);
    218     }
    219 };