Thomas Wolf commited on
Commit
4567fd6
Β·
unverified Β·
1 Parent(s): d7f74c1

visibility: don't let a stale HF_TOKEN wedge a private Space forever (#1)

Browse files

After 3 consecutive non-OK authed /api/spaces responses (e.g. a stale
HF_TOKEN secret carried over when a Space was duplicated), give up on
bucket discovery and unlock with bucketUnverified:true instead of
keeping known:false forever. Network blips (thrown fetches) don't burn
the counter β€” only consistent auth failures do.

Confirmed-private Spaces can no longer be bricked closed by a bad token.

Files changed (1) hide show
  1. server/src/visibility.js +32 -4
server/src/visibility.js CHANGED
@@ -24,6 +24,13 @@ const hfToken = () => process.env.HF_TOKEN || process.env.HUGGING_FACE_HUB_TOKEN
24
  // private β€” but we surface a warning so the operator can verify or add a token.
25
  let state = { spaceId: SPACE_ID, public: false, known: false, checkedAt: 0, reason: null, bucket: null, buckets: [], bucketUnverified: false };
26
  let volumes = null; // bucket ids mounted on this Space; discovered once (fixed until restart)
 
 
 
 
 
 
 
27
 
28
  const HEADERS = { 'user-agent': 'agent-manager' };
29
  // Every HF API call is bounded: a hung request must never wedge the boot
@@ -31,6 +38,11 @@ const HEADERS = { 'user-agent': 'agent-manager' };
31
  const HF_TIMEOUT_MS = 8000;
32
  const hfFetch = (url, opts = {}) => fetch(url, { ...opts, signal: AbortSignal.timeout(HF_TIMEOUT_MS) });
33
 
 
 
 
 
 
34
  async function discoverBuckets() {
35
  if (volumes !== null) return volumes;
36
  const token = hfToken();
@@ -39,12 +51,24 @@ async function discoverBuckets() {
39
  const r = await hfFetch(`https://huggingface.co/api/spaces/${SPACE_ID}`, {
40
  headers: { ...HEADERS, authorization: `Bearer ${token}` },
41
  });
42
- if (!r.ok) return null; // transient β€” retry next cycle
 
 
 
 
 
 
 
 
 
43
  const j = await r.json();
44
  volumes = ((j.runtime && j.runtime.volumes) || [])
45
  .filter((v) => v && v.type === 'bucket' && v.source)
46
  .map((v) => v.source);
47
- } catch { return null; }
 
 
 
48
  return volumes;
49
  }
50
 
@@ -66,8 +90,12 @@ async function check() {
66
  // Not publicly visible (401/404) β†’ the Space is private. Now the bucket(s).
67
  const buckets = await discoverBuckets();
68
  if (buckets === null) { state = { ...state, checkedAt: Date.now() }; return state; } // keep last verdict
69
- // No token β†’ buckets couldn't be discovered β†’ we can't verify the bucket.
70
- if (!hfToken()) {
 
 
 
 
71
  state = { spaceId: SPACE_ID, public: false, known: true, checkedAt: Date.now(), reason: null, bucket: null, buckets: [], bucketUnverified: true };
72
  return state;
73
  }
 
24
  // private β€” but we surface a warning so the operator can verify or add a token.
25
  let state = { spaceId: SPACE_ID, public: false, known: false, checkedAt: 0, reason: null, bucket: null, buckets: [], bucketUnverified: false };
26
  let volumes = null; // bucket ids mounted on this Space; discovered once (fixed until restart)
27
+ // Failure counter for the authed volumes call. A transient network blip
28
+ // should retry; a permanently bad token (e.g. a stale HF_TOKEN carried over
29
+ // when a Space was duplicated) must NOT wedge the Space forever. After this
30
+ // many consecutive failures we give up, treat the bucket as undiscoverable,
31
+ // and unlock with a `bucketUnverified` warning instead of staying closed.
32
+ let bucketDiscoverFails = 0;
33
+ const BUCKET_DISCOVER_MAX_FAILS = 3;
34
 
35
  const HEADERS = { 'user-agent': 'agent-manager' };
36
  // Every HF API call is bounded: a hung request must never wedge the boot
 
38
  const HF_TIMEOUT_MS = 8000;
39
  const hfFetch = (url, opts = {}) => fetch(url, { ...opts, signal: AbortSignal.timeout(HF_TIMEOUT_MS) });
40
 
41
+ // Returns:
42
+ // - an array of bucket ids (may be empty) once discovery has succeeded OR
43
+ // has been given up on (so the Space can unlock with a warning).
44
+ // - null only for a TRULY transient failure (first/short retry window) so
45
+ // the caller keeps the last verdict and tries again next cycle.
46
  async function discoverBuckets() {
47
  if (volumes !== null) return volumes;
48
  const token = hfToken();
 
51
  const r = await hfFetch(`https://huggingface.co/api/spaces/${SPACE_ID}`, {
52
  headers: { ...HEADERS, authorization: `Bearer ${token}` },
53
  });
54
+ if (!r.ok) {
55
+ // 401/403 β†’ the token is bad/expired. A network blip would have thrown.
56
+ // Retry a couple of times in case HF is having a moment, then give up
57
+ // so we don't wedge a confirmed-private Space forever.
58
+ if (++bucketDiscoverFails >= BUCKET_DISCOVER_MAX_FAILS) {
59
+ console.warn(`[visibility] authed /api/spaces returned ${r.status} ${bucketDiscoverFails}x β€” giving up on bucket discovery; unlocking with bucketUnverified (check the Space's HF_TOKEN secret)`);
60
+ volumes = []; return volumes;
61
+ }
62
+ return null; // transient β€” retry next cycle
63
+ }
64
  const j = await r.json();
65
  volumes = ((j.runtime && j.runtime.volumes) || [])
66
  .filter((v) => v && v.type === 'bucket' && v.source)
67
  .map((v) => v.source);
68
+ } catch {
69
+ // Thrown (DNS/timeout/network) β†’ genuinely transient, don't burn the counter.
70
+ return null;
71
+ }
72
  return volumes;
73
  }
74
 
 
90
  // Not publicly visible (401/404) β†’ the Space is private. Now the bucket(s).
91
  const buckets = await discoverBuckets();
92
  if (buckets === null) { state = { ...state, checkedAt: Date.now() }; return state; } // keep last verdict
93
+ // Treat the give-up path the same as having no token: the bucket is
94
+ // unverifiable, NOT public. Unlock the Space with a warning rather than
95
+ // wedging it closed forever (a bad/stale HF_TOKEN must never brick a
96
+ // confirmed-private Space).
97
+ const bucketUnverified = !hfToken() || bucketDiscoverFails >= BUCKET_DISCOVER_MAX_FAILS;
98
+ if (bucketUnverified) {
99
  state = { spaceId: SPACE_ID, public: false, known: true, checkedAt: Date.now(), reason: null, bucket: null, buckets: [], bucketUnverified: true };
100
  return state;
101
  }