Spaces:
Running on Zero
Running on Zero
| // 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=<token>` 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 }; | |
| } | |
| } | |