/** * `?space=owner/name[&fn=/endpoint]` deep link ("Open in Flow" badges). The Space is read on load, kept in * sessionStorage so it survives the sign-in redirect, and added once the editor is up. * Kept free of app imports: main.tsx and hfAuth use it before the editor loads. */ const KEY = "flow-pending-space"; const FN_KEY = "flow-pending-fn"; const FN = /^\/?[\w.-]{1,80}$/; const ID = /^[\w.-]+\/[\w.-]+$/; const SUB = /^[a-z0-9-]+$/; /** `owner/name`, or `sub:` for a *.hf.space URL (resolved later from the Space's config). */ export function normaliseSpace(raw: string): string | null { const s = raw.trim(); if (ID.test(s)) return s; let u: URL; try { u = new URL(s); } catch { return null; } if (u.protocol !== "https:" && u.protocol !== "http:") return null; if (u.hostname === "huggingface.co" || u.hostname === "www.huggingface.co") { const m = u.pathname.match(/^\/spaces\/([^/]+)\/([^/]+)/); const id = m ? `${m[1]}/${m[2]}` : ""; return ID.test(id) ? id : null; } if (u.hostname.endsWith(".hf.space")) { const sub = u.hostname.split(".")[0]; return SUB.test(sub) ? `sub:${sub}` : null; } return null; } /** Reads `?space=` into the stash and drops it from the address bar so a refresh doesn't add it twice. */ export function stashSpaceParam() { const q = new URLSearchParams(location.search); if (!q.has("space")) return; const v = normaliseSpace(q.get("space") ?? ""); const fn = (q.get("fn") ?? "").trim(); q.delete("space"); q.delete("fn"); const s = q.toString(); history.replaceState(history.state, "", `${location.pathname}${s ? `?${s}` : ""}${location.hash}`); if (!v) return; try { sessionStorage.setItem(KEY, v); if (FN.test(fn)) sessionStorage.setItem(FN_KEY, fn.replace(/^\//, "")); else sessionStorage.removeItem(FN_KEY); } catch { /* storage blocked */ } } /** The endpoint asked for with `&fn=`, without the leading slash. */ export function pendingFn(): string | null { try { return sessionStorage.getItem(FN_KEY); } catch { return null; } } export function pendingSpace(): string | null { try { return sessionStorage.getItem(KEY); } catch { return null; } } /** Returns the pending Space (and endpoint, if one was asked for) and forgets them. */ export function takePendingSpace(): { id: string; fn: string | null } | null { const v = pendingSpace(); const fn = pendingFn(); try { sessionStorage.removeItem(KEY); sessionStorage.removeItem(FN_KEY); } catch { /* storage blocked */ } return v ? { id: v, fn } : null; } /* `?w=` share link: same idea, kept until the editor is up and the viewer has said yes. */ const SHARE_KEY = "flow-pending-share"; const SHARE = /^[A-Za-z0-9]{10}$/; /** Reads `?w=` into the stash and drops it from the address bar. */ export function stashShareParam() { const q = new URLSearchParams(location.search); if (!q.has("w")) return; const v = (q.get("w") ?? "").trim(); q.delete("w"); const s = q.toString(); history.replaceState(history.state, "", `${location.pathname}${s ? `?${s}` : ""}${location.hash}`); if (!SHARE.test(v)) return; try { sessionStorage.setItem(SHARE_KEY, v); } catch { /* storage blocked */ } } export function pendingShare(): string | null { try { return sessionStorage.getItem(SHARE_KEY); } catch { return null; } } export function clearPendingShare() { try { sessionStorage.removeItem(SHARE_KEY); } catch { /* storage blocked */ } }