Spaces:
Running on Zero
Running on Zero
File size: 3,159 Bytes
5f43c7d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | // 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 };
}
}
|