// Derives the auto-rotating wire token from the root rotation secret, mirroring the // backend's authtoken.py exactly: base64url(hmac_sha256(secret, str(floor(now/window)))[:18]). // The secret stays on-device; only the short-lived derived token travels, and a leaked // one self-expires when the window rolls. The server also accepts the previous window's // token, so pass windowsBack=1 to retry across a boundary / small clock skew. // 6-hour window (was 1 hour). With the server's grace=2, a token stays valid for // up to 18 hours — so a user who closes the tab and comes back the next morning // isn't greeted with "missing or invalid bearer token" just because the window // rolled. The client + server MUST use the same window or token derivation // diverges; this matches authtoken.py's TOKEN_WINDOW_S default. const WINDOW_S = 21600; // 6 hours — must match the server's TOKEN_WINDOW_S export async function deriveToken(secret: string, windowsBack = 0): Promise { if (!crypto?.subtle) { // WebCrypto needs a secure context; hf.space is always https, dev is localhost. throw new Error("rotating tokens need a secure (https) context"); } const enc = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const win = Math.floor(Date.now() / 1000 / WINDOW_S) - windowsBack; const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, enc.encode(String(win)))); let bin = ""; for (const b of mac.slice(0, 18)) bin += String.fromCharCode(b); // 18 bytes -> exactly 24 url-safe base64 chars, no padding (matches urlsafe_b64encode) return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_"); }