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