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