"""Which token pays for a request. The rule this file exists to enforce: **bill the viewer, not the Space owner, whenever we possibly can.** A harness that quietly spends the owner's inference credits on every visitor is one shared link away from an unpleasant invoice. Precedence, highest first: 1. the signed-in viewer's Hugging Face OAuth token -> billed to the viewer 2. a key the viewer pasted into the UI this session -> billed to the viewer 3. the Space's own ``HF_TOKEN`` secret -> billed to the owner Step 3 is off unless ``ASE_ALLOW_SPACE_CREDITS`` is set, so the fallback is a deliberate act rather than a default. Locally it is on, because there is no OAuth off-Space and a dev loop that cannot make a request is useless. """ from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path @dataclass(frozen=True) class Credential: token: str | None mode: str # oauth | user-key | space-secret | none who: str | None = None detail: str = "" @property def bills_viewer(self) -> bool: return self.mode in ("oauth", "user-key") def to_json(self) -> dict: return { "mode": self.mode, "who": self.who, "billsViewer": self.bills_viewer, "detail": self.detail, } def on_space() -> bool: return bool(os.environ.get("SPACE_ID")) def allow_space_credits() -> bool: """Falling back to the owner's token is opt-in on a Space, on by default off it.""" flag = os.environ.get("ASE_ALLOW_SPACE_CREDITS") if flag is not None: return flag.strip().lower() in ("1", "true", "yes", "on") return not on_space() def resolve(oauth_token: str | None = None, oauth_user: str | None = None, user_key: str | None = None) -> Credential: """Pick the credential for one request. Never logs or returns the value.""" if oauth_token: return Credential(oauth_token, "oauth", oauth_user, "Signed in with Hugging Face; inference is billed to you.") if user_key: return Credential(user_key, "user-key", None, "Using the key you supplied; it is held for this session only.") secret = (os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") or _dev_key_file()) if secret and allow_space_credits(): return Credential( secret, "space-secret", None, "Falling back to the Space's own token — this bills the Space owner.", ) if secret: return Credential( None, "none", None, "A Space token exists but owner-billed requests are disabled. " "Sign in, or supply your own key.", ) return Credential(None, "none", None, "No credential available. Supply a Hugging Face token.") def _dev_key_file() -> str | None: """The shared ``key.txt`` this workspace keeps for local development. Only consulted off a Space. On a Space the token arrives as a secret in the environment and this path does not exist, so the lookup is dead code there rather than a second way in. """ if on_space(): return None explicit = os.environ.get("ASE_KEY_FILE") if explicit: # Authoritative when set, including when it points at nothing. The # search below would otherwise reach outside a test's tmp_path and # find the developer's real key, which is a test that passes for the # wrong reason and a suite that behaves differently on CI. candidates = [Path(explicit)] else: # The workspace deliberately keeps one gitignored key above the # projects so several of them can share it. Walk up to find it. here = Path(__file__).resolve() candidates = [parent / "key.txt" for parent in here.parents[:5]] for path in candidates: try: if path.is_file(): token = path.read_text().strip() if token: return token except OSError: continue return None