somratpro commited on
Commit
70d2c8e
·
1 Parent(s): ea48603

feat: implement server-side space privacy detection to conditionally render keep-awake UI

Browse files
Files changed (1) hide show
  1. health-server.js +140 -75
health-server.js CHANGED
@@ -20,6 +20,8 @@ const DASHBOARD_HEALTH_PATH = `${DASHBOARD_BASE}/health`;
20
  const DASHBOARD_UPTIMEROBOT_PATH = `${DASHBOARD_BASE}/uptimerobot/setup`;
21
  const DASHBOARD_APP_BASE = `${DASHBOARD_BASE}/app`;
22
  const APP_BASE = "/app";
 
 
23
 
24
  function parseRequestUrl(url) {
25
  try {
@@ -131,6 +133,78 @@ function readGuardianStatus() {
131
  return { configured: true, connected: false, pairing: false };
132
  }
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  function renderChannelBadge(channel, configuredLabel) {
135
  if (channel && channel.connected) {
136
  return '<div class="status-badge status-online"><div class="pulse"></div>Active</div>';
@@ -158,6 +232,42 @@ function renderSyncBadge(syncData) {
158
 
159
  function renderDashboard(initialData) {
160
  const controlUiHref = `${APP_BASE}/`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  return `
162
  <!DOCTYPE html>
163
  <html lang="en">
@@ -402,6 +512,10 @@ function renderDashboard(initialData) {
402
  cursor: wait;
403
  }
404
 
 
 
 
 
405
  .helper-note {
406
  margin-top: 10px;
407
  font-size: 0.82rem;
@@ -560,37 +674,7 @@ function renderDashboard(initialData) {
560
 
561
  <div class="stat-card helper-card">
562
  <span class="stat-label">Keep Space Awake</span>
563
- <div id="uptimerobot-public-flow">
564
- <div id="uptimerobot-summary" class="helper-summary">
565
- One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.
566
- </div>
567
- <button id="uptimerobot-toggle" class="helper-toggle" type="button">
568
- Set Up Monitor
569
- </button>
570
- <div id="uptimerobot-shell" class="helper-shell hidden">
571
- <div class="helper-copy">
572
- Do <strong>not</strong> use the Read-only API key or a Monitor-specific API key.
573
- </div>
574
- <div class="helper-row">
575
- <input
576
- id="uptimerobot-key"
577
- class="helper-input"
578
- type="password"
579
- placeholder="Paste your UptimeRobot Main API key"
580
- autocomplete="off"
581
- />
582
- <button id="uptimerobot-btn" class="helper-button" type="button">
583
- Create Monitor
584
- </button>
585
- </div>
586
- <div class="helper-note">
587
- One-time setup. Your key is only used to create the monitor for this Space.
588
- </div>
589
- </div>
590
- </div>
591
- <div id="uptimerobot-private-note" class="helper-summary hidden">
592
- <strong>This Space is private.</strong> External monitors cannot reliably access private HF health URLs, so keep-awake setup is only available on public Spaces.
593
- </div>
594
  <div id="uptimerobot-result" class="helper-result"></div>
595
  </div>
596
 
@@ -655,31 +739,17 @@ function renderDashboard(initialData) {
655
  }
656
 
657
  const monitorStateKey = 'huggingclaw_uptimerobot_setup_v1';
658
-
659
- async function detectPrivateSpace() {
660
- const params = new URLSearchParams(window.location.search || '');
661
-
662
- if (!params.has('__sign')) {
663
- return false;
664
- }
665
-
666
- try {
667
- const res = await fetch(getDashboardBase(), {
668
- method: 'GET',
669
- cache: 'no-store',
670
- credentials: 'same-origin'
671
- });
672
- return !res.ok;
673
- } catch {
674
- return true;
675
- }
676
- }
677
 
678
  function setMonitorUiState(isConfigured) {
679
  const summary = document.getElementById('uptimerobot-summary');
680
  const shell = document.getElementById('uptimerobot-shell');
681
  const toggle = document.getElementById('uptimerobot-toggle');
682
 
 
 
 
 
683
  if (isConfigured) {
684
  summary.classList.add('success');
685
  summary.innerHTML = '<strong>Already set up.</strong> Your UptimeRobot monitor should keep this public Space awake.';
@@ -754,20 +824,12 @@ function renderDashboard(initialData) {
754
 
755
  updateStats();
756
  setInterval(updateStats, 10000);
757
- restoreMonitorUiState();
758
  document.getElementById('control-ui-link').setAttribute('href', getDashboardBase() + '/app/' + getCurrentSearch());
759
- detectPrivateSpace().then((isPrivate) => {
760
- if (isPrivate) {
761
- document.getElementById('uptimerobot-public-flow').classList.add('hidden');
762
- document.getElementById('uptimerobot-private-note').classList.remove('hidden');
763
- document.getElementById('uptimerobot-result').className = 'helper-result';
764
- document.getElementById('uptimerobot-result').textContent = '';
765
- return;
766
- }
767
-
768
  document.getElementById('uptimerobot-btn').addEventListener('click', setupUptimeRobot);
769
  document.getElementById('uptimerobot-toggle').addEventListener('click', toggleMonitorSetup);
770
- });
771
  </script>
772
  </body>
773
  </html>
@@ -1064,20 +1126,23 @@ const server = http.createServer((req, res) => {
1064
  }
1065
 
1066
  if (isDashboardRoute(pathname)) {
1067
- const guardianStatus = readGuardianStatus();
1068
- const initialData = {
1069
- model: LLM_MODEL,
1070
- whatsapp: {
1071
- configured: guardianStatus.configured,
1072
- connected: guardianStatus.connected,
1073
- pairing: guardianStatus.pairing,
1074
- },
1075
- telegram: normalizeChannelStatus(null, TELEGRAM_ENABLED),
1076
- sync: readSyncStatus(),
1077
- uptime: uptimeHuman,
1078
- };
1079
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1080
- res.end(renderDashboard(initialData));
 
 
 
1081
  return;
1082
  }
1083
 
 
20
  const DASHBOARD_UPTIMEROBOT_PATH = `${DASHBOARD_BASE}/uptimerobot/setup`;
21
  const DASHBOARD_APP_BASE = `${DASHBOARD_BASE}/app`;
22
  const APP_BASE = "/app";
23
+ const SPACE_VISIBILITY_TTL_MS = 10 * 60 * 1000;
24
+ const spaceVisibilityCache = new Map();
25
 
26
  function parseRequestUrl(url) {
27
  try {
 
133
  return { configured: true, connected: false, pairing: false };
134
  }
135
 
136
+ function decodeJwtPayload(token) {
137
+ try {
138
+ const parts = String(token || "").split(".");
139
+ if (parts.length < 2) return null;
140
+ const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
141
+ const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
142
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ function getSpaceRef(parsedUrl) {
149
+ const signedToken = parsedUrl.searchParams.get("__sign");
150
+ if (!signedToken) return null;
151
+
152
+ const payload = decodeJwtPayload(signedToken);
153
+ const subject = payload && payload.sub;
154
+ const match =
155
+ typeof subject === "string"
156
+ ? subject.match(/^\/spaces\/([^/]+)\/([^/]+)$/)
157
+ : null;
158
+
159
+ if (!match) return null;
160
+ return { owner: match[1], repo: match[2] };
161
+ }
162
+
163
+ function fetchStatusCode(url) {
164
+ return new Promise((resolve, reject) => {
165
+ const req = https.get(
166
+ url,
167
+ {
168
+ headers: {
169
+ "user-agent": "HuggingClaw/1.0",
170
+ accept: "application/json",
171
+ },
172
+ },
173
+ (res) => {
174
+ res.resume();
175
+ resolve(res.statusCode || 0);
176
+ },
177
+ );
178
+ req.on("error", reject);
179
+ req.setTimeout(5000, () => {
180
+ req.destroy(new Error("Request timed out"));
181
+ });
182
+ });
183
+ }
184
+
185
+ async function resolveSpaceIsPrivate(parsedUrl) {
186
+ const ref = getSpaceRef(parsedUrl);
187
+ if (!ref) return false;
188
+
189
+ const cacheKey = `${ref.owner}/${ref.repo}`;
190
+ const cached = spaceVisibilityCache.get(cacheKey);
191
+ if (cached && Date.now() - cached.timestamp < SPACE_VISIBILITY_TTL_MS) {
192
+ return cached.isPrivate;
193
+ }
194
+
195
+ try {
196
+ const statusCode = await fetchStatusCode(
197
+ `https://huggingface.co/api/spaces/${ref.owner}/${ref.repo}`,
198
+ );
199
+ const isPrivate = statusCode === 401 || statusCode === 403 || statusCode === 404;
200
+ spaceVisibilityCache.set(cacheKey, { isPrivate, timestamp: Date.now() });
201
+ return isPrivate;
202
+ } catch {
203
+ if (cached) return cached.isPrivate;
204
+ return false;
205
+ }
206
+ }
207
+
208
  function renderChannelBadge(channel, configuredLabel) {
209
  if (channel && channel.connected) {
210
  return '<div class="status-badge status-online"><div class="pulse"></div>Active</div>';
 
232
 
233
  function renderDashboard(initialData) {
234
  const controlUiHref = `${APP_BASE}/`;
235
+ const keepAwakeHtml = initialData.spacePrivate
236
+ ? `
237
+ <div id="uptimerobot-private-note" class="helper-summary">
238
+ <strong>This Space is private.</strong> External monitors cannot reliably access private HF health URLs, so keep-awake setup is only available on public Spaces.
239
+ </div>
240
+ `
241
+ : `
242
+ <div id="uptimerobot-public-flow">
243
+ <div id="uptimerobot-summary" class="helper-summary">
244
+ One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.
245
+ </div>
246
+ <button id="uptimerobot-toggle" class="helper-toggle" type="button">
247
+ Set Up Monitor
248
+ </button>
249
+ <div id="uptimerobot-shell" class="helper-shell hidden">
250
+ <div class="helper-copy">
251
+ Do <strong>not</strong> use the Read-only API key or a Monitor-specific API key.
252
+ </div>
253
+ <div class="helper-row">
254
+ <input
255
+ id="uptimerobot-key"
256
+ class="helper-input"
257
+ type="password"
258
+ placeholder="Paste your UptimeRobot Main API key"
259
+ autocomplete="off"
260
+ />
261
+ <button id="uptimerobot-btn" class="helper-button" type="button">
262
+ Create Monitor
263
+ </button>
264
+ </div>
265
+ <div class="helper-note">
266
+ One-time setup. Your key is only used to create the monitor for this Space.
267
+ </div>
268
+ </div>
269
+ </div>
270
+ `;
271
  return `
272
  <!DOCTYPE html>
273
  <html lang="en">
 
512
  cursor: wait;
513
  }
514
 
515
+ .hidden {
516
+ display: none !important;
517
+ }
518
+
519
  .helper-note {
520
  margin-top: 10px;
521
  font-size: 0.82rem;
 
674
 
675
  <div class="stat-card helper-card">
676
  <span class="stat-label">Keep Space Awake</span>
677
+ ${keepAwakeHtml}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  <div id="uptimerobot-result" class="helper-result"></div>
679
  </div>
680
 
 
739
  }
740
 
741
  const monitorStateKey = 'huggingclaw_uptimerobot_setup_v1';
742
+ const KEEP_AWAKE_PRIVATE = ${initialData.spacePrivate ? "true" : "false"};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
 
744
  function setMonitorUiState(isConfigured) {
745
  const summary = document.getElementById('uptimerobot-summary');
746
  const shell = document.getElementById('uptimerobot-shell');
747
  const toggle = document.getElementById('uptimerobot-toggle');
748
 
749
+ if (!summary || !shell || !toggle) {
750
+ return;
751
+ }
752
+
753
  if (isConfigured) {
754
  summary.classList.add('success');
755
  summary.innerHTML = '<strong>Already set up.</strong> Your UptimeRobot monitor should keep this public Space awake.';
 
824
 
825
  updateStats();
826
  setInterval(updateStats, 10000);
 
827
  document.getElementById('control-ui-link').setAttribute('href', getDashboardBase() + '/app/' + getCurrentSearch());
828
+ if (!KEEP_AWAKE_PRIVATE) {
829
+ restoreMonitorUiState();
 
 
 
 
 
 
 
830
  document.getElementById('uptimerobot-btn').addEventListener('click', setupUptimeRobot);
831
  document.getElementById('uptimerobot-toggle').addEventListener('click', toggleMonitorSetup);
832
+ }
833
  </script>
834
  </body>
835
  </html>
 
1126
  }
1127
 
1128
  if (isDashboardRoute(pathname)) {
1129
+ void (async () => {
1130
+ const guardianStatus = readGuardianStatus();
1131
+ const initialData = {
1132
+ model: LLM_MODEL,
1133
+ whatsapp: {
1134
+ configured: guardianStatus.configured,
1135
+ connected: guardianStatus.connected,
1136
+ pairing: guardianStatus.pairing,
1137
+ },
1138
+ telegram: normalizeChannelStatus(null, TELEGRAM_ENABLED),
1139
+ sync: readSyncStatus(),
1140
+ uptime: uptimeHuman,
1141
+ spacePrivate: await resolveSpaceIsPrivate(parsedUrl),
1142
+ };
1143
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1144
+ res.end(renderDashboard(initialData));
1145
+ })();
1146
  return;
1147
  }
1148