web_lib

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

commit 9d66f40ad598b13b02cdc3edfe032744955170ea
parent 2edab708aa8920b8b8eaf7381478afdaed12010d
Author: triesap <triesap@radroots.dev>
Date:   Mon, 22 Dec 2025 15:04:05 +0000

client: centralize indexeddb configuration, adopt shared http and geolocation modules, harden responses

Diffstat:
Mclient/package.json | 7++-----
Mclient/src/cipher/web.ts | 6++----
Mclient/src/crypto/registry.ts | 6++----
Mclient/src/datastore/web.ts | 6++----
Mclient/src/geolocation/types.ts | 3++-
Mclient/src/geolocation/web.ts | 3++-
Dclient/src/http/error.ts | 8--------
Dclient/src/http/index.ts | 3---
Dclient/src/http/types.ts | 12------------
Dclient/src/http/web.ts | 83-------------------------------------------------------------------------------
Aclient/src/idb/config.ts | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mclient/src/keystore/web-nostr.ts | 7++++++-
Mclient/src/keystore/web.ts | 11+++++++----
Mclient/src/radroots/web.ts | 16+++++++++++-----
Mclient/src/sql/web.ts | 6++----
Mclient/src/tangle/web.ts | 6++----
16 files changed, 87 insertions(+), 143 deletions(-)

diff --git a/client/package.json b/client/package.json @@ -40,11 +40,6 @@ "import": "./dist/esm/geolocation/index.js", "require": "./dist/cjs/geolocation/index.js" }, - "./http": { - "types": "./dist/types/http/index.d.ts", - "import": "./dist/esm/http/index.js", - "require": "./dist/cjs/http/index.js" - }, "./keystore": { "types": "./dist/types/keystore/index.d.ts", "import": "./dist/esm/keystore/index.js", @@ -81,6 +76,8 @@ "watch": "tsc -w" }, "dependencies": { + "@radroots/geo": "*", + "@radroots/http": "*", "@radroots/tangle-schema-bindings": "*", "@radroots/tangle-sql-wasm": "*", "@radroots/utils": "*", diff --git a/client/src/cipher/web.ts b/client/src/cipher/web.ts @@ -4,13 +4,11 @@ import type { WebAesGcmCipherConfig } from "../keystore/web.js"; import { crypto_registry_clear_key_entry, crypto_registry_clear_store_index, crypto_registry_get_store_index } from "../crypto/registry.js"; import { WebCryptoService } from "../crypto/service.js"; import type { LegacyKeyConfig } from "../crypto/types.js"; +import { IDB_CONFIG_CIPHER_AES_GCM } from "../idb/config.js"; import { cl_cipher_error } from "./error.js"; import type { IClientCipher } from "./types.js"; -const DEFAULT_IDB_CONFIG: IdbClientConfig = { - database: "radroots-aes-gcm-keystore", - store: "default" -}; +const DEFAULT_IDB_CONFIG: IdbClientConfig = IDB_CONFIG_CIPHER_AES_GCM; const DEFAULT_WEB_AES_GCM_CONFIG = { key_name: "radroots.aes-gcm.key", diff --git a/client/src/crypto/registry.ts b/client/src/crypto/registry.ts @@ -1,12 +1,10 @@ import { createStore, del as idb_del, get as idb_get, keys as idb_keys, set as idb_set } from "idb-keyval"; import type { IdbClientConfig } from "@radroots/utils"; +import { IDB_CONFIG_CRYPTO_REGISTRY } from "../idb/config.js"; import { cl_crypto_error } from "./error.js"; import type { CryptoKeyEntry, CryptoRegistryExport, CryptoStoreIndex } from "./types.js"; -const CRYPTO_IDB_CONFIG: IdbClientConfig = { - database: "radroots-client-crypto", - store: "default" -}; +const CRYPTO_IDB_CONFIG: IdbClientConfig = IDB_CONFIG_CRYPTO_REGISTRY; const CRYPTO_STORE = createStore(CRYPTO_IDB_CONFIG.database, CRYPTO_IDB_CONFIG.store); const STORE_INDEX_PREFIX = "store:"; diff --git a/client/src/datastore/web.ts b/client/src/datastore/web.ts @@ -3,6 +3,7 @@ import { createStore, clear as idb_clear, del as idb_del, get as idb_get, keys a import type { BackupDatastorePayload } from "../backup/types.js"; import { WebCryptoService } from "../crypto/service.js"; import { crypto_registry_clear_key_entry, crypto_registry_clear_store_index, crypto_registry_get_store_index } from "../crypto/registry.js"; +import { IDB_CONFIG_DATASTORE } from "../idb/config.js"; import { cl_datastore_error } from "./error.js"; import type { IClientDatastore, @@ -12,10 +13,7 @@ import type { IClientDatastoreKeyParamMap } from "./types.js"; -const DEFAULT_IDB_CONFIG: IdbClientConfig = { - database: "radroots-web-datastore", - store: "default", -}; +const DEFAULT_IDB_CONFIG: IdbClientConfig = IDB_CONFIG_DATASTORE; const is_record = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/client/src/geolocation/types.ts b/client/src/geolocation/types.ts @@ -1,4 +1,5 @@ -import type { IClientGeolocationPosition, ResolveErrorMsg } from "@radroots/utils"; +import type { IClientGeolocationPosition } from "@radroots/geo"; +import type { ResolveErrorMsg } from "@radroots/utils"; import { type ClientGeolocationErrorMessage } from "./error.js"; export interface IClientGeolocation { diff --git a/client/src/geolocation/web.ts b/client/src/geolocation/web.ts @@ -1,4 +1,5 @@ -import { err_msg, type IClientGeolocationPosition, type ResolveErrorMsg } from "@radroots/utils"; +import type { IClientGeolocationPosition } from "@radroots/geo"; +import { err_msg, type ResolveErrorMsg } from "@radroots/utils"; import { cl_geolocation_error, type ClientGeolocationErrorMessage } from "./error.js"; import type { IClientGeolocation } from "./types.js"; diff --git a/client/src/http/error.ts b/client/src/http/error.ts @@ -1,8 +0,0 @@ -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 +0,0 @@ -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,12 +0,0 @@ -import type { IHttpImageResponse, IHttpOpts, IHttpResponse, ResolveError } from "@radroots/utils"; - -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,83 +0,0 @@ -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 interface IWebHttp extends IClientHttp { } - -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); - } - } - - 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): 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) { - handle_err(e); - return err_msg(cl_http_error.fetch_failure); - }; - } - - public async fetch_image(url: string): Promise<ResolveError<IHttpImageResponse>> { - try { - const headers = new Headers(this._headers); - const response = await fetch(url, { - method: "GET", - headers, - }); - switch (response.ok) { - case true: { - const blob = await response.blob(); - return { - status: response.status, - url: response.url, - blob, - headers: lib_http_parse_headers(response.headers) - }; - } - case false: { - return { - status: response.status, - url: response.url, - headers: lib_http_parse_headers(response.headers) - }; - } - } - } catch (e) { - handle_err(e); - return err_msg(cl_http_error.fetch_image_failure); - }; - } -} diff --git a/client/src/idb/config.ts b/client/src/idb/config.ts @@ -0,0 +1,47 @@ +import type { IdbClientConfig } from "@radroots/utils"; + +export const RADROOTS_IDB_DATABASE = "radroots-pwa-v1"; + +export const IDB_STORE_DATASTORE = "radroots.app.datastore"; +export const IDB_STORE_KEYSTORE = "radroots.security.keystore"; +export const IDB_STORE_KEYSTORE_NOSTR = "radroots.security.keystore.nostr"; +export const IDB_STORE_CRYPTO_REGISTRY = "radroots.security.crypto.registry"; +export const IDB_STORE_CIPHER_AES_GCM = "radroots.security.cipher.aes-gcm"; +export const IDB_STORE_CIPHER_SQL = "radroots.security.cipher.sql"; +export const IDB_STORE_TANGLE = "radroots.storage.tangle.sql"; +export const IDB_STORE_CIPHER_SUFFIX = ".cipher"; + +export const IDB_CONFIG_DATASTORE: IdbClientConfig = { + database: RADROOTS_IDB_DATABASE, + store: IDB_STORE_DATASTORE +}; + +export const IDB_CONFIG_KEYSTORE: IdbClientConfig = { + database: RADROOTS_IDB_DATABASE, + store: IDB_STORE_KEYSTORE +}; + +export const IDB_CONFIG_KEYSTORE_NOSTR: IdbClientConfig = { + database: RADROOTS_IDB_DATABASE, + store: IDB_STORE_KEYSTORE_NOSTR +}; + +export const IDB_CONFIG_CRYPTO_REGISTRY: IdbClientConfig = { + database: RADROOTS_IDB_DATABASE, + store: IDB_STORE_CRYPTO_REGISTRY +}; + +export const IDB_CONFIG_CIPHER_AES_GCM: IdbClientConfig = { + database: RADROOTS_IDB_DATABASE, + store: IDB_STORE_CIPHER_AES_GCM +}; + +export const IDB_CONFIG_CIPHER_SQL: IdbClientConfig = { + database: RADROOTS_IDB_DATABASE, + store: IDB_STORE_CIPHER_SQL +}; + +export const IDB_CONFIG_TANGLE: IdbClientConfig = { + database: RADROOTS_IDB_DATABASE, + store: IDB_STORE_TANGLE +}; diff --git a/client/src/keystore/web-nostr.ts b/client/src/keystore/web-nostr.ts @@ -12,6 +12,7 @@ import { 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 { IDB_CONFIG_KEYSTORE_NOSTR } from "../idb/config.js"; import { WebKeystore } from './web.js'; export interface IWebKeystoreNostr extends IClientKeystoreNostr { @@ -23,7 +24,11 @@ export class WebKeystoreNostr implements IWebKeystoreNostr { private _keystore: WebKeystore; constructor(config?: Partial<IdbClientConfig>) { - this.keystore_config = { database: config?.database || "radroots-web-keystore-nostr", store: config?.store || "default" }; + const config_base = config ?? {}; + this.keystore_config = { + database: config_base.database ?? IDB_CONFIG_KEYSTORE_NOSTR.database, + store: config_base.store ?? IDB_CONFIG_KEYSTORE_NOSTR.store + }; this._keystore = new WebKeystore(this.keystore_config); } diff --git a/client/src/keystore/web.ts b/client/src/keystore/web.ts @@ -14,6 +14,7 @@ import type { BackupKeystorePayload } from "../backup/types.js"; import { WebCryptoService } from "../crypto/service.js"; import type { LegacyKeyConfig } from "../crypto/types.js"; import { crypto_registry_clear_key_entry, crypto_registry_clear_store_index, crypto_registry_get_store_index } from "../crypto/registry.js"; +import { IDB_CONFIG_KEYSTORE, IDB_STORE_CIPHER_SUFFIX } from "../idb/config.js"; import { cl_keystore_error } from "./error.js"; import type { IClientKeystore, IClientKeystoreValue } from "./types.js"; @@ -40,18 +41,20 @@ export class WebKeystore implements IWebKeystore { private legacy_key_config: LegacyKeyConfig; constructor(config?: IdbClientConfig) { + const config_base = config ?? {}; this.config = { - database: config?.database || "radroots-web-keystore", - store: config?.store || "default" + database: config_base.database ?? IDB_CONFIG_KEYSTORE.database, + store: config_base.store ?? IDB_CONFIG_KEYSTORE.store }; this.store = null; this.store_id = `keystore:${this.config.database}:${this.config.store}`; this.crypto = new WebCryptoService(); + const legacy_store = `${this.config.store}${IDB_STORE_CIPHER_SUFFIX}`; this.legacy_key_config = { idb_config: { - database: `${this.config.database}-cipher`, - store: this.config.store + database: this.config.database, + store: legacy_store }, key_name: `radroots.keystore.${this.config.store}.aes-gcm.key`, iv_length: 12, diff --git a/client/src/radroots/web.ts b/client/src/radroots/web.ts @@ -1,6 +1,6 @@ -import { err_msg, type IHttpResponse, is_err_response, is_error_response, schema_media_resource } from '@radroots/utils'; +import { err_msg, schema_media_resource } from '@radroots/utils'; +import { type IHttpResponse, is_err_response, is_error_response, is_pass_response, WebHttp } from '@radroots/http'; import { lib_nostr_event_sign_attest } from '@radroots/utils-nostr'; -import { WebHttp } from '../http/web.js'; import { cl_radroots_error } from "./error.js"; import type { IClientRadroots, @@ -29,13 +29,17 @@ export class WebClientRadroots implements IWebClientRadroots { } private is_res_pass(res: IHttpResponse): boolean { - return res.data && res.data.pass === true; + return is_pass_response(res.data); } private parse_res_field(field: unknown): string | undefined { if (typeof field === `string` && field) return field; } + private is_record(value: unknown): value is Record<string, unknown> { + return typeof value === "object" && value !== null; + } + private create_x_nostr_event(secret_key: string): string { return JSON.stringify(lib_nostr_event_sign_attest(secret_key)); } @@ -53,7 +57,8 @@ export class WebClientRadroots implements IWebClientRadroots { if (is_err_response(res)) return res; if (is_error_response(res)) return err_msg(res.error); else if (this.is_res_pass(res)) { - const tok = this.parse_res_field(res.data.tok); + const res_data = this.is_record(res.data) ? res.data : null; + const tok = res_data ? this.parse_res_field(res_data["tok"]) : undefined; if (tok) return { result: tok }; } return err_msg(cl_radroots_error.account_registered); @@ -72,7 +77,8 @@ export class WebClientRadroots implements IWebClientRadroots { if (is_err_response(res)) return res; if (is_error_response(res)) return err_msg(res.error); else if (this.is_res_pass(res)) { - const id = this.parse_res_field(res.data.id); + const res_data = this.is_record(res.data) ? res.data : null; + const id = res_data ? this.parse_res_field(res_data["id"]) : undefined; if (id) return { result: id }; } return err_msg(cl_radroots_error.request_failure); diff --git a/client/src/sql/web.ts b/client/src/sql/web.ts @@ -6,12 +6,10 @@ import { backup_b64_to_bytes, backup_bytes_to_b64 } from "../backup/codec.js"; import type { BackupSqlPayload } from "../backup/types.js"; import { WebCryptoService } from "../crypto/service.js"; import type { LegacyKeyConfig } from "../crypto/types.js"; +import { IDB_CONFIG_CIPHER_SQL } from "../idb/config.js"; import type { IClientSqlEncryptedStore, IWebSqlEngine, SqlJsExecOutcome, SqlJsParams, SqlJsResultRow, WebSqlEngineConfig } from "./types.js"; -const DEFAULT_SQL_CIPHER_CONFIG: IdbClientConfig = { - database: "radroots-web-sql-cipher", - store: "default" -}; +const DEFAULT_SQL_CIPHER_CONFIG: IdbClientConfig = IDB_CONFIG_CIPHER_SQL; interface IWebSqlEngineEncryptedStore extends IClientSqlEncryptedStore { get_store_id(): string; diff --git a/client/src/tangle/web.ts b/client/src/tangle/web.ts @@ -130,6 +130,7 @@ import init_wasm, { } from "@radroots/tangle-sql-wasm"; import type { IError } from "@radroots/types-bindings"; import { err_msg, handle_err, type IdbClientConfig } from "@radroots/utils"; +import { IDB_CONFIG_TANGLE } from "../idb/config.js"; import type { SqlJsMigrationRow, SqlJsMigrationState, WebSqlEngineConfig } from "../sql/types.js"; import { WebSqlEngine } from "../sql/web.js"; import { radroots_sql_install_bridges } from "./bridge.js"; @@ -211,10 +212,7 @@ const is_tangle_database_backup = (value: unknown): value is TangleDatabaseBacku }; const DEFAULT_TANGLE_STORE_KEY = "radroots-pwa-v1-tangle-db"; -const DEFAULT_TANGLE_IDB_CONFIG: IdbClientConfig = { - database: "radroots-pwa-v1-tangle", - store: "default" -}; +const DEFAULT_TANGLE_IDB_CONFIG: IdbClientConfig = IDB_CONFIG_TANGLE; export class WebTangleDatabase implements IWebTangleDatabase { private engine: WebSqlEngine | null = null;