// Per-browser client token — namespaces a user's uploads on the hosted Space so each // person only ever sees and analyzes their own sessions (public-safe), and so they can // be auto-expired/cleared. Sent as the `X-Her-Client` header on the deterministic REST // calls and as the `client` arg on the gradio GPU calls (see gradioApi.js). // // The token is opaque (the server only hashes it to a storage namespace). It persists // in localStorage so a reload keeps your sessions; a `?client=` deep link (the // bulk uploader prints one) binds this browser to the namespace the script uploaded to. const KEY = "her.clientId"; function genId() { try { return crypto.randomUUID(); } catch { return "c-" + Math.random().toString(36).slice(2) + Date.now().toString(36); } } function initId() { let stored = null; try { stored = localStorage.getItem(KEY); } catch { /* private mode */ } let fromUrl = null; try { fromUrl = new URL(window.location.href).searchParams.get("client"); } catch { /* noop */ } if (fromUrl) { // ALWAYS strip the token from the URL so it can't linger in history/referrer or be // re-shared. The deep link is single-use to hand off the uploader's namespace. try { history.replaceState({}, "", window.location.pathname + window.location.hash); } catch { /* noop */ } // Bind to a URL-supplied namespace ONLY with explicit consent — otherwise a crafted // `?client=…` link could fixate a victim's browser onto someone else's namespace and // capture whatever they upload. If it already matches the stored token, no prompt. if (stored && stored === fromUrl) return stored; let ok = false; try { ok = window.confirm( "Open the uploaded sessions linked here?\n\nThis binds this browser to that upload's private session namespace. " + "Only continue if you created this link (e.g. from the her_upload script)." ); } catch { ok = false; } if (ok) { try { localStorage.setItem(KEY, fromUrl); } catch { /* noop */ } return fromUrl; } } if (stored) return stored; const id = genId(); try { localStorage.setItem(KEY, id); } catch { /* noop */ } return id; } let _id = initId(); export function clientId() { return _id; } // Merge the client header into a fetch headers object. export function withClient(headers = {}) { return { ...headers, "X-Her-Client": clientId() }; } // Best-effort wipe of THIS client's uploads when the tab is closed/hidden. The 24h // server-side sweeper is the hard guarantee; this just clears promptly on exit. export function installExitClear() { const fire = () => { try { navigator.sendBeacon("/api/clear?client=" + encodeURIComponent(clientId())); } catch { /* noop */ } }; window.addEventListener("pagehide", fire); window.addEventListener("beforeunload", fire); } // Explicit "clear my data now" (the Clear button). Resolves to the server's result. export async function clearMyData() { try { const r = await fetch("/api/clear?client=" + encodeURIComponent(clientId()), { method: "POST" }); return await r.json(); } catch { return { ok: false }; } }