somratpro commited on
Commit
1557f14
·
1 Parent(s): 3cbbb46

feat: add server-side space privacy detection to conditionally disable UptimeRobot setup UI

Browse files
Files changed (1) hide show
  1. health-server.js +141 -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,79 @@ 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 +233,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 +513,10 @@ function renderDashboard(initialData) {
402
  cursor: wait;
403
  }
404
 
 
 
 
 
405
  .helper-note {
406
  margin-top: 10px;
407
  font-size: 0.82rem;
@@ -560,37 +675,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 +740,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 +825,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 +1127,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 =
200
+ statusCode === 401 || statusCode === 403 || statusCode === 404;
201
+ spaceVisibilityCache.set(cacheKey, { isPrivate, timestamp: Date.now() });
202
+ return isPrivate;
203
+ } catch {
204
+ if (cached) return cached.isPrivate;
205
+ return false;
206
+ }
207
+ }
208
+
209
  function renderChannelBadge(channel, configuredLabel) {
210
  if (channel && channel.connected) {
211
  return '<div class="status-badge status-online"><div class="pulse"></div>Active</div>';
 
233
 
234
  function renderDashboard(initialData) {
235
  const controlUiHref = `${APP_BASE}/`;
236
+ const keepAwakeHtml = initialData.spacePrivate
237
+ ? `
238
+ <div id="uptimerobot-private-note" class="helper-summary">
239
+ <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.
240
+ </div>
241
+ `
242
+ : `
243
+ <div id="uptimerobot-public-flow">
244
+ <div id="uptimerobot-summary" class="helper-summary">
245
+ One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.
246
+ </div>
247
+ <button id="uptimerobot-toggle" class="helper-toggle" type="button">
248
+ Set Up Monitor
249
+ </button>
250
+ <div id="uptimerobot-shell" class="helper-shell hidden">
251
+ <div class="helper-copy">
252
+ Do <strong>not</strong> use the Read-only API key or a Monitor-specific API key.
253
+ </div>
254
+ <div class="helper-row">
255
+ <input
256
+ id="uptimerobot-key"
257
+ class="helper-input"
258
+ type="password"
259
+ placeholder="Paste your UptimeRobot Main API key"
260
+ autocomplete="off"
261
+ />
262
+ <button id="uptimerobot-btn" class="helper-button" type="button">
263
+ Create Monitor
264
+ </button>
265
+ </div>
266
+ <div class="helper-note">
267
+ One-time setup. Your key is only used to create the monitor for this Space.
268
+ </div>
269
+ </div>
270
+ </div>
271
+ `;
272
  return `
273
  <!DOCTYPE html>
274
  <html lang="en">
 
513
  cursor: wait;
514
  }
515
 
516
+ .hidden {
517
+ display: none !important;
518
+ }
519
+
520
  .helper-note {
521
  margin-top: 10px;
522
  font-size: 0.82rem;
 
675
 
676
  <div class="stat-card helper-card">
677
  <span class="stat-label">Keep Space Awake</span>
678
+ ${keepAwakeHtml}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
  <div id="uptimerobot-result" class="helper-result"></div>
680
  </div>
681
 
 
740
  }
741
 
742
  const monitorStateKey = 'huggingclaw_uptimerobot_setup_v1';
743
+ const KEEP_AWAKE_PRIVATE = ${initialData.spacePrivate ? "true" : "false"};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
 
745
  function setMonitorUiState(isConfigured) {
746
  const summary = document.getElementById('uptimerobot-summary');
747
  const shell = document.getElementById('uptimerobot-shell');
748
  const toggle = document.getElementById('uptimerobot-toggle');
749
 
750
+ if (!summary || !shell || !toggle) {
751
+ return;
752
+ }
753
+
754
  if (isConfigured) {
755
  summary.classList.add('success');
756
  summary.innerHTML = '<strong>Already set up.</strong> Your UptimeRobot monitor should keep this public Space awake.';
 
825
 
826
  updateStats();
827
  setInterval(updateStats, 10000);
 
828
  document.getElementById('control-ui-link').setAttribute('href', getDashboardBase() + '/app/' + getCurrentSearch());
829
+ if (!KEEP_AWAKE_PRIVATE) {
830
+ restoreMonitorUiState();
 
 
 
 
 
 
 
831
  document.getElementById('uptimerobot-btn').addEventListener('click', setupUptimeRobot);
832
  document.getElementById('uptimerobot-toggle').addEventListener('click', toggleMonitorSetup);
833
+ }
834
  </script>
835
  </body>
836
  </html>
 
1127
  }
1128
 
1129
  if (isDashboardRoute(pathname)) {
1130
+ void (async () => {
1131
+ const guardianStatus = readGuardianStatus();
1132
+ const initialData = {
1133
+ model: LLM_MODEL,
1134
+ whatsapp: {
1135
+ configured: guardianStatus.configured,
1136
+ connected: guardianStatus.connected,
1137
+ pairing: guardianStatus.pairing,
1138
+ },
1139
+ telegram: normalizeChannelStatus(null, TELEGRAM_ENABLED),
1140
+ sync: readSyncStatus(),
1141
+ uptime: uptimeHuman,
1142
+ spacePrivate: await resolveSpaceIsPrivate(parsedUrl),
1143
+ };
1144
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1145
+ res.end(renderDashboard(initialData));
1146
+ })();
1147
  return;
1148
  }
1149