import { CONFIG } from "./config.js"; const AUTH_URL = "https://huggingface.co/oauth/authorize"; const TOKEN_URL = "https://huggingface.co/oauth/token"; const redirectUri = () => location.origin + location.pathname; function b64url(bytes) { let s = ""; bytes.forEach((b) => (s += String.fromCharCode(b))); return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function randomString(len) { const a = new Uint8Array(len); crypto.getRandomValues(a); return b64url(a).slice(0, len); } export async function login() { if (!CONFIG.HF_OAUTH_CLIENT_ID) { alert("HF_OAUTH_CLIENT_ID is empty in config.js"); return; } const verifier = randomString(64); const state = randomString(24); const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)); const challenge = b64url(new Uint8Array(digest)); sessionStorage.setItem("hf_pkce", JSON.stringify({ verifier, state })); const params = new URLSearchParams({ client_id: CONFIG.HF_OAUTH_CLIENT_ID, redirect_uri: redirectUri(), response_type: "code", scope: "openid profile write-repos", state, code_challenge: challenge, code_challenge_method: "S256", }); location.href = `${AUTH_URL}?${params}`; } export function logout() { sessionStorage.removeItem("hf_token"); sessionStorage.removeItem("hf_pkce"); } // Call once on load. Returns the access token if this page was opened from the OAuth redirect. export async function handleRedirect() { const url = new URL(location.href); const code = url.searchParams.get("code"); if (!code) return null; const saved = JSON.parse(sessionStorage.getItem("hf_pkce") || "null"); history.replaceState({}, "", location.pathname); if (!saved || saved.state !== url.searchParams.get("state")) { throw new Error("OAuth state mismatch, please sign in again"); } const body = new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirectUri(), client_id: CONFIG.HF_OAUTH_CLIENT_ID, code_verifier: saved.verifier, }); const res = await fetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }); sessionStorage.removeItem("hf_pkce"); if (!res.ok) throw new Error(`token exchange failed (${res.status})`); const json = await res.json(); sessionStorage.setItem("hf_token", json.access_token); return json.access_token; } export async function whoami(token) { const res = await fetch("https://huggingface.co/api/whoami-v2", { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return null; return res.json(); } // Public read. The timestamp query defeats stale caches. export async function readData() { const url = `https://huggingface.co/datasets/${CONFIG.DATA_REPO}/resolve/main/${CONFIG.DATA_FILE}?t=${Date.now()}`; const res = await fetch(url, { cache: "no-store" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } // Admin write: commit the full JSON file to the dataset repo. export async function writeData(token, data) { const bytes = new TextEncoder().encode(JSON.stringify(data, null, 2)); let bin = ""; bytes.forEach((b) => (bin += String.fromCharCode(b))); const content = btoa(bin); const ndjson = [ JSON.stringify({ key: "header", value: { summary: `Update ${CONFIG.DATA_FILE} from dashboard`, description: "" } }), JSON.stringify({ key: "file", value: { path: CONFIG.DATA_FILE, encoding: "base64", content } }), ].join("\n"); const res = await fetch(`https://huggingface.co/api/datasets/${CONFIG.DATA_REPO}/commit/main`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson", }, body: ndjson, }); if (!res.ok) throw new Error(`commit failed (${res.status})`); }