akenomainx4 commited on
Commit
4a1fc0c
Β·
verified Β·
1 Parent(s): d07ec39

feat: manual registration fallback card when rate-limited

Browse files
Files changed (4) hide show
  1. app.js +49 -0
  2. public/app.js +64 -0
  3. public/index.html +38 -0
  4. public/style.css +78 -0
app.js CHANGED
@@ -65,6 +65,55 @@ function seedShellularConfig() {
65
 
66
  seedShellularConfig();
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  // ── Auth routes ───────────────────────────────────────────────────────────────
69
  app.post('/api/login', (req, res) => {
70
  const { key } = req.body;
 
65
 
66
  seedShellularConfig();
67
 
68
+ // ── Shellular machine-id helper ───────────────────────────────────────────────
69
+ // node-machine-id hashes /etc/machine-id with SHA-256. We replicate that here
70
+ // so the frontend can show the correct curl registration command.
71
+ function getHashedMachineId() {
72
+ try {
73
+ const raw = fs.readFileSync('/etc/machine-id', 'utf-8').trim();
74
+ return crypto.createHash('sha256').update(raw).digest('hex');
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ // Returns the hashed machine-id (safe to expose β€” not a secret).
81
+ app.get('/api/shellular/machine-id', (_req, res) => {
82
+ const id = getHashedMachineId();
83
+ id ? res.json({ machineId: id }) : res.status(500).json({ error: 'Cannot read machine-id' });
84
+ });
85
+
86
+ // Accepts a hostId obtained manually by the user, writes ~/.shellular/config.json,
87
+ // and restarts shellular so it skips the registration API entirely.
88
+ app.post('/api/shellular/seed-host', requireAuth, (req, res) => {
89
+ const { hostId } = req.body || {};
90
+ if (!hostId || typeof hostId !== 'string' || !hostId.trim()) {
91
+ return res.status(400).json({ error: 'hostId is required' });
92
+ }
93
+ const machineId = getHashedMachineId();
94
+ if (!machineId) return res.status(500).json({ error: 'Cannot read machine-id' });
95
+
96
+ try {
97
+ const shellularDir = path.join(os.homedir(), '.shellular');
98
+ fs.mkdirSync(shellularDir, { recursive: true });
99
+ fs.writeFileSync(
100
+ path.join(shellularDir, 'config.json'),
101
+ JSON.stringify({ hostId: hostId.trim(), machineId }, null, 2),
102
+ 'utf-8'
103
+ );
104
+
105
+ // Restart shellular so it picks up the new config
106
+ stopShellular();
107
+ outputBuffer = '';
108
+ broadcast({ type: 'clear' });
109
+ setTimeout(startShellular, 600);
110
+
111
+ res.json({ ok: true });
112
+ } catch (err) {
113
+ res.status(500).json({ error: err.message });
114
+ }
115
+ });
116
+
117
  // ── Auth routes ───────────────────────────────────────────────────────────────
118
  app.post('/api/login', (req, res) => {
119
  const { key } = req.body;
public/app.js CHANGED
@@ -269,6 +269,12 @@ function appendOutput(text) {
269
  fullOutput += text;
270
  logPre.textContent = fullOutput;
271
  logPre.scrollTop = logPre.scrollHeight;
 
 
 
 
 
 
272
  }
273
 
274
  /* ── Controls ────────────────────────────────────────────────────────────── */
@@ -299,6 +305,64 @@ async function authFetch(url, method = 'GET') {
299
  return res;
300
  }
301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  /* ── First-time setup panel ──────────────────────────────────────────────── */
303
  const setupCard = $('setup-card');
304
  let setupDone = false; // true once panel is shown OR secrets are already seeded
 
269
  fullOutput += text;
270
  logPre.textContent = fullOutput;
271
  logPre.scrollTop = logPre.scrollHeight;
272
+
273
+ // Show manual registration card as soon as rate-limit message appears
274
+ if (text.includes('rate-limited') || text.includes('Registration rate-limited')) {
275
+ rateLimitCount++;
276
+ if (rateLimitCount >= 1) loadManualCard();
277
+ }
278
  }
279
 
280
  /* ── Controls ────────────────────────────────────────────────────────────── */
 
305
  return res;
306
  }
307
 
308
+ /* ── Manual registration fallback ───────────────────────────────────────── */
309
+ const manualCard = $('manual-reg-card');
310
+ const manualCurlCmd = $('manual-curl-cmd');
311
+ const manualHostInput = $('manual-host-id');
312
+ const manualSubmitBtn = $('manual-submit-btn');
313
+ const manualError = $('manual-error');
314
+
315
+ let rateLimitCount = 0;
316
+ let machineIdLoaded = false;
317
+
318
+ async function loadManualCard() {
319
+ if (machineIdLoaded) { manualCard.classList.remove('hidden'); return; }
320
+ try {
321
+ const res = await fetch('/api/shellular/machine-id');
322
+ const { machineId } = await res.json();
323
+ const cmd = `curl -s -X POST "https://api.shellular.dev/register" -H "Content-Type: application/json" -d '{"machineId":"${machineId}","platform":"linux"}'`;
324
+ manualCurlCmd.textContent = cmd;
325
+ machineIdLoaded = true;
326
+ manualCard.classList.remove('hidden');
327
+ } catch { /* silent */ }
328
+ }
329
+
330
+ manualSubmitBtn.addEventListener('click', async () => {
331
+ const hostId = manualHostInput.value.trim();
332
+ if (!hostId) {
333
+ manualError.textContent = 'Please enter the hostId from the curl response.';
334
+ manualError.classList.remove('hidden');
335
+ return;
336
+ }
337
+ manualError.classList.add('hidden');
338
+ manualSubmitBtn.disabled = true;
339
+ manualSubmitBtn.textContent = 'Connecting…';
340
+
341
+ try {
342
+ const r = await fetch('/api/shellular/seed-host', {
343
+ method: 'POST',
344
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
345
+ body: JSON.stringify({ hostId }),
346
+ });
347
+ const data = await r.json();
348
+ if (!r.ok) throw new Error(data.error || 'Failed');
349
+
350
+ manualCard.classList.add('hidden');
351
+ rateLimitCount = 0;
352
+ // shellular is restarting β€” clear output and show loading
353
+ fullOutput = '';
354
+ logPre.textContent = '';
355
+ qrRendered = false;
356
+ setQrState('loading');
357
+ } catch (err) {
358
+ manualError.textContent = err.message;
359
+ manualError.classList.remove('hidden');
360
+ } finally {
361
+ manualSubmitBtn.disabled = false;
362
+ manualSubmitBtn.textContent = 'Connect';
363
+ }
364
+ });
365
+
366
  /* ── First-time setup panel ──────────────────────────────────────────────── */
367
  const setupCard = $('setup-card');
368
  let setupDone = false; // true once panel is shown OR secrets are already seeded
public/index.html CHANGED
@@ -145,6 +145,44 @@
145
  </div>
146
  </section>
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  <!-- Log card -->
149
  <section class="card log-card">
150
  <div class="card-header">
 
145
  </div>
146
  </section>
147
 
148
+ <!-- Manual registration fallback (shown when rate-limited) -->
149
+ <section id="manual-reg-card" class="card manual-card hidden">
150
+ <div class="card-header">
151
+ <h2>&#9888; Rate Limited β€” Manual Registration</h2>
152
+ <p>The shellular relay rejected automatic registration. Run one command from your terminal.</p>
153
+ </div>
154
+ <div class="manual-body">
155
+ <p class="manual-intro">
156
+ The Shellular registration API is temporarily rate-limiting this server's IP.
157
+ You can bypass it by registering from <strong>your own machine</strong> β€” it only takes 10 seconds.
158
+ </p>
159
+
160
+ <div class="manual-step">
161
+ <span class="step-num">1</span>
162
+ <div>
163
+ <p>Run this in your terminal (Mac / Linux / Windows WSL):</p>
164
+ <div class="code-block">
165
+ <code id="manual-curl-cmd">Loading…</code>
166
+ <button class="btn-copy" data-target="manual-curl-cmd">Copy</button>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <div class="manual-step">
172
+ <span class="step-num">2</span>
173
+ <div>
174
+ <p>You'll get back something like <code class="inline-code">{"success":true,"data":{"hostId":"<strong>XXXX</strong>"}}</code></p>
175
+ <p>Paste the <strong>hostId</strong> value below and click <strong>Connect</strong>:</p>
176
+ <div class="manual-input-row">
177
+ <input id="manual-host-id" type="text" placeholder='e.g. M58FBHn3YzbN' spellcheck="false" />
178
+ <button id="manual-submit-btn" class="btn btn-primary manual-btn">Connect</button>
179
+ </div>
180
+ <p id="manual-error" class="error-msg hidden"></p>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </section>
185
+
186
  <!-- Log card -->
187
  <section class="card log-card">
188
  <div class="card-header">
public/style.css CHANGED
@@ -353,6 +353,84 @@ html, body {
353
  color: var(--error) !important;
354
  }
355
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  /* ── Setup card ───────────────────────────────────────────────────────────── */
357
  .setup-body { padding: 16px 20px 20px; display: flex; flex-direction: column; gap: 14px; }
358
 
 
353
  color: var(--error) !important;
354
  }
355
 
356
+ /* ── Manual registration card ─────────────────────────────────────────────── */
357
+ .manual-card { grid-column: 1 / -1; border-color: rgba(250,204,21,.3); }
358
+
359
+ .manual-body { padding: 16px 20px 22px; display: flex; flex-direction: column; gap: 18px; }
360
+
361
+ .manual-intro { font-size: 13.5px; color: var(--muted); line-height: 1.6; }
362
+
363
+ .manual-step {
364
+ display: flex;
365
+ gap: 14px;
366
+ align-items: flex-start;
367
+ }
368
+ .manual-step > div { display: flex; flex-direction: column; gap: 8px; flex: 1; }
369
+ .manual-step p { font-size: 13.5px; color: var(--muted); line-height: 1.6; }
370
+
371
+ .step-num {
372
+ width: 26px; height: 26px;
373
+ border-radius: 50%;
374
+ background: var(--accent);
375
+ color: #0d0f14;
376
+ font-size: 13px;
377
+ font-weight: 700;
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ flex-shrink: 0;
382
+ margin-top: 2px;
383
+ }
384
+
385
+ .code-block {
386
+ display: flex;
387
+ align-items: center;
388
+ gap: 10px;
389
+ background: var(--bg);
390
+ border: 1px solid var(--border);
391
+ border-radius: var(--radius-sm);
392
+ padding: 10px 14px;
393
+ flex-wrap: wrap;
394
+ }
395
+ .code-block code {
396
+ font-family: var(--font-mono);
397
+ font-size: 12px;
398
+ color: var(--text);
399
+ flex: 1;
400
+ word-break: break-all;
401
+ }
402
+
403
+ .inline-code {
404
+ font-family: var(--font-mono);
405
+ font-size: 12px;
406
+ background: var(--surface2);
407
+ border: 1px solid var(--border);
408
+ border-radius: 4px;
409
+ padding: 1px 5px;
410
+ }
411
+
412
+ .manual-input-row {
413
+ display: flex;
414
+ gap: 10px;
415
+ align-items: center;
416
+ flex-wrap: wrap;
417
+ }
418
+ .manual-input-row input {
419
+ flex: 1;
420
+ min-width: 180px;
421
+ background: var(--surface2);
422
+ border: 1px solid var(--border);
423
+ border-radius: var(--radius-sm);
424
+ color: var(--text);
425
+ font-family: var(--font-mono);
426
+ font-size: 14px;
427
+ padding: 9px 12px;
428
+ outline: none;
429
+ transition: border-color .15s;
430
+ }
431
+ .manual-input-row input:focus { border-color: var(--accent); }
432
+ .manual-btn { padding: 9px 20px; }
433
+
434
  /* ── Setup card ───────────────────────────────────────────────────────────── */
435
  .setup-body { padding: 16px 20px 20px; display: flex; flex-direction: column; gap: 14px; }
436