From b177fbf3cd4e03176a09db612a8eabe0d9b90470 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 24 Jun 2026 14:10:07 -0400 Subject: [PATCH] feat(core): add obscure_email to ae_util and browser reset utilities to core_func MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ae_utils: add obscure_email() — first 3 chars + *** + domain; was duplicated in 6 files - core__browser_reset.ts: new module with clear_idb() and clear_all_storage() - clear_idb: indexedDB.databases() with known-names fallback for older browsers - clear_all_storage: mandatory order (SW → Cache Storage → IDB → localStorage → sessionStorage) - optional BrowserResetLogFn callback for step-by-step UI feedback - ae_core_functions: registers both reset functions under core_func; re-exports BrowserResetLogFn Co-Authored-By: Claude Sonnet 4.6 --- src/lib/ae_core/ae_core_functions.ts | 8 +- src/lib/ae_core/core__browser_reset.ts | 130 +++++++++++++++++++++++++ src/lib/ae_utils/ae_utils.ts | 14 +++ 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 src/lib/ae_core/core__browser_reset.ts diff --git a/src/lib/ae_core/ae_core_functions.ts b/src/lib/ae_core/ae_core_functions.ts index bae3ed42..a0d78c0a 100644 --- a/src/lib/ae_core/ae_core_functions.ts +++ b/src/lib/ae_core/ae_core_functions.ts @@ -47,6 +47,9 @@ import { add_url_params, clean_headers } from '$lib/ae_core/core__api_helpers'; import { download_export__obj_type } from '$lib/ae_core/core__export'; +import { clear_idb, clear_all_storage } from '$lib/ae_core/core__browser_reset'; +export type { BrowserResetLogFn } from '$lib/ae_core/core__browser_reset'; + const export_obj = { check_hosted_file_obj_w_hash: check_hosted_file_obj_w_hash, @@ -84,6 +87,9 @@ const export_obj = { download_export__obj_type: download_export__obj_type, generate_qr_code: generate_qr_code, - js_generate_qr_code: js_generate_qr_code + js_generate_qr_code: js_generate_qr_code, + + clear_idb: clear_idb, + clear_all_storage: clear_all_storage, }; export const core_func = export_obj; diff --git a/src/lib/ae_core/core__browser_reset.ts b/src/lib/ae_core/core__browser_reset.ts new file mode 100644 index 00000000..d9a790c4 --- /dev/null +++ b/src/lib/ae_core/core__browser_reset.ts @@ -0,0 +1,130 @@ +import { browser } from '$app/environment'; + +/** + * Optional progress callback for clear_idb() and clear_all_storage(). + * level: 'ok' = success, 'warn' = nothing to do / non-fatal, 'error' = failure, 'info' = general step + */ +export type BrowserResetLogFn = (msg: string, level?: 'info' | 'ok' | 'warn' | 'error') => void; + +// Fallback list for browsers that don't support indexedDB.databases() (pre-2021 Safari etc.) +const KNOWN_IDB_NAMES = [ + 'ae_core_db', 'ae_events_db', 'ae_journals_db', + 'ae_archives_db', 'ae_posts_db', 'ae_idaa_db', + 'ae_sponsorships_db', 'ae_reports_db', +]; + +function delete_one_idb(name: string, log?: BrowserResetLogFn): Promise { + return new Promise((resolve) => { + const req = indexedDB.deleteDatabase(name); + req.onsuccess = () => { log?.(`Deleted IDB database: ${name}`, 'ok'); resolve(); }; + req.onerror = () => { log?.(`ERROR deleting IDB database: ${name}`, 'error'); resolve(); }; + // blocked = another tab still has the DB open; we resolve anyway and continue + req.onblocked = () => { log?.(`IDB delete blocked (open connections): ${name} — will proceed`, 'warn'); resolve(); }; + }); +} + +/** + * Deletes all IndexedDB databases for this origin. + * + * Uses indexedDB.databases() when available; falls back to the known Aether DB + * name list for older browsers. Does not reload — caller decides navigation. + */ +export async function clear_idb(log?: BrowserResetLogFn): Promise { + if (!browser) return; + + try { + if ('databases' in indexedDB) { + const db_list = await indexedDB.databases(); + if (db_list.length === 0) { + log?.('No IndexedDB databases found.', 'warn'); + } else { + for (const db_info of db_list) { + if (!db_info.name) continue; + await delete_one_idb(db_info.name, log); + } + } + } else { + log?.('indexedDB.databases() not available — deleting known Aether databases by name.', 'warn'); + for (const name of KNOWN_IDB_NAMES) { + await delete_one_idb(name, log); + } + } + } catch (err: any) { + log?.(`ERROR clearing IndexedDB: ${err.message}`, 'error'); + } +} + +/** + * Full browser storage reset for this origin. + * + * Clears in mandatory order: SW unregister → Cache Storage → IDB → localStorage → sessionStorage. + * SW and Cache Storage MUST be cleared before IDB/localStorage — otherwise the SW continues + * serving stale JS bundles from its own Cache Storage on the next load, keeping users stuck. + * + * Does not reload — caller decides whether to navigate after (e.g. window.location.href = '/'). + * + * @param log Optional progress callback. Receives each step message and a severity level. + * Use this to surface step-by-step feedback to the user (e.g. the fix-sw page). + * Omit for silent operation (e.g. the sys bar reset buttons). + */ +export async function clear_all_storage(log?: BrowserResetLogFn): Promise { + if (!browser) return; + + log?.('Starting full storage reset...', 'info'); + + // 1. Unregister all service workers + if ('serviceWorker' in navigator) { + try { + const registrations = await navigator.serviceWorker.getRegistrations(); + if (registrations.length === 0) { + log?.('No active service worker registrations found.', 'warn'); + } else { + log?.(`Found ${registrations.length} service worker registration(s).`, 'info'); + for (const reg of registrations) { + const ok = await reg.unregister(); + log?.(`Unregistered SW at scope: ${reg.scope} (success: ${ok})`, 'ok'); + } + } + } catch (err: any) { + log?.(`ERROR unregistering service workers: ${err.message}`, 'error'); + } + } else { + log?.('Service Workers not supported in this browser.', 'warn'); + } + + // 2. Clear all Cache Storage caches + try { + const cache_keys = await caches.keys(); + if (cache_keys.length === 0) { + log?.('No Cache Storage entries found.', 'warn'); + } else { + for (const key of cache_keys) { + await caches.delete(key); + log?.(`Cleared cache: ${key}`, 'ok'); + } + } + } catch (err: any) { + log?.(`ERROR clearing Cache Storage: ${err.message}`, 'error'); + } + + // 3. Clear all IndexedDB databases (handles enumeration + fallback internally) + await clear_idb(log); + + // 4. Clear localStorage + try { + const local_count = localStorage.length; + localStorage.clear(); + log?.(`Cleared localStorage (${local_count} item(s)).`, 'ok'); + } catch (err: any) { + log?.(`ERROR clearing localStorage: ${err.message}`, 'error'); + } + + // 5. Clear sessionStorage + try { + const session_count = sessionStorage.length; + sessionStorage.clear(); + log?.(`Cleared sessionStorage (${session_count} item(s)).`, 'ok'); + } catch (err: any) { + log?.(`ERROR clearing sessionStorage: ${err.message}`, 'error'); + } +} diff --git a/src/lib/ae_utils/ae_utils.ts b/src/lib/ae_utils/ae_utils.ts index 2bdfcd25..9ef089b2 100644 --- a/src/lib/ae_utils/ae_utils.ts +++ b/src/lib/ae_utils/ae_utils.ts @@ -267,6 +267,19 @@ export const shorten_string = function shorten_string({ return new_string; }; +/** + * Returns a partially-obscured email address for display to non-trusted users. + * Shows the first 3 characters of the local part + *** + the full domain. + * Example: "john.doe@example.com" → "joh***@example.com" + * Trusted staff should receive the full address; pass it through unmodified. + */ +export function obscure_email(email: string | null | undefined): string { + if (!email) return ''; + const at = email.indexOf('@'); + if (at < 0) return email; + return `${email.slice(0, Math.min(3, at))}***${email.slice(at)}`; +} + /** * Strips HTML tags from a string. */ @@ -352,6 +365,7 @@ export const ae_util = { to_title_case: to_title_case, shorten_string: shorten_string, shorten_filename: shorten_filename, + obscure_email: obscure_email, strip_html: strip_html, file_extension_icon: file_extension_icon, file_extension_icon_lucide: file_extension_icon_lucide,