feat(core): add obscure_email to ae_util and browser reset utilities to core_func

- 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 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-24 14:10:07 -04:00
parent 7ab5ffe224
commit b177fbf3cd
3 changed files with 151 additions and 1 deletions

View File

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

View File

@@ -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<void> {
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<void> {
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<void> {
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');
}
}

View File

@@ -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,