Update app.js
Browse files
app.js
CHANGED
|
@@ -30,13 +30,10 @@ function stripAnsi(str) {
|
|
| 30 |
}
|
| 31 |
|
| 32 |
// ββ Pre-seed shellular config from env vars βββββββββββββββββββββββββββββββββββ
|
| 33 |
-
// If SHELLULAR_HOST_ID and SHELLULAR_KEY are set (as HF Secrets), we write
|
| 34 |
-
// them into ~/.shellular/ so shellular skips the registration API call entirely.
|
| 35 |
-
// This avoids rate-limit errors during container cold-starts.
|
| 36 |
function seedShellularConfig() {
|
| 37 |
-
const hostId
|
| 38 |
-
const keyB64
|
| 39 |
-
const machineId = process.env.SHELLULAR_MACHINE_ID;
|
| 40 |
|
| 41 |
if (!hostId || !keyB64 || !machineId) return;
|
| 42 |
|
|
@@ -47,13 +44,11 @@ function seedShellularConfig() {
|
|
| 47 |
try {
|
| 48 |
fs.mkdirSync(shellularDir, { recursive: true });
|
| 49 |
|
| 50 |
-
// Write config.json (skips registration on next shellular start)
|
| 51 |
if (!fs.existsSync(configFile)) {
|
| 52 |
fs.writeFileSync(configFile, JSON.stringify({ hostId, machineId }), 'utf-8');
|
| 53 |
console.log(`[shellular] seeded config: hostId=${hostId}`);
|
| 54 |
}
|
| 55 |
|
| 56 |
-
// Write the E2E key file (32 bytes from base64)
|
| 57 |
if (!fs.existsSync(keyFile)) {
|
| 58 |
fs.writeFileSync(keyFile, Buffer.from(keyB64, 'base64'), { mode: 0o600 });
|
| 59 |
console.log(`[shellular] seeded key: ${keyFile}`);
|
|
@@ -66,8 +61,6 @@ function seedShellularConfig() {
|
|
| 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();
|
|
@@ -77,14 +70,11 @@ function getHashedMachineId() {
|
|
| 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()) {
|
|
@@ -102,7 +92,6 @@ app.post('/api/shellular/seed-host', requireAuth, (req, res) => {
|
|
| 102 |
'utf-8'
|
| 103 |
);
|
| 104 |
|
| 105 |
-
// Restart shellular so it picks up the new config
|
| 106 |
stopShellular();
|
| 107 |
outputBuffer = '';
|
| 108 |
broadcast({ type: 'clear' });
|
|
@@ -168,7 +157,6 @@ function startShellular() {
|
|
| 168 |
stdio: ['ignore', 'pipe', 'pipe'],
|
| 169 |
});
|
| 170 |
|
| 171 |
-
// Accumulate stdout/stderr so we can detect the error type on exit
|
| 172 |
let procOutput = '';
|
| 173 |
|
| 174 |
const handleData = (chunk) => {
|
|
@@ -192,7 +180,6 @@ function startShellular() {
|
|
| 192 |
shellularProc.on('exit', (code, signal) => {
|
| 193 |
shellularProc = null;
|
| 194 |
|
| 195 |
-
// Detect rate-limit / registration failure (exit code 1, no signal)
|
| 196 |
const isRegError = code === 1 && !signal &&
|
| 197 |
(procOutput.includes('invalid_union') || procOutput.includes('Too many requests') ||
|
| 198 |
procOutput.includes('host registration'));
|
|
@@ -232,12 +219,52 @@ function stopShellular() {
|
|
| 232 |
shellularProc = null;
|
| 233 |
}
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
// ββ SSE stream βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 236 |
app.get('/api/stream', requireAuth, (req, res) => {
|
| 237 |
res.setHeader('Content-Type', 'text/event-stream');
|
| 238 |
res.setHeader('Cache-Control', 'no-cache');
|
| 239 |
res.setHeader('Connection', 'keep-alive');
|
| 240 |
-
res.setHeader('X-Accel-Buffering', 'no');
|
| 241 |
res.flushHeaders();
|
| 242 |
|
| 243 |
send(res, { type: 'status', status: shellularProc ? 'running' : 'stopped' });
|
|
@@ -275,8 +302,6 @@ app.get('/api/status', requireAuth, (_req, res) => {
|
|
| 275 |
res.json({ running: !!shellularProc });
|
| 276 |
});
|
| 277 |
|
| 278 |
-
// Tells the frontend whether SHELLULAR_* secrets are already saved.
|
| 279 |
-
// If not, the UI shows a first-time setup panel with values to copy into HF Secrets.
|
| 280 |
app.get('/api/setup-status', requireAuth, (_req, res) => {
|
| 281 |
const seeded = !!(
|
| 282 |
process.env.SHELLULAR_HOST_ID &&
|
|
@@ -286,7 +311,6 @@ app.get('/api/setup-status', requireAuth, (_req, res) => {
|
|
| 286 |
res.json({ seeded });
|
| 287 |
});
|
| 288 |
|
| 289 |
-
// Returns the registered hostId + base64 key so they can be saved as HF Secrets.
|
| 290 |
app.get('/api/shellular/credentials', requireAuth, (_req, res) => {
|
| 291 |
try {
|
| 292 |
const shellularDir = path.join(os.homedir(), '.shellular');
|
|
@@ -300,9 +324,6 @@ app.get('/api/shellular/credentials', requireAuth, (_req, res) => {
|
|
| 300 |
}
|
| 301 |
});
|
| 302 |
|
| 303 |
-
// Returns the QR data string ("hostId:keyBase64") for client-side QR rendering.
|
| 304 |
-
// This is safe to expose post-auth β the key is shared with the scanning device
|
| 305 |
-
// anyway (that is the point of the QR code).
|
| 306 |
app.get('/api/shellular/qr-data', requireAuth, (_req, res) => {
|
| 307 |
try {
|
| 308 |
const shellularDir = path.join(os.homedir(), '.shellular');
|
|
@@ -310,7 +331,6 @@ app.get('/api/shellular/qr-data', requireAuth, (_req, res) => {
|
|
| 310 |
const { hostId, machineId } = JSON.parse(configRaw);
|
| 311 |
const keyFile = path.join(shellularDir, `shellular-${machineId}.e2ee`);
|
| 312 |
const keyB64 = fs.readFileSync(keyFile).toString('base64');
|
| 313 |
-
// Same format shellular itself encodes into the terminal QR
|
| 314 |
res.json({ qrData: `${hostId}:${keyB64}` });
|
| 315 |
} catch {
|
| 316 |
res.status(404).json({ error: 'Config not seeded yet.' });
|
|
@@ -320,4 +340,9 @@ app.get('/api/shellular/qr-data', requireAuth, (_req, res) => {
|
|
| 320 |
// ββ Start ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 321 |
app.listen(PORT, '0.0.0.0', () => {
|
| 322 |
console.log(`Shellular Web UI β http://0.0.0.0:${PORT}`);
|
|
|
|
| 323 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
// ββ Pre-seed shellular config from env vars βββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
| 33 |
function seedShellularConfig() {
|
| 34 |
+
const hostId = process.env.SHELLULAR_HOST_ID;
|
| 35 |
+
const keyB64 = process.env.SHELLULAR_KEY;
|
| 36 |
+
const machineId = process.env.SHELLULAR_MACHINE_ID;
|
| 37 |
|
| 38 |
if (!hostId || !keyB64 || !machineId) return;
|
| 39 |
|
|
|
|
| 44 |
try {
|
| 45 |
fs.mkdirSync(shellularDir, { recursive: true });
|
| 46 |
|
|
|
|
| 47 |
if (!fs.existsSync(configFile)) {
|
| 48 |
fs.writeFileSync(configFile, JSON.stringify({ hostId, machineId }), 'utf-8');
|
| 49 |
console.log(`[shellular] seeded config: hostId=${hostId}`);
|
| 50 |
}
|
| 51 |
|
|
|
|
| 52 |
if (!fs.existsSync(keyFile)) {
|
| 53 |
fs.writeFileSync(keyFile, Buffer.from(keyB64, 'base64'), { mode: 0o600 });
|
| 54 |
console.log(`[shellular] seeded key: ${keyFile}`);
|
|
|
|
| 61 |
seedShellularConfig();
|
| 62 |
|
| 63 |
// ββ Shellular machine-id helper βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 64 |
function getHashedMachineId() {
|
| 65 |
try {
|
| 66 |
const raw = fs.readFileSync('/etc/machine-id', 'utf-8').trim();
|
|
|
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
|
|
|
| 73 |
app.get('/api/shellular/machine-id', (_req, res) => {
|
| 74 |
const id = getHashedMachineId();
|
| 75 |
id ? res.json({ machineId: id }) : res.status(500).json({ error: 'Cannot read machine-id' });
|
| 76 |
});
|
| 77 |
|
|
|
|
|
|
|
| 78 |
app.post('/api/shellular/seed-host', requireAuth, (req, res) => {
|
| 79 |
const { hostId } = req.body || {};
|
| 80 |
if (!hostId || typeof hostId !== 'string' || !hostId.trim()) {
|
|
|
|
| 92 |
'utf-8'
|
| 93 |
);
|
| 94 |
|
|
|
|
| 95 |
stopShellular();
|
| 96 |
outputBuffer = '';
|
| 97 |
broadcast({ type: 'clear' });
|
|
|
|
| 157 |
stdio: ['ignore', 'pipe', 'pipe'],
|
| 158 |
});
|
| 159 |
|
|
|
|
| 160 |
let procOutput = '';
|
| 161 |
|
| 162 |
const handleData = (chunk) => {
|
|
|
|
| 180 |
shellularProc.on('exit', (code, signal) => {
|
| 181 |
shellularProc = null;
|
| 182 |
|
|
|
|
| 183 |
const isRegError = code === 1 && !signal &&
|
| 184 |
(procOutput.includes('invalid_union') || procOutput.includes('Too many requests') ||
|
| 185 |
procOutput.includes('host registration'));
|
|
|
|
| 219 |
shellularProc = null;
|
| 220 |
}
|
| 221 |
|
| 222 |
+
// ββ Python sync.py subprocess βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 223 |
+
let syncProc = null;
|
| 224 |
+
|
| 225 |
+
function startSyncPy() {
|
| 226 |
+
if (syncProc) return;
|
| 227 |
+
|
| 228 |
+
console.log('[sync] Starting sync.py...');
|
| 229 |
+
|
| 230 |
+
syncProc = spawn('python3', [path.join(__dirname, 'sync.py')], {
|
| 231 |
+
env: { ...process.env },
|
| 232 |
+
stdio: ['ignore', 'pipe', 'pipe'],
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
syncProc.stdout.on('data', (chunk) => {
|
| 236 |
+
console.log('[sync]', chunk.toString().trim());
|
| 237 |
+
});
|
| 238 |
+
|
| 239 |
+
syncProc.stderr.on('data', (chunk) => {
|
| 240 |
+
console.error('[sync:err]', chunk.toString().trim());
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
syncProc.on('error', (err) => {
|
| 244 |
+
console.error('[sync] Spawn error:', err.message);
|
| 245 |
+
syncProc = null;
|
| 246 |
+
});
|
| 247 |
+
|
| 248 |
+
syncProc.on('exit', (code, signal) => {
|
| 249 |
+
console.warn(`[sync] sync.py exited β code=${code ?? '?'}, signal=${signal ?? 'none'}`);
|
| 250 |
+
syncProc = null;
|
| 251 |
+
// Auto-restart after 10s if it crashes
|
| 252 |
+
setTimeout(startSyncPy, 10_000);
|
| 253 |
+
});
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
function stopSyncPy() {
|
| 257 |
+
if (!syncProc) return;
|
| 258 |
+
syncProc.kill('SIGTERM');
|
| 259 |
+
syncProc = null;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
// ββ SSE stream βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 263 |
app.get('/api/stream', requireAuth, (req, res) => {
|
| 264 |
res.setHeader('Content-Type', 'text/event-stream');
|
| 265 |
res.setHeader('Cache-Control', 'no-cache');
|
| 266 |
res.setHeader('Connection', 'keep-alive');
|
| 267 |
+
res.setHeader('X-Accel-Buffering', 'no');
|
| 268 |
res.flushHeaders();
|
| 269 |
|
| 270 |
send(res, { type: 'status', status: shellularProc ? 'running' : 'stopped' });
|
|
|
|
| 302 |
res.json({ running: !!shellularProc });
|
| 303 |
});
|
| 304 |
|
|
|
|
|
|
|
| 305 |
app.get('/api/setup-status', requireAuth, (_req, res) => {
|
| 306 |
const seeded = !!(
|
| 307 |
process.env.SHELLULAR_HOST_ID &&
|
|
|
|
| 311 |
res.json({ seeded });
|
| 312 |
});
|
| 313 |
|
|
|
|
| 314 |
app.get('/api/shellular/credentials', requireAuth, (_req, res) => {
|
| 315 |
try {
|
| 316 |
const shellularDir = path.join(os.homedir(), '.shellular');
|
|
|
|
| 324 |
}
|
| 325 |
});
|
| 326 |
|
|
|
|
|
|
|
|
|
|
| 327 |
app.get('/api/shellular/qr-data', requireAuth, (_req, res) => {
|
| 328 |
try {
|
| 329 |
const shellularDir = path.join(os.homedir(), '.shellular');
|
|
|
|
| 331 |
const { hostId, machineId } = JSON.parse(configRaw);
|
| 332 |
const keyFile = path.join(shellularDir, `shellular-${machineId}.e2ee`);
|
| 333 |
const keyB64 = fs.readFileSync(keyFile).toString('base64');
|
|
|
|
| 334 |
res.json({ qrData: `${hostId}:${keyB64}` });
|
| 335 |
} catch {
|
| 336 |
res.status(404).json({ error: 'Config not seeded yet.' });
|
|
|
|
| 340 |
// ββ Start ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 341 |
app.listen(PORT, '0.0.0.0', () => {
|
| 342 |
console.log(`Shellular Web UI β http://0.0.0.0:${PORT}`);
|
| 343 |
+
startSyncPy(); // π Launch sync.py on server start
|
| 344 |
});
|
| 345 |
+
|
| 346 |
+
// ββ Graceful shutdown ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 347 |
+
process.on('SIGTERM', () => { stopShellular(); stopSyncPy(); });
|
| 348 |
+
process.on('SIGINT', () => { stopShellular(); stopSyncPy(); });
|