web_lib

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

commit ff72e36fc17e79d4c6b89aa9b01d741cda314dbe
parent 459f17a0cada4ad4fc8b8ebc8634da5b1455e678
Author: triesap <triesap@radroots.dev>
Date:   Sun, 21 Dec 2025 04:19:16 +0000

client: standardized client library exports with centralized typed error codes, updated interfaces, and hardened web storage/crypto/http implementations

Diffstat:
Mclient/package.json | 8+++++++-
Aclient/src/cipher/error.ts | 10++++++++++
Aclient/src/cipher/index.ts | 4++++
Aclient/src/cipher/types.ts | 8++++++++
Mclient/src/cipher/web.ts | 101++++++++++++++++++++++++++++++++-----------------------------------------------
Aclient/src/datastore/error.ts | 8++++++++
Mclient/src/datastore/index.ts | 2+-
Mclient/src/datastore/types.ts | 16++++++++--------
Mclient/src/datastore/web.ts | 91++++++++++++++++++++++++++++++++++++++++---------------------------------------
Aclient/src/error.ts | 10++++++++++
Aclient/src/fs/error.ts | 6++++++
Mclient/src/fs/index.ts | 1+
Mclient/src/fs/types.ts | 5++---
Mclient/src/fs/web.ts | 16+++++++++-------
Aclient/src/geolocation/error.ts | 12++++++++++++
Mclient/src/geolocation/index.ts | 1+
Mclient/src/geolocation/types.ts | 14++------------
Mclient/src/geolocation/web.ts | 27++++++++++++++-------------
Aclient/src/http/error.ts | 8++++++++
Mclient/src/http/index.ts | 2+-
Mclient/src/http/types.ts | 8+++++++-
Mclient/src/http/web.ts | 75++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Aclient/src/index.ts | 1+
Aclient/src/keystore/error.ts | 10++++++++++
Mclient/src/keystore/index.ts | 1+
Mclient/src/keystore/types.ts | 13++++++-------
Mclient/src/keystore/web-nostr.ts | 36+++++++++++++++++++++++++-----------
Mclient/src/keystore/web.ts | 49++++++++++++++++++++++++++++++++++---------------
Aclient/src/notifications/error.ts | 6++++++
Mclient/src/notifications/index.ts | 2+-
Mclient/src/notifications/types.ts | 5++---
Mclient/src/notifications/web.ts | 32++++++++++++++++++++++----------
Aclient/src/radroots/error.ts | 8++++++++
Mclient/src/radroots/index.ts | 2+-
Mclient/src/radroots/types.ts | 16++++++++--------
Mclient/src/radroots/web.ts | 46+++++++++++++++++++++++++++++-----------------
Aclient/src/sql/error.ts | 5+++++
Mclient/src/sql/index.ts | 2+-
Mclient/src/sql/types.ts | 5++---
Mclient/src/sql/web.ts | 55+++++++++++++++++++++++--------------------------------
Aclient/src/tangle/error.ts | 5+++++
Mclient/src/tangle/index.ts | 2+-
Mclient/src/tangle/web.ts | 5++---
43 files changed, 445 insertions(+), 294 deletions(-)

diff --git a/client/package.json b/client/package.json @@ -5,6 +5,11 @@ "license": "GPLv3", "type": "module", "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + }, "./cipher": { "types": "./dist/types/cipher/index.d.ts", "import": "./dist/esm/cipher/index.js", @@ -66,9 +71,10 @@ "watch": "tsc -w" }, "dependencies": { + "@radroots/tangle-schema-bindings": "*", + "@radroots/tangle-sql-wasm": "*", "@radroots/utils": "*", "@radroots/utils-nostr": "*", - "@radroots/tangle-sql-wasm": "*", "idb": "^8.0.3", "idb-keyval": "^6.2.1", "sql.js": "1.13.0" diff --git a/client/src/cipher/error.ts b/client/src/cipher/error.ts @@ -0,0 +1,9 @@ +export const cl_cipher_error = { + idb_undefined: "error.client.cipher.idb_undefined", + crypto_undefined: "error.client.cipher.crypto_undefined", + invalid_ciphertext: "error.client.cipher.invalid_ciphertext", + decrypt_failure: "error.client.cipher.decrypt_failure" +} as const; + +export type ClientCipherError = keyof typeof cl_cipher_error; +export type ClientCipherErrorMessage = (typeof cl_cipher_error)[ClientCipherError]; +\ No newline at end of file diff --git a/client/src/cipher/index.ts b/client/src/cipher/index.ts @@ -0,0 +1,4 @@ +export * from "./error.js"; +export * from "./types.js"; +export * from "./web.js"; + diff --git a/client/src/cipher/types.ts b/client/src/cipher/types.ts @@ -0,0 +1,8 @@ +import { type IdbClientConfig } from "@radroots/utils"; + +export interface IClientCipher { + get_config(): IdbClientConfig; + reset(): Promise<void>; + encrypt(data: Uint8Array): Promise<Uint8Array>; + decrypt(blob: Uint8Array): Promise<Uint8Array>; +} diff --git a/client/src/cipher/web.ts b/client/src/cipher/web.ts @@ -1,30 +1,26 @@ -import { IdbClientConfig } from "@radroots/utils"; +import { as_array_buffer, type IdbClientConfig } from "@radroots/utils"; import { createStore, del as idb_del, get as idb_get, set as idb_set, type UseStore } from "idb-keyval"; +import { type WebAesGcmCipherConfig } from "../keystore/web.js"; +import { cl_cipher_error } from "./error.js"; +import type { IClientCipher } from "./types.js"; -function as_array_buffer(u8: Uint8Array): ArrayBuffer { - if (u8.byteOffset === 0 && u8.buffer instanceof ArrayBuffer && u8.byteLength === u8.buffer.byteLength) { - return u8.buffer; - } - return u8.slice().buffer; -} +const DEFAULT_IDB_CONFIG: IdbClientConfig = { + database: "radroots-aes-gcm-keystore", + store: "default" +}; + +const DEFAULT_WEB_AES_GCM_CONFIG = { + key_name: "radroots.aes-gcm.key", + algorithm: "AES-GCM", + key_length: 256, + iv_length: 12 +} as const; -const DEFAULT_DB_NAME = "radroots-aes-gcm-keystore"; -const DEFAULT_STORE_NAME = "default"; -const DEFAULT_KEY_NAME = "radroots.aes-gcm.key"; -const DEFAULT_ALGORITHM_NAME = "AES-GCM"; -const DEFAULT_KEY_LENGTH = 256; -const DEFAULT_IV_LENGTH = 12; const DEFAULT_KEY_USAGES: KeyUsage[] = ["encrypt", "decrypt"]; -export type WebAesGcmCipherConfig = { - idb_config?: Partial<IdbClientConfig>; - key_name?: string; - key_length?: number; - iv_length?: number; - algorithm?: string; -}; +export interface IWebAesGcmCipher extends IClientCipher { } -export class WebAesGcmCipher { +export class WebAesGcmCipher implements IWebAesGcmCipher { private readonly db_name: string; private readonly store_name: string; private readonly key_name: string; @@ -38,20 +34,20 @@ export class WebAesGcmCipher { constructor(config?: WebAesGcmCipherConfig) { const idb_config = config?.idb_config ?? {}; - this.db_name = idb_config.database ?? DEFAULT_DB_NAME; - this.store_name = idb_config.store ?? DEFAULT_STORE_NAME; - this.key_name = config?.key_name ?? DEFAULT_KEY_NAME; - this.algorithm_name = config?.algorithm ?? DEFAULT_ALGORITHM_NAME; + this.db_name = idb_config.database ?? DEFAULT_IDB_CONFIG.database; + this.store_name = idb_config.store ?? DEFAULT_IDB_CONFIG.store; + this.key_name = config?.key_name ?? DEFAULT_WEB_AES_GCM_CONFIG.key_name; + this.algorithm_name = config?.algorithm ?? DEFAULT_WEB_AES_GCM_CONFIG.algorithm; this.key_usages = DEFAULT_KEY_USAGES; - this.iv_length = config?.iv_length ?? DEFAULT_IV_LENGTH; - this.key_length = config?.key_length ?? DEFAULT_KEY_LENGTH; + this.iv_length = Number.isInteger(config?.iv_length) && (config?.iv_length ?? 0) > 0 + ? config?.iv_length ?? DEFAULT_WEB_AES_GCM_CONFIG.iv_length + : DEFAULT_WEB_AES_GCM_CONFIG.iv_length; + this.key_length = Number.isInteger(config?.key_length) && (config?.key_length ?? 0) > 0 + ? config?.key_length ?? DEFAULT_WEB_AES_GCM_CONFIG.key_length + : DEFAULT_WEB_AES_GCM_CONFIG.key_length; - if (typeof indexedDB === "undefined") { - throw new Error("error.client.keystore.idb_undefined"); - } - if (!globalThis.crypto || !globalThis.crypto.subtle) { - throw new Error("error.client.keystore.crypto_undefined"); - } + if (typeof indexedDB === "undefined") throw new Error(cl_cipher_error.idb_undefined); + if (!globalThis.crypto || !globalThis.crypto.subtle) throw new Error(cl_cipher_error.crypto_undefined); this.store = createStore(this.db_name, this.store_name); this.cached_key = null; @@ -102,29 +98,20 @@ export class WebAesGcmCipher { private async resolve_persisted_key(): Promise<CryptoKey | null> { const stored = await idb_get(this.key_name, this.store); - if (!stored) { - return null; - } - if (stored instanceof CryptoKey) { - return stored; - } - if (stored instanceof Uint8Array) { - return this.import_key(stored); - } + if (!stored) return null; + if (stored instanceof CryptoKey) return stored; + if (stored instanceof Uint8Array) return this.import_key(stored); if (ArrayBuffer.isView(stored) && stored.buffer instanceof ArrayBuffer) { - const view = new Uint8Array(stored.buffer); - return this.import_key(view); + const slice = new Uint8Array(stored.buffer, stored.byteOffset, stored.byteLength); + const bytes = new Uint8Array(slice); + return this.import_key(bytes); } return null; } private async load_key(): Promise<CryptoKey> { - if (this.cached_key) { - return this.cached_key; - } - if (this.key_promise) { - return this.key_promise; - } + if (this.cached_key) return this.cached_key; + if (this.key_promise) return this.key_promise; this.key_promise = this.inner_load_key(); const key = await this.key_promise; this.cached_key = key; @@ -134,9 +121,7 @@ export class WebAesGcmCipher { private async inner_load_key(): Promise<CryptoKey> { const existing = await this.resolve_persisted_key(); - if (existing) { - return existing; - } + if (existing) return existing; return this.generate_and_persist_key(); } @@ -147,9 +132,7 @@ export class WebAesGcmCipher { } public async encrypt(data: Uint8Array): Promise<Uint8Array> { - if (data.byteLength === 0) { - return data; - } + if (data.byteLength === 0) return data; const key = await this.load_key(); const iv = crypto.getRandomValues(new Uint8Array(this.iv_length)); const ciphertext_buffer = await crypto.subtle.encrypt( @@ -165,9 +148,7 @@ export class WebAesGcmCipher { } public async decrypt(blob: Uint8Array): Promise<Uint8Array> { - if (blob.byteLength <= this.iv_length) { - throw new Error("error.client.keystore.invalid_ciphertext"); - } + if (blob.byteLength <= this.iv_length) throw new Error(cl_cipher_error.invalid_ciphertext); const key = await this.load_key(); const iv = blob.slice(0, this.iv_length); const ciphertext = blob.slice(this.iv_length); @@ -179,7 +160,7 @@ export class WebAesGcmCipher { ); return new Uint8Array(plaintext); } catch { - throw new Error("error.client.keystore.decrypt_failure"); + throw new Error(cl_cipher_error.decrypt_failure); } } } diff --git a/client/src/datastore/error.ts b/client/src/datastore/error.ts @@ -0,0 +1,7 @@ +export const cl_datastore_error = { + idb_undefined: "error.client.datastore.idb_undefined", + no_result: "error.client.datastore.no_result" +} as const; + +export type ClientDatastoreError = keyof typeof cl_datastore_error; +export type ClientDatastoreErrorMessage = (typeof cl_datastore_error)[ClientDatastoreError]; +\ No newline at end of file diff --git a/client/src/datastore/index.ts b/client/src/datastore/index.ts @@ -1,3 +1,3 @@ +export * from "./error.js"; export * from "./types.js"; export * from "./web.js"; - diff --git a/client/src/datastore/types.ts b/client/src/datastore/types.ts @@ -14,23 +14,23 @@ export type IClientDatastoreEntriesResolve = ResolveError<ResultsList<[string, I export type IClientDatastoreKeyMap = Record<string, string>; export type IClientDatastoreKeyParamMap = Record<string, (...args: string[]) => string>; -export type IClientDatastore< +export interface IClientDatastore< TKeyMap extends IClientDatastoreKeyMap, TKeyParamMap extends IClientDatastoreKeyParamMap, TKeyObjMap extends IClientDatastoreKeyMap, -> = { +> { init(): Promise<ResolveError<void>>; - get_config(): IdbClientConfig - set(key: keyof TKeyMap, value: string): Promise<ResolveError<ResultPass>>; + get_config(): IdbClientConfig; + set(key: keyof TKeyMap, value: string): Promise<ResolveError<ResultObj<string>>>; get(key: keyof TKeyMap): Promise<ResolveError<ResultObj<string>>>; - set_obj(key: keyof TKeyObjMap, value: TKeyObjMap): Promise<ResolveError<ResultPass>>; - update_obj(key: keyof TKeyObjMap, value: Partial<TKeyObjMap>): Promise<ResolveError<ResultPass>>; + set_obj(key: keyof TKeyObjMap, value: TKeyObjMap): Promise<ResolveError<ResultObj<TKeyObjMap>>>; + update_obj(key: keyof TKeyObjMap, value: Partial<TKeyObjMap>): Promise<ResolveError<ResultObj<TKeyObjMap>>>; get_obj<T>(key: keyof TKeyObjMap): Promise<ResolveError<ResultObj<T>>>; del_obj(key: keyof TKeyObjMap): Promise<ResolveError<ResultObj<string>>>; del(key: keyof TKeyMap): Promise<IClientDatastoreDelResolve>; del_pref(key_prefix: string): Promise<IClientDatastoreDelPrefResolve>; - setp<K extends keyof TKeyParamMap>(key: K, key_param: Parameters<TKeyParamMap[K]>[0], value: string): Promise<ResolveError<ResultPass>>; + setp<K extends keyof TKeyParamMap>(key: K, key_param: Parameters<TKeyParamMap[K]>[0], value: string): Promise<ResolveError<ResultObj<string>>>; getp<K extends keyof TKeyParamMap>(key: K, key_param: Parameters<TKeyParamMap[K]>[0]): Promise<ResolveError<ResultObj<string>>>; keys(): Promise<ResolveError<ResultsList<string>>>; reset(): Promise<ResolveError<ResultPass>>; -}; +} diff --git a/client/src/datastore/web.ts b/client/src/datastore/web.ts @@ -1,31 +1,33 @@ -import { err_msg, handle_err, IdbClientConfig, ResolveError, ResultObj } from "@radroots/utils"; -import { - createStore, - 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"; +import { err_msg, handle_err, type IdbClientConfig, type ResolveError, type ResultObj, type ResultPass, type ResultsList } from "@radroots/utils"; +import { createStore, 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"; +import { cl_datastore_error } from "./error.js"; import type { IClientDatastore, IClientDatastoreDelPrefResolve, IClientDatastoreDelResolve, - IClientDatastoreGetPResolve, - IClientDatastoreGetResolve, IClientDatastoreKeyMap, - IClientDatastoreKeyParamMap, - IClientDatastoreKeysResolve, - IClientDatastoreSetPResolve, - IClientDatastoreSetResolve + IClientDatastoreKeyParamMap } from "./types.js"; +const DEFAULT_IDB_CONFIG: IdbClientConfig = { + database: "radroots-web-datastore", + store: "default", +}; + +const is_record = (value: unknown): value is Record<string, unknown> => + typeof value === "object" && value !== null && !Array.isArray(value); + +export interface IWebDatastore< + Tk extends IClientDatastoreKeyMap, + Tp extends IClientDatastoreKeyParamMap, + TkO extends IClientDatastoreKeyMap, +> extends IClientDatastore<Tk, Tp, TkO> { } + export class WebDatastore< Tk extends IClientDatastoreKeyMap, Tp extends IClientDatastoreKeyParamMap, TkO extends IClientDatastoreKeyMap, -> implements IClientDatastore<Tk, Tp, TkO> { +> implements IWebDatastore<Tk, Tp, TkO> { private db_name: string; private store_name: string; private store: UseStore | null = null; @@ -34,8 +36,8 @@ export class WebDatastore< private _key_obj_map: TkO; constructor(key_map: Tk, key_param_map: Tp, key_obj_map: TkO, config?: Partial<IdbClientConfig>) { - this.db_name = config?.database || "radroots-web-datastore"; - this.store_name = config?.store || "default"; + this.db_name = config?.database ?? DEFAULT_IDB_CONFIG.database; + this.store_name = config?.store ?? DEFAULT_IDB_CONFIG.store; this.store = null; this._key_map = key_map; this._key_param_map = key_param_map; @@ -44,7 +46,7 @@ export class WebDatastore< private get_store(): UseStore { if (!this.store) { - if (typeof indexedDB === "undefined") throw new Error("error.client.keystore.idb_undefined"); + if (typeof indexedDB === "undefined") throw new Error(cl_datastore_error.idb_undefined); this.store = createStore(this.db_name, this.store_name); } return this.store; @@ -57,7 +59,7 @@ export class WebDatastore< }; } - public async init() { + public async init(): Promise<ResolveError<void>> { try { this.get_store(); } catch (e) { @@ -65,19 +67,19 @@ export class WebDatastore< } } - public async set(key: keyof Tk, value: string): Promise<IClientDatastoreSetResolve> { + public async set(key: keyof Tk, value: string): Promise<ResolveError<ResultObj<string>>> { try { await idb_set(this._key_map[key], value, this.get_store()); - return { pass: true }; + return { result: value }; } catch (e) { return handle_err(e); } } - public async get(key: keyof Tk): Promise<IClientDatastoreGetResolve> { + public async get(key: keyof Tk): Promise<ResolveError<ResultObj<string>>> { try { const value = await idb_get(this._key_map[key], this.get_store()); - if (!value) return err_msg("error.client.datastore.no_result") + if (!value) return err_msg(cl_datastore_error.no_result); return { result: value }; } catch (e) { return handle_err(e); @@ -93,26 +95,27 @@ export class WebDatastore< } } - public async set_obj<T>(key: keyof TkO, value: T): Promise<IClientDatastoreSetResolve> { + public async set_obj<T extends TkO>(key: keyof TkO, value: T): Promise<ResolveError<ResultObj<TkO>>> { try { await idb_set(this._key_obj_map[key], JSON.stringify(value), this.get_store()); - return { pass: true }; + return { result: value }; } catch (e) { return handle_err(e); } } - public async update_obj<T>(key: keyof TkO, value: Partial<T>): Promise<IClientDatastoreSetResolve> { + public async update_obj<T extends TkO>(key: keyof TkO, value: Partial<T>): Promise<ResolveError<ResultObj<TkO>>> { try { const k = this._key_obj_map[key]; + const obj_curr: Record<string, unknown> = {}; const curr = await idb_get(k, this.get_store()); - const obj_u: any = {} - if (curr) for (const [curr_key, curr_val] of Object.entries(JSON.parse(curr))) if (curr_val) obj_u[curr_key] = curr_val; - await idb_set(k, JSON.stringify({ - ...obj_u, - ...value - }), this.get_store()); - return { pass: true }; + if (curr) { + const parsed: unknown = JSON.parse(curr); + if (is_record(parsed)) for (const [curr_key, curr_val] of Object.entries(parsed)) if (curr_val) obj_curr[curr_key] = curr_val; + } + const obj: T = { ...obj_curr, ...value } as any; + await idb_set(k, JSON.stringify(obj), this.get_store()); + return { result: obj }; } catch (e) { return handle_err(e); } @@ -121,14 +124,14 @@ export class WebDatastore< public async get_obj<T>(key: keyof TkO): Promise<ResolveError<ResultObj<T>>> { try { const value = await idb_get(this._key_obj_map[key], this.get_store()); - if (!value) return err_msg("error.client.datastore.no_result") + if (!value) return err_msg(cl_datastore_error.no_result); return { result: JSON.parse(value) }; } catch (e) { return handle_err(e); } } - public async del_obj(key: keyof TkO): Promise<IClientDatastoreDelResolve> { + public async del_obj(key: keyof TkO): Promise<ResolveError<ResultObj<string>>> { try { await idb_del(this._key_obj_map[key], this.get_store()); return { result: key.toString() }; @@ -141,10 +144,10 @@ export class WebDatastore< key: K, key_param: Parameters<Tp[K]>[0], value: string - ): Promise<IClientDatastoreSetPResolve> { + ): Promise<ResolveError<ResultObj<string>>> { try { await idb_set(this._key_param_map[key](key_param), value, this.get_store()); - return { pass: true }; + return { result: value }; } catch (e) { return handle_err(e); } @@ -153,10 +156,10 @@ export class WebDatastore< public async getp<K extends keyof Tp>( key: K, key_param: Parameters<Tp[K]>[0] - ): Promise<IClientDatastoreGetPResolve> { + ): Promise<ResolveError<ResultObj<string>>> { try { const value = await idb_get(this._key_param_map[key](key_param), this.get_store()); - if (!value) return err_msg("error.client.datastore.no_result") + if (!value) return err_msg(cl_datastore_error.no_result); return { result: value }; } catch (e) { return handle_err(e); @@ -166,9 +169,7 @@ export class WebDatastore< public async del_pref(key_prefix: string): Promise<IClientDatastoreDelPrefResolve> { try { const all_keys = await idb_keys(this.get_store()); - console.log(JSON.stringify(all_keys, null, 4), `all_keys`) const filtered_keys = all_keys.filter((k): k is string => (typeof k === "string" && k.startsWith(key_prefix))); - console.log(JSON.stringify(filtered_keys, null, 4), `filtered_keys`) for (const key of filtered_keys) { await idb_del(key, this.get_store()); } @@ -178,7 +179,7 @@ export class WebDatastore< } } - public async keys(): Promise<IClientDatastoreKeysResolve> { + public async keys(): Promise<ResolveError<ResultsList<string>>> { try { const all_keys = await idb_keys(this.get_store()); return { results: all_keys.filter((k): k is string => typeof k === "string") }; @@ -187,7 +188,7 @@ export class WebDatastore< } } - public async reset() { + public async reset(): Promise<ResolveError<ResultPass>> { try { await idb_clear(this.get_store()); return { pass: true } as const; diff --git a/client/src/error.ts b/client/src/error.ts @@ -0,0 +1,10 @@ +export * from "./cipher/error.js"; +export * from "./datastore/error.js"; +export * from "./fs/error.js"; +export * from "./geolocation/error.js"; +export * from "./http/error.js"; +export * from "./keystore/error.js"; +export * from "./notifications/error.js"; +export * from "./radroots/error.js"; +export * from "./sql/error.js"; +export * from "./tangle/error.js"; diff --git a/client/src/fs/error.ts b/client/src/fs/error.ts @@ -0,0 +1,6 @@ +export const cl_fs_error = { + +} as const; + +export type ClientFsError = keyof typeof cl_fs_error; +export type ClientFsErrorMessage = (typeof cl_fs_error)[ClientFsError]; diff --git a/client/src/fs/index.ts b/client/src/fs/index.ts @@ -1,3 +1,4 @@ +export * from "./error.js"; export * from "./types.js"; export * from "./web.js"; diff --git a/client/src/fs/types.ts b/client/src/fs/types.ts @@ -11,12 +11,11 @@ export type IClientFsFileInfo = { createdAt?: number }; - export type IClientFsReadBinResolve = ResolveError<Uint8Array> -export type IClientFs = { +export interface IClientFs { exists(path: string): Promise<ResolveError<boolean>>; open(path: string): Promise<ResolveError<IClientFsOpenResult>>; info(path: string): Promise<ResolveError<IClientFsFileInfo>>; read_bin(path: string): Promise<IClientFsReadBinResolve>; -}; +} diff --git a/client/src/fs/web.ts b/client/src/fs/web.ts @@ -1,8 +1,10 @@ -import { handle_err } from "@radroots/utils"; -import type { IClientFs } from "./types.js"; +import { handle_err, type ResolveError } from "@radroots/utils"; +import type { IClientFs, IClientFsFileInfo, IClientFsOpenResult, IClientFsReadBinResolve } from "./types.js"; -export class WebFs implements IClientFs { - public async exists(path: string) { +export interface IWebFs extends IClientFs {} + +export class WebFs implements IWebFs { + public async exists(path: string): Promise<ResolveError<boolean>> { try { const res = await fetch(path, { method: 'HEAD' }); return res.ok; @@ -11,11 +13,11 @@ export class WebFs implements IClientFs { } } - public async open(path: string) { + public async open(path: string): Promise<ResolveError<IClientFsOpenResult>> { return { path }; } - public async info(path: string) { + public async info(path: string): Promise<ResolveError<IClientFsFileInfo>> { try { const res = await fetch(path, { method: 'HEAD' }); const size_header = res.headers.get('Content-Length'); @@ -26,7 +28,7 @@ export class WebFs implements IClientFs { } } - public async read_bin(path: string) { + public async read_bin(path: string): Promise<IClientFsReadBinResolve> { try { const res = await fetch(path); const buf = await res.arrayBuffer(); diff --git a/client/src/geolocation/error.ts b/client/src/geolocation/error.ts @@ -0,0 +1,11 @@ +export const cl_geolocation_error = { + permission_denied: "error.client.geolocation.permission_denied", + location_unavailable: "error.client.geolocation.location_unavailable", + position_unavailable: "error.client.geolocation.position_unavailable", + timeout: "error.client.geolocation.timeout", + blocked_by_permissions_policy: "error.client.geolocation.blocked_by_permissions_policy", + unknown_error: "error.client.geolocation.unknown_error" +} as const; + +export type ClientGeolocationError = keyof typeof cl_geolocation_error; +export type ClientGeolocationErrorMessage = (typeof cl_geolocation_error)[ClientGeolocationError]; +\ No newline at end of file diff --git a/client/src/geolocation/index.ts b/client/src/geolocation/index.ts @@ -1,3 +1,4 @@ +export * from "./error.js"; export * from "./types.js"; export * from "./web.js"; diff --git a/client/src/geolocation/types.ts b/client/src/geolocation/types.ts @@ -1,16 +1,6 @@ import type { IClientGeolocationPosition, ResolveErrorMsg } from "@radroots/utils"; - -export type ClientGeolocationError = - | "error.client.geolocation.permission_denied" - | "error.client.geolocation.location_unavailable" - | "error.client.geolocation.position_unavailable" - | "error.client.geolocation.timeout" - | "error.client.geolocation.blocked_by_permissions_policy" - | "error.client.geolocation.unknown_error" - | "*"; - -export type IGeolocationIError = ClientGeolocationError; +import { type ClientGeolocationErrorMessage } from "./error.js"; export interface IClientGeolocation { - current(): Promise<ResolveErrorMsg<IClientGeolocationPosition, ClientGeolocationError>>; + current(): Promise<ResolveErrorMsg<IClientGeolocationPosition, ClientGeolocationErrorMessage>>; } diff --git a/client/src/geolocation/web.ts b/client/src/geolocation/web.ts @@ -1,4 +1,5 @@ -import { err_msg } from "@radroots/utils"; +import { err_msg, type IClientGeolocationPosition, type ResolveErrorMsg } from "@radroots/utils"; +import { cl_geolocation_error, type ClientGeolocationErrorMessage } from "./error.js"; import type { IClientGeolocation } from "./types.js"; type GeoPolicyAllows = boolean | "unknown"; @@ -85,27 +86,27 @@ function map_error_key( ) { if (error.code === 1) { if (debug.policy_allows === false) { - return "error.client.geolocation.blocked_by_permissions_policy"; + return cl_geolocation_error.blocked_by_permissions_policy; } - return "error.client.geolocation.permission_denied"; + return cl_geolocation_error.permission_denied; } if (error.code === 2) { - return "error.client.geolocation.position_unavailable"; + return cl_geolocation_error.position_unavailable; } if (error.code === 3) { - return "error.client.geolocation.timeout"; + return cl_geolocation_error.timeout; } - return "error.client.geolocation.unknown_error"; + return cl_geolocation_error.unknown_error; } -export class WebGeolocation implements IClientGeolocation { - public async current() { - if (!navigator.geolocation) { - return err_msg("error.client.geolocation.location_unavailable"); - } +export interface IWebGeolocation extends IClientGeolocation {} + +export class WebGeolocation implements IWebGeolocation { + public async current(): Promise<ResolveErrorMsg<IClientGeolocationPosition, ClientGeolocationErrorMessage>> { + if (!navigator.geolocation) return err_msg(cl_geolocation_error.location_unavailable); const policy_allows = read_policy_allows_geolocation(document); const permission_state = await read_permission_state_geolocation(navigator); @@ -114,7 +115,7 @@ export class WebGeolocation implements IClientGeolocation { if (policy_allows === false) { log_geo_debug("[geolocation] blocked_by_policy", base_debug); - return err_msg("error.client.geolocation.blocked_by_permissions_policy"); + return err_msg(cl_geolocation_error.blocked_by_permissions_policy); } try { @@ -140,7 +141,7 @@ export class WebGeolocation implements IClientGeolocation { } log_geo_debug("[geolocation] unknown_exception", base_debug); - return err_msg("error.client.geolocation.unknown_error"); + return err_msg(cl_geolocation_error.unknown_error); } } } diff --git a/client/src/http/error.ts b/client/src/http/error.ts @@ -0,0 +1,8 @@ +export const cl_http_error = { + init_failure: "error.client.http.init_failure", + fetch_failure: "error.client.http.fetch_failure", + fetch_image_failure: "error.client.http.fetch_image_failure" +} as const; + +export type ClientHttpError = keyof typeof cl_http_error; +export type ClientHttpErrorMessage = (typeof cl_http_error)[ClientHttpError]; diff --git a/client/src/http/index.ts b/client/src/http/index.ts @@ -1,3 +1,3 @@ +export * from "./error.js"; export * from "./types.js"; export * from "./web.js"; - diff --git a/client/src/http/types.ts b/client/src/http/types.ts @@ -1,6 +1,12 @@ import type { IHttpImageResponse, IHttpOpts, IHttpResponse, ResolveError } from "@radroots/utils"; -export type IClientHttp = { +export interface IClientHttp { fetch(opts: IHttpOpts): Promise<ResolveError<IHttpResponse>>; fetch_image(url: string): Promise<ResolveError<IHttpImageResponse>>; +} + +export type WebHttpConfig = { + app_name?: string; + app_version?: string; + app_hash?: string; }; diff --git a/client/src/http/web.ts b/client/src/http/web.ts @@ -1,45 +1,62 @@ -import { handle_err, http_fetch_opts, lib_http_parse_headers, lib_http_parse_response, type FieldRecord, type IHttpOpts } from '@radroots/utils'; -import type { IClientHttp } from "./types.js"; +import { + err_msg, + handle_err, + http_fetch_opts, + lib_http_parse_headers, + lib_http_parse_response, + type IHttpImageResponse, + type IHttpOpts, + type IHttpResponse, + type ResolveError +} from '@radroots/utils'; +import { cl_http_error } from "./error.js"; +import type { IClientHttp, WebHttpConfig } from "./types.js"; -export class WebHttp implements IClientHttp { - private _headers: FieldRecord; +export interface IWebHttp extends IClientHttp { } - constructor() { - this._headers = { - "Content-Type": 'application/json', - "User-Agent": `radroots/1.0.0`, - "X-Radroots-Version": `radroots/*`, - }; +export class WebHttp implements IWebHttp { + private _headers: Headers; + + constructor(http_config?: WebHttpConfig) { + try { + const headers = new Headers({ + "Accept": "application/json", + "Content-Type": "application/json" + }); + if (http_config?.app_name) headers.set("X-Radroots-Client", http_config.app_name); + if (http_config?.app_version) headers.set("X-Radroots-App-Version", http_config.app_version); + if (http_config?.app_hash) headers.set("X-Radroots-App-Commit", http_config.app_hash); + this._headers = headers; + } catch { + throw new Error(cl_http_error.init_failure); + } } - public async init(opts?: { - app_version?: string; - app_hash?: string; - }) { - if (opts?.app_version) this._headers["User-Agent"] = `radroots/${opts.app_version}`; - if (opts?.app_hash) this._headers["X-Radroots-Version"] = `radroots/${opts.app_hash}`; + private apply_default_headers(headers: Headers): void { + this._headers.forEach((value, key) => { + if (!headers.has(key)) headers.set(key, value); + }); } - public async fetch(opts: IHttpOpts) { + public async fetch(opts: IHttpOpts): Promise<ResolveError<IHttpResponse>> { try { const { url, options } = http_fetch_opts(opts); + if (options.headers instanceof Headers) this.apply_default_headers(options.headers); const response = await fetch(url, options); return lib_http_parse_response(response); } catch (e) { - return handle_err(e); + handle_err(e); + return err_msg(cl_http_error.fetch_failure); }; } - public async fetch_image(url: string) { + public async fetch_image(url: string): Promise<ResolveError<IHttpImageResponse>> { try { - const headers: FieldRecord = { - ...this._headers, - }; - const options: RequestInit = { - method: `GET`, + const headers = new Headers(this._headers); + const response = await fetch(url, { + method: "GET", headers, - } - const response = await fetch(url, options); + }); switch (response.ok) { case true: { const blob = await response.blob(); @@ -59,7 +76,8 @@ export class WebHttp implements IClientHttp { } } } catch (e) { - return handle_err(e); + handle_err(e); + return err_msg(cl_http_error.fetch_image_failure); }; } -} -\ No newline at end of file +} diff --git a/client/src/index.ts b/client/src/index.ts @@ -0,0 +1 @@ +export * as error from "./error.js"; diff --git a/client/src/keystore/error.ts b/client/src/keystore/error.ts @@ -0,0 +1,10 @@ +export const cl_keystore_error = { + idb_undefined: "error.client.keystore.idb_undefined", + missing_key: "error.client.keystore.missing_key", + corrupt_data: "error.client.keystore.corrupt_data", + nostr_invalid_secret_key: "error.client.keystore.nostr_invalid_secret_key", + nostr_no_results: "error.client.keystore.nostr_no_results" +} as const; + +export type ClientKeystoreError = keyof typeof cl_keystore_error; +export type ClientKeystoreErrorMessage = (typeof cl_keystore_error)[ClientKeystoreError]; diff --git a/client/src/keystore/index.ts b/client/src/keystore/index.ts @@ -1,4 +1,5 @@ export * from "../cipher/web.js"; +export * from "./error.js"; export * from "./types.js"; export * from "./web-nostr.js"; export * from "./web.js"; diff --git a/client/src/keystore/types.ts b/client/src/keystore/types.ts @@ -2,19 +2,19 @@ import type { ResolveError, ResultObj, ResultPass, ResultPublicKey, ResultSecret export type IClientKeystoreValue = string | null; -export type IClientKeystore = { +export interface IClientKeystore { add(key: string, value: string): Promise<ResolveError<ResultObj<string>>>; remove(key: string): Promise<ResolveError<ResultObj<string>>>; - read(key?: string | null): Promise<ResolveError<ResultObj<IClientKeystoreValue>>>; - keys(key: string): Promise<ResolveError<ResultsList<string>>>; + read(key?: string | null): Promise<ResolveError<ResultObj<string>>>; + keys(): Promise<ResolveError<ResultsList<string>>>; reset(): Promise<ResolveError<ResultPass>>; -}; +} -export type IClientKeystoreNostr = { +export interface IClientKeystoreNostr { generate(): Promise<ResolveError<ResultPublicKey>>; add(secret_key: string): Promise<ResolveError<ResultPublicKey>>; read(public_key: string): Promise<ResolveError<ResultSecretKey>>; keys(): Promise<ResolveError<ResultsList<string>>>; remove(public_key: string): Promise<ResolveError<ResultObj<string>>>; reset(): Promise<ResolveError<ResultPass>>; -}; -\ No newline at end of file +} diff --git a/client/src/keystore/web-nostr.ts b/client/src/keystore/web-nostr.ts @@ -1,10 +1,24 @@ -import { err_msg, handle_err, IdbClientConfig } from '@radroots/utils'; +import { + err_msg, + handle_err, + IdbClientConfig, + type ResolveError, + type ResultObj, + type ResultPass, + type ResultPublicKey, + type ResultSecretKey, + type ResultsList +} from '@radroots/utils'; import { lib_nostr_key_generate, lib_nostr_public_key, lib_nostr_secret_key_validate } from '@radroots/utils-nostr'; +import { cl_keystore_error } from "./error.js"; import type { IClientKeystoreNostr } from './types.js'; import { WebKeystore } from './web.js'; +export interface IWebKeystoreNostr extends IClientKeystoreNostr { + get_config(): IdbClientConfig; +} -export class WebKeystoreNostr implements IClientKeystoreNostr { +export class WebKeystoreNostr implements IWebKeystoreNostr { private keystore_config: IdbClientConfig; private _keystore: WebKeystore; @@ -13,9 +27,9 @@ export class WebKeystoreNostr implements IClientKeystoreNostr { this._keystore = new WebKeystore(this.keystore_config); } - private async add_secret_key(secret_key_raw: string) { + private async add_secret_key(secret_key_raw: string): Promise<ResolveError<ResultObj<string>>> { const secret_key = lib_nostr_secret_key_validate(secret_key_raw); - if (!secret_key) throw new Error("error.nostr.invalid_secret_key"); + if (!secret_key) throw new Error(cl_keystore_error.nostr_invalid_secret_key); const public_key = lib_nostr_public_key(secret_key); return await this._keystore.add(public_key, secret_key); } @@ -24,7 +38,7 @@ export class WebKeystoreNostr implements IClientKeystoreNostr { return this._keystore.get_config(); } - public async generate() { + public async generate(): Promise<ResolveError<ResultPublicKey>> { try { const secret_key = lib_nostr_key_generate(); const resolve = await this.add_secret_key(secret_key); @@ -35,7 +49,7 @@ export class WebKeystoreNostr implements IClientKeystoreNostr { } }; - public async add(secret_key_raw: string) { + public async add(secret_key_raw: string): Promise<ResolveError<ResultPublicKey>> { try { const resolve = await this.add_secret_key(secret_key_raw); if ("err" in resolve) return resolve; @@ -45,7 +59,7 @@ export class WebKeystoreNostr implements IClientKeystoreNostr { } }; - public async read(public_key?: string) { + public async read(public_key?: string): Promise<ResolveError<ResultSecretKey>> { try { const resolve = await this._keystore.read(public_key); if ("err" in resolve) return resolve; @@ -55,18 +69,18 @@ export class WebKeystoreNostr implements IClientKeystoreNostr { } }; - public async keys() { + public async keys(): Promise<ResolveError<ResultsList<string>>> { try { const resolve = await this._keystore.keys(); if ("err" in resolve) return resolve; if (resolve.results.length) return resolve; - return err_msg("error.client.keystore-nostr.no_results"); + return err_msg(cl_keystore_error.nostr_no_results); } catch (e) { return handle_err(e); } }; - public async remove(public_key: string) { + public async remove(public_key: string): Promise<ResolveError<ResultObj<string>>> { try { const resolve = await this._keystore.remove(public_key); if ("err" in resolve) return resolve; @@ -76,7 +90,7 @@ export class WebKeystoreNostr implements IClientKeystoreNostr { } }; - public async reset() { + public async reset(): Promise<ResolveError<ResultPass>> { try { const resolve = await this._keystore.reset(); return resolve; diff --git a/client/src/keystore/web.ts b/client/src/keystore/web.ts @@ -1,9 +1,32 @@ -import { err_msg, handle_err, IdbClientConfig, text_dec, text_enc } from "@radroots/utils"; +import { + err_msg, + handle_err, + IdbClientConfig, + text_dec, + text_enc, + type ResolveError, + type ResultObj, + type ResultPass, + type ResultsList +} from "@radroots/utils"; import { createStore, 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"; -import { WebAesGcmCipher, type WebAesGcmCipherConfig } from "../cipher/web.js"; +import { WebAesGcmCipher, } from "../cipher/web.js"; +import { cl_keystore_error } from "./error.js"; import type { IClientKeystore } from "./types.js"; -export class WebKeystore implements IClientKeystore { +export type WebAesGcmCipherConfig = { + idb_config?: Partial<IdbClientConfig>; + key_name?: string; + key_length?: number; + iv_length?: number; + algorithm?: string; +}; + +export interface IWebKeystore extends IClientKeystore { + get_config(): IdbClientConfig; +} + +export class WebKeystore implements IWebKeystore { private config: IdbClientConfig; private store: UseStore | null; private cipher: WebAesGcmCipher; @@ -29,7 +52,7 @@ export class WebKeystore implements IClientKeystore { private get_store(): UseStore { if (!this.store) { if (typeof indexedDB === "undefined") { - throw new Error("error.client.keystore.idb_undefined"); + throw new Error(cl_keystore_error.idb_undefined); } this.store = createStore(this.config.database, this.config.store); } @@ -43,7 +66,7 @@ export class WebKeystore implements IClientKeystore { }; } - public async add(key: string, value: string) { + public async add(key: string, value: string): Promise<ResolveError<ResultObj<string>>> { try { const bytes = text_enc(value); const cipher_bytes = await this.cipher.encrypt(bytes); @@ -54,7 +77,7 @@ export class WebKeystore implements IClientKeystore { } } - public async remove(key: string) { + public async remove(key: string): Promise<ResolveError<ResultObj<string>>> { try { await idb_del(key, this.get_store()); return { result: key }; @@ -63,15 +86,11 @@ export class WebKeystore implements IClientKeystore { } } - public async read(key?: string | null) { + public async read(key?: string | null): Promise<ResolveError<ResultObj<string>>> { try { - if (!key) { - return err_msg("error.client.keystore.missing_key"); - } + if (!key) return err_msg(cl_keystore_error.missing_key); const cipher_bytes = await idb_get<Uint8Array | null>(key, this.get_store()); - if (!(cipher_bytes instanceof Uint8Array)) { - return err_msg("error.client.keystore.corrupt_data"); - } + if (!(cipher_bytes instanceof Uint8Array)) return err_msg(cl_keystore_error.corrupt_data); const bytes = await this.cipher.decrypt(cipher_bytes); const plain = text_dec(bytes); return { result: plain }; @@ -80,7 +99,7 @@ export class WebKeystore implements IClientKeystore { } } - public async keys() { + public async keys(): Promise<ResolveError<ResultsList<string>>> { try { const all_keys = await idb_keys(this.get_store()); return { results: all_keys.filter((k): k is string => typeof k === "string") }; @@ -89,7 +108,7 @@ export class WebKeystore implements IClientKeystore { } } - public async reset() { + public async reset(): Promise<ResolveError<ResultPass>> { try { await idb_clear(this.get_store()); await this.cipher.reset(); diff --git a/client/src/notifications/error.ts b/client/src/notifications/error.ts @@ -0,0 +1,6 @@ +export const cl_notifications_error = { + unavailable: "error.client.notifications.unavailable" +} as const; + +export type ClientNotificationsError = keyof typeof cl_notifications_error; +export type ClientNotificationsErrorMessage = (typeof cl_notifications_error)[ClientNotificationsError]; diff --git a/client/src/notifications/index.ts b/client/src/notifications/index.ts @@ -1,3 +1,3 @@ +export * from "./error.js"; export * from "./types.js"; export * from "./web.js"; - diff --git a/client/src/notifications/types.ts b/client/src/notifications/types.ts @@ -1,7 +1,6 @@ import { type IResultList } from "@radroots/types-bindings"; import { type ResolveError, type ResolveStatus } from "@radroots/utils"; - export type IClientNotificationsNotifyPermission = "granted" | "denied" | "default" | "unavailable"; export type IClientNotificationsDialogConfirmOpts = @@ -30,10 +29,10 @@ export type IClientNotificationsConfirmResolve = boolean; export type IClientNotificationsNotifyInitResolve = ResolveError<IClientNotificationsNotifyPermission>; export type IClientNotificationsNotifySendResolve = ResolveError<Notification> -export type IClientNotifications = { +export interface IClientNotifications { alert(opts: string, title?: string, status?: ResolveStatus): Promise<IClientNotificationsAlertResolve>; confirm(opts: IClientNotificationsDialogConfirmOpts): Promise<IClientNotificationsConfirmResolve>; notify_init(): Promise<IClientNotificationsNotifyInitResolve>; notify_send(opts: string | IClientNotificationsNotifySendOptions): Promise<IClientNotificationsNotifySendResolve>; - open_photos(): Promise<ResolveError<IResultList<string> | undefined>> + open_photos(): Promise<ResolveError<IResultList<string> | undefined>>; } diff --git a/client/src/notifications/web.ts b/client/src/notifications/web.ts @@ -1,14 +1,26 @@ import { IResultList } from "@radroots/types-bindings"; -import { err_msg, handle_err, type ResolveStatus } from "@radroots/utils"; -import type { IClientNotifications, IClientNotificationsConfig, IClientNotificationsDialogConfirmOpts, IClientNotificationsNotifySendOptions } from "./types.js"; +import { err_msg, handle_err, type ResolveError, type ResolveStatus } from "@radroots/utils"; +import { cl_notifications_error } from "./error.js"; +import type { + IClientNotifications, + IClientNotificationsAlertResolve, + IClientNotificationsConfig, + IClientNotificationsConfirmResolve, + IClientNotificationsDialogConfirmOpts, + IClientNotificationsNotifyInitResolve, + IClientNotificationsNotifySendOptions, + IClientNotificationsNotifySendResolve +} from "./types.js"; -export class WebNotifications implements IClientNotifications { +export interface IWebNotifications extends IClientNotifications {} + +export class WebNotifications implements IWebNotifications { private _config: IClientNotificationsConfig; constructor(config: IClientNotificationsConfig = { app_name: "Radroots" }) { this._config = config; } - public async alert(opts: string, title?: string, kind?: ResolveStatus) { + public async alert(opts: string, title?: string, kind?: ResolveStatus): Promise<IClientNotificationsAlertResolve> { try { const msg = title ? `${title}\n\n${opts}` : opts; window.alert(msg); @@ -19,7 +31,7 @@ export class WebNotifications implements IClientNotifications { } } - public async confirm(opts: IClientNotificationsDialogConfirmOpts) { + public async confirm(opts: IClientNotificationsDialogConfirmOpts): Promise<IClientNotificationsConfirmResolve> { try { const msg = typeof opts === 'string' ? opts : opts.message return window.confirm(msg); @@ -29,7 +41,7 @@ export class WebNotifications implements IClientNotifications { } } - public async notify_init() { + public async notify_init(): Promise<IClientNotificationsNotifyInitResolve> { try { if (!("Notification" in window)) return "unavailable"; if (Notification.permission === 'granted') return "granted"; @@ -39,12 +51,12 @@ export class WebNotifications implements IClientNotifications { } } - public async notify_send(opts: string | IClientNotificationsNotifySendOptions) { + public async notify_send(opts: string | IClientNotificationsNotifySendOptions): Promise<IClientNotificationsNotifySendResolve> { try { - if (!("Notification" in window)) return err_msg("unavailable"); + if (!("Notification" in window)) return err_msg(cl_notifications_error.unavailable); if (Notification.permission !== "granted") { const permission = await this.notify_init(); - if (permission !== "granted") return err_msg("unavailable"); + if (permission !== "granted") return err_msg(cl_notifications_error.unavailable); } if (typeof opts === "string") return new Notification(this._config.app_name, { body: opts }); else return new Notification(opts.title || this._config.app_name, { body: opts.body }); @@ -53,7 +65,7 @@ export class WebNotifications implements IClientNotifications { } } - public async open_photos() { + public async open_photos(): Promise<ResolveError<IResultList<string> | undefined>> { return await new Promise<IResultList<string> | undefined>((resolve) => { const input = document.createElement('input'); input.type = 'file'; diff --git a/client/src/radroots/error.ts b/client/src/radroots/error.ts @@ -0,0 +1,8 @@ +export const cl_radroots_error = { + missing_base_url: "error.client.radroots.missing_base_url", + account_registered: "error.client.radroots.account_registered", + request_failure: "error.client.radroots.request_failure" +} as const; + +export type ClientRadrootsError = keyof typeof cl_radroots_error; +export type ClientRadrootsErrorMessage = (typeof cl_radroots_error)[ClientRadrootsError]; diff --git a/client/src/radroots/index.ts b/client/src/radroots/index.ts @@ -1,3 +1,3 @@ +export * from "./error.js"; export * from "./types.js"; export * from "./web.js"; - diff --git a/client/src/radroots/types.ts b/client/src/radroots/types.ts @@ -1,4 +1,4 @@ -import { MediaResource, ResolveErrorMsg, type ResultObj, type ResultPass } from '@radroots/utils'; +import { MediaResource, ResolveErrorMsg, type ResultObj } from '@radroots/utils'; export type IClientRadrootsAccountsRequestMessage = | string @@ -10,13 +10,13 @@ export type IClientRadrootsAccountsRequestResolve = ResolveErrorMsg<ResultObj<st export type IClientRadrootsAccountsCreate = { tok: string; secret_key: string; }; export type IClientRadrootsAccountsCreateResolve = ResolveErrorMsg<ResultObj<string>, IClientRadrootsAccountsRequestMessage>; export type IClientRadrootsAccountsActivate = { id: string; secret_key: string; }; -export type IClientRadrootsAccountsActivateResolve = ResolveErrorMsg<ResultPass, IClientRadrootsAccountsRequestMessage>; +export type IClientRadrootsAccountsActivateResolve = ResolveErrorMsg<ResultObj<string>, IClientRadrootsAccountsRequestMessage>; export type IClientRadrootsMediaImageUpload = { mime_type?: string; file_data: Uint8Array; secret_key: string; }; export type IClientRadrootsMediaImageUploadResolve = ResolveErrorMsg<MediaResource, IClientRadrootsAccountsRequestMessage>; -export type IClientRadroots = { - accounts_request: (opts: IClientRadrootsAccountsRequest) => Promise<IClientRadrootsAccountsRequestResolve>; - accounts_create: (opts: IClientRadrootsAccountsCreate) => Promise<IClientRadrootsAccountsCreateResolve>; - accounts_activate: (opts: IClientRadrootsAccountsActivate) => Promise<IClientRadrootsAccountsActivateResolve>; - media_image_upload: (opts: IClientRadrootsMediaImageUpload) => Promise<IClientRadrootsMediaImageUploadResolve>; -}; +export interface IClientRadroots { + accounts_request(opts: IClientRadrootsAccountsRequest): Promise<IClientRadrootsAccountsRequestResolve>; + accounts_create(opts: IClientRadrootsAccountsCreate): Promise<IClientRadrootsAccountsCreateResolve>; + accounts_activate(opts: IClientRadrootsAccountsActivate): Promise<IClientRadrootsAccountsActivateResolve>; + media_image_upload(opts: IClientRadrootsMediaImageUpload): Promise<IClientRadrootsMediaImageUploadResolve>; +} diff --git a/client/src/radroots/web.ts b/client/src/radroots/web.ts @@ -1,14 +1,27 @@ import { err_msg, type IHttpResponse, is_err_response, is_error_response, schema_media_resource } from '@radroots/utils'; import { lib_nostr_event_sign_attest } from '@radroots/utils-nostr'; import { WebHttp } from '../http/web.js'; -import type { IClientRadroots, IClientRadrootsAccountsActivate, IClientRadrootsAccountsActivateResolve, IClientRadrootsAccountsCreate, IClientRadrootsAccountsCreateResolve, IClientRadrootsAccountsRequest, IClientRadrootsAccountsRequestResolve, IClientRadrootsMediaImageUpload, IClientRadrootsMediaImageUploadResolve } from "./types.js"; +import { cl_radroots_error } from "./error.js"; +import type { + IClientRadroots, + IClientRadrootsAccountsActivate, + IClientRadrootsAccountsActivateResolve, + IClientRadrootsAccountsCreate, + IClientRadrootsAccountsCreateResolve, + IClientRadrootsAccountsRequest, + IClientRadrootsAccountsRequestResolve, + IClientRadrootsMediaImageUpload, + IClientRadrootsMediaImageUploadResolve +} from "./types.js"; -export class WebClientRadroots implements IClientRadroots { +export interface IWebClientRadroots extends IClientRadroots { } + +export class WebClientRadroots implements IWebClientRadroots { private _base_url: string private _http_client: WebHttp constructor(base_url: string) { - if (!base_url) throw new Error(`Missing base_url`); + if (!base_url) throw new Error(cl_radroots_error.missing_base_url); const parsed_url = new URL(base_url); const sanitized_base_url = `${parsed_url.origin}${parsed_url.pathname}`.replace(/\/+$/, ``); this._base_url = sanitized_base_url; @@ -27,11 +40,11 @@ export class WebClientRadroots implements IClientRadroots { return JSON.stringify(lib_nostr_event_sign_attest(secret_key)); } - public accounts_request = async (opts: IClientRadrootsAccountsRequest): Promise<IClientRadrootsAccountsRequestResolve> => { + public async accounts_request(opts: IClientRadrootsAccountsRequest): Promise<IClientRadrootsAccountsRequestResolve> { const { profile_name, secret_key } = opts const res = await this._http_client.fetch({ url: `${this._base_url}/v1/accounts/request`, - method: `post`, + method: "post", headers: { "X-Nostr-Event": this.create_x_nostr_event(secret_key), }, @@ -43,14 +56,14 @@ export class WebClientRadroots implements IClientRadroots { const tok = this.parse_res_field(res.data.tok); if (tok) return { result: tok }; } - return err_msg(`error.radroots.account_registered`); + return err_msg(cl_radroots_error.account_registered); } - public accounts_create = async (opts: IClientRadrootsAccountsCreate): Promise<IClientRadrootsAccountsCreateResolve> => { + public async accounts_create(opts: IClientRadrootsAccountsCreate): Promise<IClientRadrootsAccountsCreateResolve> { const { tok, secret_key } = opts const res = await this._http_client.fetch({ url: `${this._base_url}/v1/accounts/create`, - method: `post`, + method: "post", headers: { "X-Nostr-Event": this.create_x_nostr_event(secret_key), }, @@ -62,14 +75,14 @@ export class WebClientRadroots implements IClientRadroots { const id = this.parse_res_field(res.data.id); if (id) return { result: id }; } - return err_msg(`error.client.request_failure`); + return err_msg(cl_radroots_error.request_failure); } - public accounts_activate = async (opts: IClientRadrootsAccountsActivate): Promise<IClientRadrootsAccountsActivateResolve> => { + public async accounts_activate(opts: IClientRadrootsAccountsActivate): Promise<IClientRadrootsAccountsActivateResolve> { const { id, secret_key } = opts const res = await this._http_client.fetch({ url: `${this._base_url}/v1/accounts/activate`, - method: `post`, + method: "post", headers: { "X-Nostr-Event": this.create_x_nostr_event(secret_key), }, @@ -77,15 +90,15 @@ export class WebClientRadroots implements IClientRadroots { }); if (is_err_response(res)) return res; if (is_error_response(res)) return err_msg(res.error); - else if (this.is_res_pass(res)) return { pass: true }; - return err_msg(`error.client.request_failure`); + else if (this.is_res_pass(res)) return { result: id }; + return err_msg(cl_radroots_error.request_failure); } - public media_image_upload = async (opts: IClientRadrootsMediaImageUpload): Promise<IClientRadrootsMediaImageUploadResolve> => { + public async media_image_upload(opts: IClientRadrootsMediaImageUpload): Promise<IClientRadrootsMediaImageUploadResolve> { const { mime_type, file_data, secret_key } = opts const res = await this._http_client.fetch({ url: `${this._base_url}/v1/media/image/upload`, - method: `put`, + method: "put", headers: { "Content-Type": mime_type || "image/png", "X-Nostr-Event": this.create_x_nostr_event(secret_key), @@ -96,9 +109,8 @@ export class WebClientRadroots implements IClientRadroots { if (is_error_response(res)) return err_msg(res.error); else if (this.is_res_pass(res)) { const res_data = schema_media_resource.safeParse(res.data); - console.log(`res_data `, res_data) if (res_data.success && res_data.data) return res_data.data; } - return err_msg(`error.client.request_failure`); + return err_msg(cl_radroots_error.request_failure); } } diff --git a/client/src/sql/error.ts b/client/src/sql/error.ts @@ -0,0 +1,5 @@ +export const cl_sql_error = { +} as const; + +export type ClientSqlError = keyof typeof cl_sql_error; +export type ClientSqlErrorMessage = (typeof cl_sql_error)[ClientSqlError]; diff --git a/client/src/sql/index.ts b/client/src/sql/index.ts @@ -1,3 +1,3 @@ +export * from "./error.js"; export * from "./types.js"; export * from "./web.js"; - diff --git a/client/src/sql/types.ts b/client/src/sql/types.ts @@ -23,8 +23,8 @@ export type SqlJsValue = SqlValue; export type SqlJsParams = Readonly<Record<string, SqlJsValue>> | ReadonlyArray<SqlJsValue>; -export type IClientSqlEncryptedStore = { +export interface IClientSqlEncryptedStore { load(): Promise<Uint8Array | null>; save(bytes: Uint8Array): Promise<void>; remove(): Promise<void>; -} -\ No newline at end of file +} diff --git a/client/src/sql/web.ts b/client/src/sql/web.ts @@ -1,5 +1,5 @@ import { IdbClientConfig } from "@radroots/utils"; -import { del as idb_del } from "idb-keyval"; +import { del as idb_del, get as idb_get, set as idb_set } from "idb-keyval"; import type { BindParams, Database, SqlJsStatic, SqlValue, Statement } from "sql.js"; import init_sql_js from "sql.js/dist/sql-wasm.js"; import { WebAesGcmCipher } from "../cipher/web.js"; @@ -20,33 +20,33 @@ class WebSqlEngineEncryptedStore implements IClientSqlEncryptedStore { }); } - async load() { - if (typeof indexedDB === "undefined") { - return null; - } - const { get } = await import("idb-keyval"); - const data = await get(this.db_key); - if (data instanceof Uint8Array) { - return this.cipher.decrypt(data); - } + async load(): Promise<Uint8Array | null> { + if (typeof indexedDB === "undefined") return null; + const data = await idb_get(this.db_key); + if (data instanceof Uint8Array) return this.cipher.decrypt(data); return null; } - async save(bytes: Uint8Array) { - if (typeof indexedDB === "undefined") { - return; - } - const { set } = await import("idb-keyval"); + async save(bytes: Uint8Array): Promise<void> { + if (typeof indexedDB === "undefined") return; const enc = await this.cipher.encrypt(bytes); - await set(this.db_key, enc); + await idb_set(this.db_key, enc); } - async remove() { + async remove(): Promise<void> { await idb_del(this.db_key); } } -export class WebSqlEngine { +export interface IWebSqlEngine { + close(): Promise<void>; + purge_storage(): Promise<void>; + exec(sql: string, params: SqlJsParams): SqlJsExecOutcome; + query(sql: string, params: SqlJsParams): SqlJsResultRow[]; + export_bytes(): Uint8Array; +} + +export class WebSqlEngine implements IWebSqlEngine { private save_timer: number | undefined; private constructor( @@ -83,9 +83,7 @@ export class WebSqlEngine { } private schedule_persist(): void { - if (this.save_timer) { - return; - } + if (this.save_timer) return; this.save_timer = self.setTimeout(async () => { const bytes = this.db.export(); await this.store.save(bytes); @@ -120,11 +118,8 @@ export class WebSqlEngine { private bind(st: Statement, params: SqlJsParams): void { let bind_params: BindParams; - if (Array.isArray(params)) { - bind_params = [...params]; - } else { - bind_params = { ...(params as Readonly<Record<string, SqlValue>>) }; - } + if (Array.isArray(params)) bind_params = [...params]; + else bind_params = { ...(params as Readonly<Record<string, SqlValue>>) }; st.bind(bind_params); } @@ -137,9 +132,7 @@ export class WebSqlEngine { const idx = col_names.indexOf("last_insert_rowid()"); if (idx >= 0) { const v = st.get()[idx]; - if (typeof v === "number") { - last_id = v; - } + if (typeof v === "number") last_id = v; } } @@ -165,9 +158,7 @@ export class WebSqlEngine { while (st.step()) { const row = st.get(); const obj: SqlJsResultRow = {}; - for (let i = 0; i < names.length; i++) { - obj[names[i]] = row[i]; - } + for (let i = 0; i < names.length; i++) obj[names[i]] = row[i]; out.push(obj); } diff --git a/client/src/tangle/error.ts b/client/src/tangle/error.ts @@ -0,0 +1,5 @@ +export const cl_tangle_error = { +} as const; + +export type ClientTangleError = keyof typeof cl_tangle_error; +export type ClientTangleErrorMessage = (typeof cl_tangle_error)[ClientTangleError]; diff --git a/client/src/tangle/index.ts b/client/src/tangle/index.ts @@ -1,3 +1,3 @@ +export * from "./error.js"; export * from "./types.js"; export * from "./web.js"; - diff --git a/client/src/tangle/web.ts b/client/src/tangle/web.ts @@ -220,7 +220,7 @@ export class WebTangleDatabase implements IClientTangleDatabase { async import_backup(backup: TangleDatabaseBackup): Promise<void> { await this.init(); - await tangle_db_import_backup(this.serialize(backup)); + tangle_db_import_backup(this.serialize(backup)); } async farm_create(opts: IFarmCreate): Promise<IFarmCreateResolve | IError<string>> { @@ -438,4 +438,4 @@ export class WebTangleDatabase implements IClientTangleDatabase { return this.deserialize<ITradeProductMediaResolve>(res); } -} -\ No newline at end of file +}