Spaces:
Running
Running
Download space-auth.js from embedl/hfviewer: direct link, hf CLI and curl.
- Browser
- Download file 33 kB
-
https://huggingface.co/spaces/embedl/hfviewer/resolve/main/space-auth.js
- Command line
-
hf download hf://spaces/embedl/hfviewer/space-auth.js
-
curl -L -o space-auth.js https://huggingface.co/spaces/embedl/hfviewer/resolve/main/space-auth.js
33 kB
| const STORAGE_KEY = "hfviewer:space-native-oauth:v2"; | |
| const START_URL = "https://hfviewer.com/api/hf_space_oauth/start"; | |
| const CONSUME_URL = "https://hfviewer.com/api/hf_space_oauth/consume"; | |
| const DESTINATION_URL = "https://hfviewer.com/api/hf_space_oauth/destination"; | |
| const POLL_INTERVAL_MS = 800; | |
| const FLOW_TIMEOUT_MS = 5 * 60 * 1000; | |
| const PREPARE_REFRESH_MS = 4 * 60 * 1000; | |
| const PREPARE_RETRY_DELAYS_MS = [0, 450, 1400]; | |
| // Recovery after a failed preparation backs off and then stops: a page load | |
| // never loops on an unreachable start endpoint (a copied Space once emitted | |
| // ~15 failures per visitor that way). The next sign-in intent starts over. | |
| const PREPARE_RECOVERY_DELAYS_MS = [2000, 8000, 30000, 60000]; | |
| // Hard ceiling on consecutive failed start requests that were not | |
| // user-initiated; a successful preparation resets it. | |
| const PREPARE_MAX_BACKGROUND_ATTEMPTS = 6; | |
| // A user may retry by clicking, but a page reports at most this many | |
| // user-initiated preparation failures. | |
| const PREPARE_MAX_USER_FAILURE_REPORTS = 3; | |
| const OFFICIAL_SPACE_URL = "https://huggingface.co/spaces/embedl/hfviewer"; | |
| const REQUEST_TIMEOUT_MS = 12 * 1000; | |
| const AUTH_CHANGED_EVENT = "hfviewer-space-auth-changed"; | |
| const OFFICIAL_SPACE_HOSTS = new Set([ | |
| "embedl-hfviewer.static.hf.space", | |
| "hfvwr-hfviewer-private-qa.static.hf.space", | |
| ]); | |
| // The Space shell intercepts canonical hfviewer.com links during capture and | |
| // treats every root-path URL as "return to the Space landing page", even when | |
| // its query opens an account view. Use the canonicalizing www host so these | |
| // remain ordinary external links; its 301 preserves the full query string. | |
| const ACCOUNT_DESTINATION_ORIGIN = "https://www.hfviewer.com"; | |
| const authHost = document.getElementById("hfnx-auth"); | |
| const compactAuthHost = document.getElementById("sx-auth"); | |
| let oauthResult = readStoredResult(); | |
| let rendering = false; | |
| let activeFlow = null; | |
| let preparedFlow = null; | |
| let preparePromise = null; | |
| let prepareFailureCount = 0; | |
| let prepareRecoveryTimer = null; | |
| let prepareFailureEpisodeActive = false; | |
| let prepareSuccessReported = false; | |
| let preparePromiseTrigger = ""; | |
| let prepareIntentEscalated = false; | |
| let backgroundPrepareAttempts = 0; | |
| let backgroundPrepareFailureReported = false; | |
| let userPrepareFailureReports = 0; | |
| let preparedFlowExpiryTimer = null; | |
| let loginContinueRequested = false; | |
| let accountDestinationLinks = []; | |
| let lastAnnouncedAuthIdentity = null; | |
| let nativeLoginPopover = null; | |
| function currentSpaceDeployment() { | |
| const host = String(window.location.hostname || "").trim().toLowerCase(); | |
| if (host === "embedl-hfviewer.static.hf.space") return "production"; | |
| if (host === "hfvwr-hfviewer-private-qa.static.hf.space") return "qa"; | |
| if (host.endsWith(".hf.space")) return "clone"; | |
| return "local"; | |
| } | |
| // OAuth only works on the two official Space origins (the backend trusts | |
| // them for the handoff). A duplicated Space can copy our space_repo value, | |
| // but it can never sign in, so it must not prepare OAuth or report failures. | |
| function isCloneDeployment() { | |
| return currentSpaceDeployment() === "clone"; | |
| } | |
| function preparedFlowIsFresh() { | |
| return !!preparedFlow && Date.now() - preparedFlow.preparedAt < PREPARE_REFRESH_MS; | |
| } | |
| function readStoredResult() { | |
| try { | |
| const value = localStorage.getItem(STORAGE_KEY); | |
| const result = value ? JSON.parse(value) : null; | |
| const expiresAt = Number(result?.spaceSession?.expiresAt || 0) * 1000; | |
| if (!result || !expiresAt || expiresAt <= Date.now()) { | |
| localStorage.removeItem(STORAGE_KEY); | |
| return null; | |
| } | |
| return result; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function writeStoredResult(value) { | |
| try { | |
| if (value) localStorage.setItem(STORAGE_KEY, JSON.stringify(value)); | |
| else localStorage.removeItem(STORAGE_KEY); | |
| } catch { | |
| // Keep the current-tab session even when durable storage is unavailable. | |
| } | |
| } | |
| function randomFlowValue() { | |
| const bytes = new Uint8Array(32); | |
| crypto.getRandomValues(bytes); | |
| return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join(""); | |
| } | |
| function userFromResult(result) { | |
| const raw = result?.userInfo || result?.userinfo || result?.user_info || result?.user || {}; | |
| const username = String( | |
| raw.preferred_username || raw.preferredUsername || raw.username || raw.name || "" | |
| ).trim(); | |
| if (!username) return null; | |
| return { | |
| username, | |
| name: String(raw.name || username).trim(), | |
| picture: String(raw.picture || raw.avatar_url || raw.avatarUrl || "").trim(), | |
| profile: String(raw.profile || `https://huggingface.co/${encodeURIComponent(username)}`).trim(), | |
| subject: String(raw.sub || raw.id || "").trim(), | |
| email: String(raw.email || "").trim().toLowerCase(), | |
| }; | |
| } | |
| function trackAuthEvent(eventName, properties = {}) { | |
| try { | |
| const productContext = | |
| window.HFVIEWER_PRODUCT_CONTEXT && typeof window.HFVIEWER_PRODUCT_CONTEXT === "object" | |
| ? window.HFVIEWER_PRODUCT_CONTEXT | |
| : {}; | |
| window.hfviewerAnalyticsBridge?.capture?.(eventName, { | |
| product_surface: "hf_space", | |
| space_repo: String(productContext.space_repo || "unknown"), | |
| space_revision: String(productContext.space_revision || "unknown"), | |
| space_host: String(window.location.hostname || "unknown").toLowerCase(), | |
| space_deployment: currentSpaceDeployment(), | |
| auth_provider: "hugging_face", | |
| ...properties, | |
| }); | |
| } catch { | |
| // Authentication must remain independent of analytics availability. | |
| } | |
| } | |
| function identifyAuthenticatedUser(user, source) { | |
| if (!user?.username) return; | |
| try { | |
| window.hfviewerAnalyticsBridge?.identifyHfSession?.( | |
| { | |
| authenticated: true, | |
| email: user.email || "", | |
| user: { | |
| id: user.subject || "", | |
| username: user.username, | |
| name: user.name || user.username, | |
| email: user.email || "", | |
| }, | |
| }, | |
| source || "hf_space_native_oauth" | |
| ); | |
| } catch { | |
| // Consent-gated identity is best effort only. | |
| } | |
| } | |
| function keepReleasePills() { | |
| if (!authHost) return []; | |
| return Array.from(authHost.children).filter((element) => | |
| element.classList.contains("hfnx-auth__pill") | |
| ); | |
| } | |
| function makeAvatar(user) { | |
| if (user.picture) { | |
| const image = document.createElement("img"); | |
| image.className = "hfnx-auth__avatar"; | |
| image.src = user.picture; | |
| image.alt = ""; | |
| image.referrerPolicy = "no-referrer"; | |
| return image; | |
| } | |
| const letter = document.createElement("span"); | |
| letter.className = "hfnx-auth__avatar hfnx-auth__avatar--letter"; | |
| letter.textContent = user.username.charAt(0).toUpperCase(); | |
| return letter; | |
| } | |
| function makeHfviewerAccountLink(label, path, target) { | |
| const link = document.createElement("a"); | |
| link.className = "hfnx-auth__item"; | |
| link.href = new URL(path, ACCOUNT_DESTINATION_ORIGIN).href; | |
| link.target = "_blank"; | |
| link.rel = "noopener"; | |
| link.textContent = label; | |
| link.setAttribute("role", "menuitem"); | |
| link.dataset.accountDestination = target; | |
| link.dataset.accountDestinationReady = "0"; | |
| accountDestinationLinks.push(link); | |
| link.addEventListener("click", () => { | |
| trackAuthEvent("space_hfviewer_account_destination_opened", { target }); | |
| // The clicked handoff is single-use. Mint its replacement after the | |
| // browser has captured this anchor's current href for native navigation. | |
| link.dataset.accountDestinationReady = "0"; | |
| window.setTimeout(() => { void prepareAccountDestination(link); }, 0); | |
| }); | |
| return link; | |
| } | |
| function liveSpaceSession() { | |
| const session = oauthResult?.spaceSession; | |
| const expiresAt = Number(session?.expiresAt || 0) * 1000; | |
| if (!session?.id || !session?.secret || !expiresAt || expiresAt <= Date.now()) { | |
| return null; | |
| } | |
| return session; | |
| } | |
| function invalidAuthResponse(code) { | |
| const error = new Error("Sign-in is temporarily unavailable. Please try again."); | |
| error.name = "AuthResponseError"; | |
| error.code = code; | |
| return error; | |
| } | |
| function validatedLoginUrl(result, destination = false) { | |
| if (!result || typeof result.login_url !== "string" || !result.login_url.trim()) { | |
| throw invalidAuthResponse("missing_login_url"); | |
| } | |
| let url; | |
| try { url = new URL(result.login_url); } | |
| catch { throw invalidAuthResponse("invalid_login_url"); } | |
| const allowed = destination | |
| ? ["https://hfviewer.com", "https://www.hfviewer.com"].includes(url.origin) && url.pathname === "/api/hf_space_oauth/enter" | |
| : url.origin === "https://huggingface.co" && url.pathname === "/oauth/authorize"; | |
| if (!allowed || url.username || url.password) throw invalidAuthResponse("invalid_login_url"); | |
| return url; | |
| } | |
| function authFailureCategory(error) { | |
| if (error?.name === "AuthResponseError") return "invalid_response"; | |
| if (error?.name === "AbortError" || error?.name === "TimeoutError") return "timeout"; | |
| if (Number(error?.status)) return "http"; | |
| if (error?.name === "TypeError") return "network"; | |
| return "unknown"; | |
| } | |
| async function prepareAccountDestination(link) { | |
| const session = liveSpaceSession(); | |
| const destination = String(link?.dataset?.accountDestination || ""); | |
| if (!session || !destination || link.dataset.accountDestinationPreparing === "1") return; | |
| link.dataset.accountDestinationPreparing = "1"; | |
| try { | |
| const result = await postJson(DESTINATION_URL, { | |
| link_session_id: session.id, | |
| link_session_secret: session.secret, | |
| destination, | |
| }); | |
| const loginUrl = validatedLoginUrl(result, true); | |
| link.href = loginUrl.toString(); | |
| link.dataset.accountDestinationReady = "1"; | |
| } catch (error) { | |
| console.warn("Hugging Face Space account handoff preparation failed", error); | |
| link.dataset.accountDestinationReady = "0"; | |
| trackAuthEvent("space_hf_account_destination_preparation_failed", { | |
| user_initiated: false, | |
| failure_category: authFailureCategory(error), | |
| error_code: error?.code, | |
| component: "space_account_destination", | |
| failure_stage: "destination_prepare", | |
| destination, | |
| error_name: String(error?.name || "Error").slice(0, 80), | |
| }); | |
| } finally { | |
| link.dataset.accountDestinationPreparing = "0"; | |
| } | |
| } | |
| function prepareAccountDestinations() { | |
| if (!liveSpaceSession()) { | |
| if (oauthResult) { | |
| oauthResult = null; | |
| writeStoredResult(null); | |
| renderAuth(); | |
| void prepareLogin("background_session_expired"); | |
| } | |
| return; | |
| } | |
| for (const link of accountDestinationLinks) void prepareAccountDestination(link); | |
| } | |
| function makeOfficialSpaceLink() { | |
| const link = document.createElement("a"); | |
| link.dataset.spaceNativeAuth = "official-space"; | |
| // Reuse the login control's class: the Space stylesheet hides every other | |
| // child of #hfnx-auth. | |
| link.className = "space-native-auth-login space-native-auth-official"; | |
| link.href = OFFICIAL_SPACE_URL; | |
| link.target = "_blank"; | |
| link.rel = "noopener"; | |
| link.textContent = "Open the official hfviewer Space"; | |
| link.title = "Sign-in is available in the official hfviewer Space"; | |
| link.style.fontSize = "12px"; | |
| link.style.opacity = "0.8"; | |
| link.style.whiteSpace = "nowrap"; | |
| link.addEventListener("click", () => { | |
| trackAuthEvent("space_clone_official_space_opened", { component: "space_nav" }); | |
| }); | |
| return link; | |
| } | |
| // Sign-in intent (hover, keyboard focus, press) re-prepares a missing or | |
| // stale OAuth URL so a failed background preparation recovers before the click. | |
| function bindLoginIntentPreparation(element) { | |
| const prepareOnIntent = (event) => { void prepareLogin(`intent_${event.type}`); }; | |
| for (const type of ["pointerenter", "focus", "pointerdown", "touchstart"]) { | |
| element.addEventListener(type, prepareOnIntent, type === "touchstart" ? { passive: true } : undefined); | |
| } | |
| } | |
| function makeLoggedOutControl() { | |
| if (isCloneDeployment()) return makeOfficialSpaceLink(); | |
| const link = document.createElement("a"); | |
| link.dataset.spaceNativeAuth = "login"; | |
| link.className = "space-native-auth-login"; | |
| const ready = !activeFlow && preparedFlowIsFresh(); | |
| link.textContent = activeFlow ? "Signing in…" : "Log in"; | |
| // A click that had to prepare first needs a second click to open the | |
| // native target=_blank navigation; say so instead of looking unchanged. | |
| if (ready && loginContinueRequested) link.textContent = "Continue to Hugging Face"; | |
| link.title = activeFlow | |
| ? "Cancel this sign-in and try again" | |
| : "Sign in with Hugging Face"; | |
| link.href = (ready && preparedFlow?.loginUrl) || "#"; | |
| link.target = "_blank"; | |
| link.rel = "noopener"; | |
| link.setAttribute("aria-disabled", ready || activeFlow ? "false" : "true"); | |
| bindLoginIntentPreparation(link); | |
| link.addEventListener("click", (event) => { | |
| if (activeFlow) { | |
| event.preventDefault(); | |
| failLogin(new Error("Sign-in cancelled"), "handoff_cancelled"); | |
| return; | |
| } | |
| if (!preparedFlowIsFresh()) { | |
| event.preventDefault(); | |
| loginContinueRequested = true; | |
| void prepareLogin("user_nav_click"); | |
| return; | |
| } | |
| startLogin(preparedFlow, link); | |
| }); | |
| return link; | |
| } | |
| function closeNativeLoginPopover() { | |
| const popover = nativeLoginPopover; | |
| nativeLoginPopover = null; | |
| if (!popover) return; | |
| window.removeEventListener("resize", popover.reposition); | |
| window.removeEventListener("scroll", popover.reposition, true); | |
| document.removeEventListener("pointerdown", popover.onPointerDown, true); | |
| document.removeEventListener("keydown", popover.onKeyDown, true); | |
| popover.element.remove(); | |
| } | |
| function positionNativeLoginPopover() { | |
| const popover = nativeLoginPopover; | |
| if (!popover?.anchor?.isConnected) { | |
| closeNativeLoginPopover(); | |
| return; | |
| } | |
| const rect = popover.anchor.getBoundingClientRect(); | |
| const width = popover.element.offsetWidth || 320; | |
| const margin = 12; | |
| const left = Math.max( | |
| margin, | |
| Math.min(rect.left + rect.width / 2 - width / 2, window.innerWidth - width - margin) | |
| ); | |
| popover.element.style.left = `${Math.round(left + window.scrollX)}px`; | |
| popover.element.style.top = `${Math.round(rect.bottom + window.scrollY + 10)}px`; | |
| } | |
| function requestNativeLogin( | |
| anchor, | |
| { | |
| title = "Sign in with Hugging Face", | |
| description = "Continue to use this feature in the Space.", | |
| intent = "protected_action", | |
| } = {} | |
| ) { | |
| if (userFromResult(oauthResult) && liveSpaceSession()) return false; | |
| if (!anchor?.isConnected) return false; | |
| closeNativeLoginPopover(); | |
| const element = document.createElement("div"); | |
| element.className = "hf-watch-popover hf-login-popover space-native-login-popover"; | |
| element.setAttribute("role", "dialog"); | |
| element.setAttribute("aria-label", title); | |
| const close = document.createElement("button"); | |
| close.type = "button"; | |
| close.className = "hf-watch-popover__close"; | |
| close.setAttribute("aria-label", "Close"); | |
| close.textContent = "×"; | |
| const titleElement = document.createElement("p"); | |
| titleElement.className = "hf-watch-popover__title"; | |
| titleElement.textContent = title; | |
| const descriptionElement = document.createElement("p"); | |
| descriptionElement.className = "hf-watch-popover__sub"; | |
| descriptionElement.textContent = description; | |
| const continueLink = document.createElement("a"); | |
| continueLink.className = "hf-login-popover__btn"; | |
| continueLink.target = "_blank"; | |
| continueLink.rel = "noopener"; | |
| continueLink.setAttribute("aria-disabled", "true"); | |
| continueLink.innerHTML = | |
| '<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="" width="22" height="20">' + | |
| '<span>Preparing sign in…</span>'; | |
| element.append(close, titleElement, descriptionElement, continueLink); | |
| const clone = isCloneDeployment(); | |
| if (clone) { | |
| descriptionElement.textContent = "Sign-in is only available in the official hfviewer Space."; | |
| continueLink.href = OFFICIAL_SPACE_URL; | |
| continueLink.setAttribute("aria-disabled", "false"); | |
| const label = continueLink.querySelector("span"); | |
| if (label) label.textContent = "Open the official hfviewer Space"; | |
| } else { | |
| bindLoginIntentPreparation(continueLink); | |
| } | |
| document.body.appendChild(element); | |
| const popover = { | |
| element, | |
| anchor, | |
| intent, | |
| continueLink, | |
| reposition: () => positionNativeLoginPopover(), | |
| onPointerDown: (event) => { | |
| if (!element.contains(event.target) && !anchor.contains(event.target)) { | |
| closeNativeLoginPopover(); | |
| } | |
| }, | |
| onKeyDown: (event) => { | |
| if (event.key === "Escape") closeNativeLoginPopover(); | |
| }, | |
| }; | |
| nativeLoginPopover = popover; | |
| close.addEventListener("click", closeNativeLoginPopover); | |
| continueLink.addEventListener("click", (event) => { | |
| if (clone) { | |
| trackAuthEvent("space_clone_official_space_opened", { | |
| component: "space_native_login_popover", | |
| login_intent: intent, | |
| }); | |
| return; | |
| } | |
| if (!preparedFlowIsFresh() || activeFlow) { | |
| event.preventDefault(); | |
| if (!activeFlow) void prepareLogin("user_continue_click").then(() => refreshNativeLoginPopover()); | |
| return; | |
| } | |
| trackAuthEvent("space_hf_login_continue_clicked", { | |
| component: "space_native_login_popover", | |
| login_intent: intent, | |
| }); | |
| startLogin(preparedFlow, continueLink); | |
| }); | |
| window.addEventListener("resize", popover.reposition); | |
| window.addEventListener("scroll", popover.reposition, true); | |
| document.addEventListener("pointerdown", popover.onPointerDown, true); | |
| document.addEventListener("keydown", popover.onKeyDown, true); | |
| positionNativeLoginPopover(); | |
| trackAuthEvent("space_hf_login_preflight_shown", { | |
| component: "space_native_login_popover", | |
| login_intent: intent, | |
| }); | |
| if (!clone) void prepareLogin("user_preflight").then(() => refreshNativeLoginPopover()); | |
| return true; | |
| } | |
| function refreshNativeLoginPopover() { | |
| const popover = nativeLoginPopover; | |
| if (!popover) return; | |
| if (isCloneDeployment()) { | |
| positionNativeLoginPopover(); | |
| return; | |
| } | |
| if (preparedFlowIsFresh() && preparedFlow.loginUrl && !activeFlow) { | |
| popover.continueLink.href = preparedFlow.loginUrl; | |
| popover.continueLink.setAttribute("aria-disabled", "false"); | |
| const label = popover.continueLink.querySelector("span"); | |
| if (label) label.textContent = "Continue with Hugging Face"; | |
| } | |
| positionNativeLoginPopover(); | |
| } | |
| function makeLoggedInControl(user, { compact = false } = {}) { | |
| const wrap = document.createElement("div"); | |
| wrap.dataset.spaceNativeAuth = "profile"; | |
| wrap.className = "space-native-auth-wrap"; | |
| if (compact) wrap.classList.add("space-native-auth-wrap--compact"); | |
| const chip = document.createElement("button"); | |
| chip.type = "button"; | |
| chip.className = "hfnx-auth__chip"; | |
| chip.setAttribute("aria-haspopup", "menu"); | |
| chip.setAttribute("aria-expanded", "false"); | |
| chip.appendChild(makeAvatar(user)); | |
| const label = document.createElement("span"); | |
| label.textContent = user.username; | |
| chip.appendChild(label); | |
| const menu = document.createElement("div"); | |
| menu.className = "hfnx-auth__menu space-native-auth-menu"; | |
| menu.setAttribute("role", "menu"); | |
| const myModels = makeHfviewerAccountLink( | |
| "My models", | |
| "/?my_models=1&blogs_with_graphs=1&next_landing=1", | |
| "my_models" | |
| ); | |
| const bookmarks = makeHfviewerAccountLink( | |
| "Bookmarks", | |
| "/?bookmarks=1&blogs_with_graphs=1&next_landing=1", | |
| "bookmarks" | |
| ); | |
| const releaseWatchlist = makeHfviewerAccountLink( | |
| "Release watchlist", | |
| "/?release_watchlist=1&blogs_with_graphs=1&next_landing=1", | |
| "release_watchlist" | |
| ); | |
| const writeArticle = makeHfviewerAccountLink( | |
| "Write article", | |
| "/?article_picker=1&login_intent=interactive_articles&blogs_with_graphs=1&next_landing=1", | |
| "write_article" | |
| ); | |
| const logout = document.createElement("button"); | |
| logout.type = "button"; | |
| logout.className = "hfnx-auth__item"; | |
| logout.textContent = "Log out"; | |
| logout.setAttribute("role", "menuitem"); | |
| logout.addEventListener("click", () => { | |
| trackAuthEvent("space_hf_logout"); | |
| oauthResult = null; | |
| writeStoredResult(null); | |
| renderAuth(); | |
| // Logged-in page loads intentionally do not prepare an OAuth flow. Once | |
| // the profile is cleared, prepare one immediately so the replacement Log | |
| // in control is a real link on the first click. | |
| void prepareLogin("background_post_logout"); | |
| }); | |
| const setOpen = (open) => { | |
| menu.classList.toggle("is-open", open); | |
| chip.setAttribute("aria-expanded", open ? "true" : "false"); | |
| }; | |
| chip.addEventListener("click", (event) => { | |
| event.stopPropagation(); | |
| setOpen(!menu.classList.contains("is-open")); | |
| }); | |
| document.addEventListener("click", () => setOpen(false)); | |
| menu.append(myModels, bookmarks, releaseWatchlist, writeArticle, logout); | |
| wrap.append(chip, menu); | |
| return wrap; | |
| } | |
| function renderAuth() { | |
| if ((!authHost && !compactAuthHost) || rendering) return; | |
| rendering = true; | |
| accountDestinationLinks = []; | |
| const pills = keepReleasePills(); | |
| const user = userFromResult(oauthResult); | |
| if (authHost) { | |
| authHost.replaceChildren(...pills, user ? makeLoggedInControl(user) : makeLoggedOutControl()); | |
| } | |
| if (compactAuthHost) { | |
| compactAuthHost.replaceChildren(); | |
| compactAuthHost.hidden = !user; | |
| if (user) compactAuthHost.appendChild(makeLoggedInControl(user, { compact: true })); | |
| } | |
| queueMicrotask(() => { | |
| rendering = false; | |
| prepareAccountDestinations(); | |
| announceAuthState(); | |
| }); | |
| } | |
| function applyOauthResult(callbackResult, source) { | |
| oauthResult = callbackResult; | |
| writeStoredResult(callbackResult); | |
| activeFlow = null; | |
| renderAuth(); | |
| closeNativeLoginPopover(); | |
| const user = userFromResult(callbackResult); | |
| identifyAuthenticatedUser(user, source); | |
| trackAuthEvent("space_hf_login_completed", { | |
| component: "space_nav", | |
| username_present: !!user?.username, | |
| subject_present: !!user?.subject, | |
| email_present: !!user?.email, | |
| }); | |
| } | |
| function failLogin(error, failureStage) { | |
| console.warn("Hugging Face Space login failed", error); | |
| activeFlow = null; | |
| renderAuth(); | |
| trackAuthEvent(failureStage === "handoff_cancelled" ? "space_hf_login_cancelled" : "space_hf_login_failed", { | |
| user_initiated: true, | |
| failure_category: authFailureCategory(error), | |
| component: "space_nav", | |
| failure_stage: failureStage, | |
| error_name: String(error?.name || "Error").slice(0, 80), | |
| error_message: String(error?.message || "Unknown error").slice(0, 240), | |
| request_status: Number(error?.status || 0) || undefined, | |
| }); | |
| void prepareLogin("background_after_login_failure"); | |
| } | |
| function delay(milliseconds) { | |
| return new Promise((resolve) => window.setTimeout(resolve, milliseconds)); | |
| } | |
| function isUserPreparationTrigger(trigger) { | |
| return String(trigger || "").startsWith("user_"); | |
| } | |
| function schedulePrepareRecovery(trigger) { | |
| if (prepareRecoveryTimer || oauthResult || activeFlow) return; | |
| // Bounded backoff: once the schedule is exhausted this page stops trying | |
| // until the next sign-in intent. | |
| if (prepareFailureCount >= PREPARE_RECOVERY_DELAYS_MS.length) return; | |
| const recoveryDelay = PREPARE_RECOVERY_DELAYS_MS[prepareFailureCount]; | |
| prepareFailureCount += 1; | |
| prepareRecoveryTimer = window.setTimeout(() => { | |
| prepareRecoveryTimer = null; | |
| void prepareLogin( | |
| isUserPreparationTrigger(trigger) ? "user_recovery" : "background_recovery" | |
| ); | |
| }, recoveryDelay); | |
| } | |
| function retryableRequestError(error) { | |
| return ( | |
| error?.name === "AuthResponseError" || | |
| error?.name === "TypeError" || | |
| error?.name === "AbortError" || | |
| [408, 425, 429, 500, 502, 503, 504].includes(Number(error?.status || 0)) | |
| ); | |
| } | |
| async function postJson(url, body, { retryDelays = [0], validate = null } = {}) { | |
| let lastError = null; | |
| for (let attempt = 0; attempt < retryDelays.length; attempt += 1) { | |
| if (retryDelays[attempt] > 0) await delay(retryDelays[attempt]); | |
| const controller = new AbortController(); | |
| const timer = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); | |
| try { | |
| const response = await fetch(url, { | |
| method: "POST", | |
| mode: "cors", | |
| credentials: "omit", | |
| headers: { "content-type": "application/json" }, | |
| body: JSON.stringify(body), | |
| signal: controller.signal, | |
| }); | |
| let payload; | |
| try { payload = await response.json(); } | |
| catch { | |
| const error = invalidAuthResponse("invalid_json"); | |
| if (!response.ok) error.status = response.status; | |
| throw error; | |
| } | |
| if (!response.ok && response.status !== 202) { | |
| const error = new Error(payload?.error || `Request failed (${response.status})`); | |
| error.status = response.status; | |
| throw error; | |
| } | |
| if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw invalidAuthResponse("invalid_payload"); | |
| validate?.(payload); | |
| return payload; | |
| } catch (error) { | |
| error.retryCount = attempt; | |
| lastError = error; | |
| if (!retryableRequestError(error) || attempt === retryDelays.length - 1) throw error; | |
| } finally { | |
| window.clearTimeout(timer); | |
| } | |
| } | |
| throw lastError || new Error("Request failed"); | |
| } | |
| function announceAuthState() { | |
| const user = userFromResult(oauthResult); | |
| const session = liveSpaceSession(); | |
| const identity = user && session ? `${user.subject || user.username}:${session.id}` : ""; | |
| if (identity === lastAnnouncedAuthIdentity) return; | |
| lastAnnouncedAuthIdentity = identity; | |
| window.dispatchEvent( | |
| new CustomEvent(AUTH_CHANGED_EVENT, { | |
| detail: { | |
| authenticated: !!identity, | |
| username: user?.username || "", | |
| }, | |
| }) | |
| ); | |
| } | |
| async function postAuthenticatedSpaceJson(url, payload) { | |
| const session = liveSpaceSession(); | |
| if (!session) throw new Error("The Space login expired. Please sign in again."); | |
| try { | |
| return await postJson(url, { | |
| ...(payload || {}), | |
| link_session_id: session.id, | |
| link_session_secret: session.secret, | |
| }); | |
| } catch (error) { | |
| if (/login expired|session expired|invalid|unauthorized/i.test(String(error?.message || ""))) { | |
| oauthResult = null; | |
| writeStoredResult(null); | |
| renderAuth(); | |
| void prepareLogin("background_session_expired"); | |
| } | |
| throw error; | |
| } | |
| } | |
| window.hfviewerSpaceAuth = Object.freeze({ | |
| isAuthenticated: () => !!userFromResult(oauthResult) && !!liveSpaceSession(), | |
| postJson: postAuthenticatedSpaceJson, | |
| requestLogin: requestNativeLogin, | |
| }); | |
| async function pollForCompletion(flow) { | |
| while (activeFlow === flow && Date.now() - flow.startedAt < FLOW_TIMEOUT_MS) { | |
| try { | |
| const result = await postJson(CONSUME_URL, { | |
| flow_id: flow.id, | |
| flow_secret: flow.secret, | |
| }); | |
| if (result?.status === "complete" && userFromResult(result)) { | |
| applyOauthResult( | |
| { userInfo: result.userInfo, spaceSession: result.spaceSession }, | |
| "hf_space_native_oauth_handoff" | |
| ); | |
| return; | |
| } | |
| } catch (error) { | |
| failLogin(error, "handoff_consume"); | |
| return; | |
| } | |
| await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); | |
| } | |
| if (activeFlow === flow) failLogin(new Error("OAuth flow timed out"), "handoff_timeout"); | |
| } | |
| function schedulePreparedFlowExpiry(flow) { | |
| if (preparedFlowExpiryTimer) window.clearTimeout(preparedFlowExpiryTimer); | |
| // A prepared URL is only valid for a few minutes. Replace it as it expires | |
| // so the Log in control stays a real link: the sign-in must open from the | |
| // user's first click (native target=_blank), which an async fetch cannot do. | |
| preparedFlowExpiryTimer = window.setTimeout(() => { | |
| preparedFlowExpiryTimer = null; | |
| if (preparedFlow !== flow || activeFlow) return; | |
| preparedFlow = null; | |
| renderAuth(); | |
| void prepareLogin("background_scheduled").then(() => refreshNativeLoginPopover()); | |
| }, PREPARE_REFRESH_MS); | |
| } | |
| function startLogin(flow, link) { | |
| flow.startedAt = Date.now(); | |
| activeFlow = flow; | |
| preparedFlow = null; | |
| loginContinueRequested = false; | |
| // Keep the clicked anchor alive until the browser has performed its native | |
| // target=_blank navigation. Replacing it synchronously can cancel the popup. | |
| link.textContent = "Signing in…"; | |
| link.title = "Cancel this sign-in and try again"; | |
| link.setAttribute("aria-disabled", "false"); | |
| trackAuthEvent("space_hf_login_clicked", { component: "space_nav" }); | |
| void pollForCompletion(flow); | |
| } | |
| async function prepareLogin(trigger = "background_initial") { | |
| if (oauthResult || activeFlow) return; | |
| // Authentication is intentionally available only in the two official | |
| // deployments. A fork can copy our declared space_repo value, but it cannot | |
| // claim an official browser origin and must never generate misleading auth | |
| // failures or receive a trusted handoff. | |
| if ( | |
| window.location.hostname.endsWith(".hf.space") && | |
| !OFFICIAL_SPACE_HOSTS.has(window.location.hostname.toLowerCase()) | |
| ) return; | |
| if (preparedFlowIsFresh()) return; | |
| if (preparePromise) { | |
| if (isUserPreparationTrigger(trigger)) prepareIntentEscalated = true; | |
| return preparePromise; | |
| } | |
| if (!isUserPreparationTrigger(trigger)) { | |
| if (backgroundPrepareAttempts >= PREPARE_MAX_BACKGROUND_ATTEMPTS) return; | |
| backgroundPrepareAttempts += 1; | |
| } | |
| const prepareStartedAt = performance.now(); | |
| preparePromiseTrigger = String(trigger || "background_initial"); | |
| prepareIntentEscalated = isUserPreparationTrigger(preparePromiseTrigger); | |
| const flow = { | |
| id: randomFlowValue(), | |
| secret: randomFlowValue(), | |
| preparedAt: Date.now(), | |
| startedAt: 0, | |
| loginUrl: "", | |
| }; | |
| preparePromise = postJson( | |
| START_URL, | |
| { flow_id: flow.id, flow_secret: flow.secret }, | |
| { retryDelays: PREPARE_RETRY_DELAYS_MS, validate: validatedLoginUrl } | |
| ).then((result) => { | |
| const loginUrl = validatedLoginUrl(result); | |
| flow.loginUrl = loginUrl.toString(); | |
| preparedFlow = flow; | |
| const recoveredFailureCount = prepareFailureCount; | |
| const recoveredFromFailure = prepareFailureEpisodeActive; | |
| if (!prepareSuccessReported || recoveredFromFailure || prepareIntentEscalated) { | |
| trackAuthEvent("space_hf_login_prepared", { | |
| component: "space_nav", | |
| preparation_trigger: preparePromiseTrigger, | |
| user_initiated: prepareIntentEscalated, | |
| duration_ms: Math.max(0, Math.round(performance.now() - prepareStartedAt)), | |
| recovered_from_failure: recoveredFromFailure, | |
| recovery_count: recoveredFailureCount, | |
| }); | |
| } | |
| prepareSuccessReported = true; | |
| prepareFailureEpisodeActive = false; | |
| prepareFailureCount = 0; | |
| backgroundPrepareAttempts = 0; | |
| if (prepareRecoveryTimer) window.clearTimeout(prepareRecoveryTimer); | |
| prepareRecoveryTimer = null; | |
| schedulePreparedFlowExpiry(flow); | |
| if (!activeFlow) renderAuth(); | |
| refreshNativeLoginPopover(); | |
| }).catch((error) => { | |
| console.warn("Hugging Face Space login preparation failed", error); | |
| // Background/intent preparation is operational prewarm telemetry, not a | |
| // failed login attempt: report it at most once per page load. A user who | |
| // explicitly asked to sign in is reported per failure, up to a small cap. | |
| const reportFailure = prepareIntentEscalated | |
| ? userPrepareFailureReports < PREPARE_MAX_USER_FAILURE_REPORTS | |
| : !backgroundPrepareFailureReported && !prepareFailureEpisodeActive; | |
| if (reportFailure) { | |
| if (prepareIntentEscalated) userPrepareFailureReports += 1; | |
| else backgroundPrepareFailureReported = true; | |
| trackAuthEvent("space_hf_login_preparation_failed", { | |
| component: "space_nav", | |
| failure_stage: "login_prepare", | |
| preparation_trigger: preparePromiseTrigger, | |
| user_initiated: prepareIntentEscalated, | |
| error_name: String(error?.name || "Error").slice(0, 80), | |
| error_message: String(error?.message || "Unknown error").slice(0, 240), | |
| request_status: Number(error?.status || 0) || undefined, | |
| failure_category: authFailureCategory(error), | |
| error_code: error?.code, | |
| retry_count: error?.retryCount || 0, | |
| recovery_count: prepareFailureCount, | |
| }); | |
| } | |
| prepareFailureEpisodeActive = true; | |
| schedulePrepareRecovery(preparePromiseTrigger); | |
| }).finally(() => { | |
| preparePromise = null; | |
| preparePromiseTrigger = ""; | |
| prepareIntentEscalated = false; | |
| }); | |
| return preparePromise; | |
| } | |
| async function initializeApp() { | |
| renderAuth(); | |
| identifyAuthenticatedUser(userFromResult(oauthResult), "hf_space_native_oauth_restored"); | |
| await prepareLogin("background_initial"); | |
| } | |
| if (authHost) { | |
| const observer = new MutationObserver(() => { | |
| if (!rendering && !authHost.querySelector("[data-space-native-auth]")) renderAuth(); | |
| }); | |
| observer.observe(authHost, { childList: true }); | |
| } | |
| window.addEventListener("storage", (event) => { | |
| if (event.key !== STORAGE_KEY) return; | |
| oauthResult = readStoredResult(); | |
| renderAuth(); | |
| }); | |
| window.setInterval(() => { | |
| void prepareLogin("background_scheduled"); | |
| prepareAccountDestinations(); | |
| }, PREPARE_REFRESH_MS); | |
| void initializeApp(); | |