soxogvv commited on
Commit
996891a
·
verified ·
1 Parent(s): 5fd7a59

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +72 -0
  2. README.md +127 -5
  3. app.js +323 -0
  4. package-lock.json +776 -0
  5. package.json +17 -0
Dockerfile ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Base image ────────────────────────────────────────────────────────────────
2
+ FROM node:20-slim
3
+
4
+ # ── System dependencies ───────────────────────────────────────────────────────
5
+ # python3 / make / g++ — required to compile node-pty (used by shellular)
6
+ # python3-pip — for yt-dlp and other Python tools
7
+ # wget / curl — general-purpose download utilities
8
+ # git — version control
9
+ # neofetch — system info display
10
+ # mediainfo — media file metadata inspector
11
+ RUN apt-get update && \
12
+ apt-get install -y --no-install-recommends \
13
+ python3 \
14
+ python3-pip \
15
+ make \
16
+ g++ \
17
+ wget \
18
+ curl \
19
+ git \
20
+ sudo \
21
+ neofetch \
22
+ mediainfo \
23
+ python3-venv \
24
+ screen \
25
+ ca-certificates \
26
+ openssl \
27
+ && rm -rf /var/lib/apt/lists/*
28
+
29
+ # ── Passwordless sudo for the node user ──────────────────────────────────────
30
+ # Lets you run sudo apt install <pkg> inside the shellular terminal
31
+ # without needing a password.
32
+ RUN echo "node ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/node && \
33
+ chmod 0440 /etc/sudoers.d/node
34
+
35
+ # ── yt-dlp (installed via pip, break-system-packages is fine in a container) ──
36
+ RUN pip3 install --no-cache-dir --break-system-packages yt-dlp
37
+
38
+ # ── Pin a stable machine-id ───────────────────────────────────────────────────
39
+ # WHY THIS IS HERE (not in secrets):
40
+ # • The shellular relay authenticates connections by matching the machineId
41
+ # that was used at registration time. The container's /etc/machine-id must
42
+ # always hash to the same value as SHELLULAR_MACHINE_ID in HF Secrets.
43
+ # • /etc/machine-id must be written as root, at BUILD time. HF Spaces runs
44
+ # all containers as UID 1000 at runtime, so it cannot be written then.
45
+ # • This value is a stable identifier, NOT a secret or auth token.
46
+ # The actual secrets (SHELLULAR_KEY, SHELLULAR_HOST_ID) live in HF Secrets.
47
+ RUN echo "d8904b4d338adf83688caac869f64c0b" > /etc/machine-id && \
48
+ mkdir -p /var/lib/dbus && \
49
+ echo "d8904b4d338adf83688caac869f64c0b" > /var/lib/dbus/machine-id
50
+
51
+ # ── Use the built-in "node" user (UID 1000, matches HF Spaces runtime) ────────
52
+ USER node
53
+ ENV HOME=/home/node \
54
+ PATH="/home/node/.npm-global/bin:${PATH}"
55
+
56
+ # ── Install shellular globally ────────────────────────────────────────────────
57
+ RUN npm config set prefix /home/node/.npm-global && \
58
+ npm install -g shellular
59
+
60
+ # ── App ───────────────────────────────────────────────────────────────────────
61
+ WORKDIR /home/node/app
62
+
63
+ COPY --chown=node:node package*.json ./
64
+ RUN npm install --omit=dev
65
+
66
+ COPY --chown=node:node . .
67
+
68
+ # ── Runtime ───────────────────────────────────────────────────────────────────
69
+ EXPOSE 7860
70
+ ENV PORT=7860
71
+
72
+ CMD ["node", "app.js"]
README.md CHANGED
@@ -1,10 +1,132 @@
1
  ---
2
- title: Shell
3
- emoji: 📚
4
- colorFrom: red
5
- colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Shellular Web UI
3
+ emoji: 🖥️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # Shellular Web UI
11
+
12
+ A self-hosted web interface that runs **[Shellular](https://shellular.dev)** inside a Hugging Face Space and shows the pairing QR code in your browser — no terminal access needed.
13
+
14
+ ---
15
+
16
+ ## What it does
17
+
18
+ - Password-protected login page hosted on your HF Space URL
19
+ - Starts Shellular automatically after login and renders a scannable QR code
20
+ - All traffic between your phone and the host is **end-to-end encrypted** (libsodium)
21
+ - One-time setup panel guides you through saving permanent secrets so restarts are instant
22
+
23
+ ---
24
+
25
+ ## Architecture
26
+
27
+ ```
28
+ Your Phone ──scan QR──▶ Shellular App
29
+
30
+ wss://api.shellular.dev (relay, E2E encrypted)
31
+
32
+ HF Space
33
+ (Node.js + Shellular CLI)
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Quick Start
39
+
40
+ ### Step 1 — Fork the Space
41
+
42
+ Click **Duplicate this Space** on the top-right of this Space page.
43
+
44
+ ### Step 2 — Add `SECRET_KEY`
45
+
46
+ Go to your forked Space → **Settings → Variables and secrets → New secret**
47
+
48
+ | Name | Value |
49
+ |------|-------|
50
+ | `SECRET_KEY` | Any strong password — this is the login password for the web UI |
51
+
52
+ The Space will restart automatically.
53
+
54
+ ### Step 3 — Login
55
+
56
+ Open your Space URL (`https://your-username-vps.hf.space`), enter your `SECRET_KEY` and click **Login**.
57
+
58
+ Shellular starts automatically. Within a few seconds the QR code appears.
59
+
60
+ ### Step 4 — Scan the QR code
61
+
62
+ Install the **Shellular app** on your phone → [shellular.dev](https://shellular.dev)
63
+
64
+ Open the app and scan the QR code on the dashboard. Your phone is now connected.
65
+
66
+ ### Step 5 — Save the permanent secrets (important!)
67
+
68
+ After the QR code appears, a **⚡ One-time Setup** panel appears showing three values with **Copy** buttons.
69
+
70
+ Add all three as secrets in your Space settings:
71
+
72
+ **Space → Settings → Variables and secrets → New secret**
73
+
74
+ | Secret name | Where to get it |
75
+ |-------------|-----------------|
76
+ | `SHELLULAR_HOST_ID` | Copy from the One-time Setup panel |
77
+ | `SHELLULAR_MACHINE_ID` | Copy from the One-time Setup panel |
78
+ | `SHELLULAR_KEY` | Copy from the One-time Setup panel |
79
+
80
+ After adding all three, restart the Space. The setup panel disappears permanently.
81
+
82
+ > **Why are these needed?**
83
+ > Shellular calls `api.shellular.dev` to register on every cold start.
84
+ > That API is rate-limited — if the Space restarts too often, registration fails
85
+ > and the QR never appears. Saving these three values means Shellular reuses the
86
+ > same registered identity on every restart with no API call at all.
87
+
88
+ ---
89
+
90
+ ## All Secrets Reference
91
+
92
+ | Name | Required | Description |
93
+ |------|----------|-------------|
94
+ | `SECRET_KEY` | ✅ Always | Login password for the web UI. Choose anything strong. |
95
+ | `SHELLULAR_HOST_ID` | ⭐ Strongly recommended | The relay server's ID for this host. Shown in the One-time Setup panel. |
96
+ | `SHELLULAR_MACHINE_ID` | ⭐ Strongly recommended | Hashed machine identifier tied to the registration. Shown in the One-time Setup panel. |
97
+ | `SHELLULAR_KEY` | ⭐ Strongly recommended | 32-byte base64 E2E encryption key. Shown in the One-time Setup panel. |
98
+
99
+ ---
100
+
101
+ ## Troubleshooting
102
+
103
+ ### QR code doesn't appear / "Shellular stopped"
104
+
105
+ The Shellular registration API was rate-limited.
106
+
107
+ **Fix:** Save the three `SHELLULAR_*` secrets from the One-time Setup panel (Step 5). Once saved, Shellular skips registration on every restart — no rate limit, instant QR.
108
+
109
+ If you have not yet seen the One-time Setup panel (e.g. the first registration also failed), wait a few minutes and click **▶ Restart** on the dashboard to try again.
110
+
111
+ ### "Invalid key" on login
112
+
113
+ `SECRET_KEY` is not set or was typed incorrectly. Check **Settings → Variables and secrets**.
114
+
115
+ ### One-time Setup panel not appearing
116
+
117
+ The panel only appears after the QR code is successfully rendered for the first time AND the `SHELLULAR_*` secrets are not yet saved. If the QR rendered but the panel did not appear, refresh the page and log in again.
118
+
119
+ ---
120
+
121
+ ## Project Structure
122
+
123
+ ```
124
+ ├── app.js # Express server — auth, SSE, shellular lifecycle
125
+ ├── package.json
126
+ ├── Dockerfile # Node 20-slim, pinned machine-id for stable registration
127
+ └── public/
128
+ ├── index.html # Login page + dashboard (QR, One-time Setup, Output log)
129
+ ├── style.css # Dark theme
130
+ ├── app.js # Frontend JS
131
+ └── qrcode.min.js # Bundled QR renderer (no CDN dependency)
132
+ ```
app.js ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { spawn } from 'child_process';
3
+ import crypto from 'crypto';
4
+ import fs from 'fs';
5
+ import os from 'os';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ const app = express();
12
+ const PORT = process.env.PORT || 7860;
13
+ const SECRET_KEY = process.env.SECRET_KEY;
14
+
15
+ if (!SECRET_KEY) {
16
+ console.error('[ERROR] SECRET_KEY environment variable is not set. Exiting.');
17
+ process.exit(1);
18
+ }
19
+
20
+ app.use(express.json());
21
+ app.use(express.static(path.join(__dirname, 'public')));
22
+
23
+ // ── Session store ─────────────────────────────────────────────────────────────
24
+ const sessions = new Set();
25
+
26
+ // ── ANSI stripper ─────────────────────────────────────────────────────────────
27
+ function stripAnsi(str) {
28
+ // eslint-disable-next-line no-control-regex
29
+ return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]/g, '');
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 = process.env.SHELLULAR_HOST_ID;
38
+ const keyB64 = process.env.SHELLULAR_KEY; // base64-encoded 32-byte key
39
+ const machineId = process.env.SHELLULAR_MACHINE_ID; // must match registration
40
+
41
+ if (!hostId || !keyB64 || !machineId) return;
42
+
43
+ const shellularDir = path.join(os.homedir(), '.shellular');
44
+ const configFile = path.join(shellularDir, 'config.json');
45
+ const keyFile = path.join(shellularDir, `shellular-${machineId}.e2ee`);
46
+
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}`);
60
+ }
61
+ } catch (err) {
62
+ console.error('[shellular] failed to seed config:', err.message);
63
+ }
64
+ }
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;
120
+ if (!key || key !== SECRET_KEY) {
121
+ return res.status(401).json({ error: 'Invalid key' });
122
+ }
123
+ const token = crypto.randomUUID();
124
+ sessions.add(token);
125
+ res.json({ token });
126
+ });
127
+
128
+ app.post('/api/logout', (req, res) => {
129
+ const token = req.headers.authorization?.split(' ')[1];
130
+ sessions.delete(token);
131
+ res.json({ ok: true });
132
+ });
133
+
134
+ // ── Auth middleware ────────────────────────────────────────────────────────────
135
+ function requireAuth(req, res, next) {
136
+ const token = req.headers.authorization?.split(' ')[1];
137
+ if (!token || !sessions.has(token)) {
138
+ return res.status(401).json({ error: 'Unauthorized' });
139
+ }
140
+ next();
141
+ }
142
+
143
+ // ── Shellular process management ──────────────────────────────────────────────
144
+ let shellularProc = null;
145
+ let outputBuffer = '';
146
+ const sseClients = new Set();
147
+
148
+ function send(res, payload) {
149
+ res.write(`data: ${JSON.stringify(payload)}\n\n`);
150
+ }
151
+
152
+ function broadcast(payload) {
153
+ const frame = `data: ${JSON.stringify(payload)}\n\n`;
154
+ for (const client of sseClients) {
155
+ client.write(frame);
156
+ }
157
+ }
158
+
159
+ let retryTimer = null;
160
+
161
+ function startShellular() {
162
+ if (shellularProc || retryTimer) return;
163
+
164
+ broadcast({ type: 'status', status: 'starting' });
165
+
166
+ shellularProc = spawn('shellular', ['--unknown-clients', 'always-allow'], {
167
+ env: { ...process.env, FORCE_COLOR: '0' },
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) => {
175
+ const text = stripAnsi(chunk.toString());
176
+ procOutput += text;
177
+ outputBuffer += text;
178
+ broadcast({ type: 'output', text });
179
+ };
180
+
181
+ shellularProc.stdout.on('data', handleData);
182
+ shellularProc.stderr.on('data', handleData);
183
+
184
+ shellularProc.on('error', (err) => {
185
+ const text = `\n[spawn error] ${err.message}\n`;
186
+ outputBuffer += text;
187
+ broadcast({ type: 'output', text });
188
+ shellularProc = null;
189
+ broadcast({ type: 'status', status: 'error' });
190
+ });
191
+
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'));
199
+
200
+ if (isRegError) {
201
+ const WAIT = 30;
202
+ const msg = `\n⚠ Registration rate-limited by shellular API.\n` +
203
+ ` Retrying automatically in ${WAIT}s — please wait…\n`;
204
+ outputBuffer += msg;
205
+ broadcast({ type: 'output', text: msg });
206
+ broadcast({ type: 'status', status: 'retrying' });
207
+
208
+ retryTimer = setTimeout(() => {
209
+ retryTimer = null;
210
+ const msg2 = '\n[Retrying registration…]\n';
211
+ outputBuffer += msg2;
212
+ broadcast({ type: 'output', text: msg2 });
213
+ startShellular();
214
+ }, WAIT * 1000);
215
+ } else {
216
+ const text = code !== 0
217
+ ? `\n[shellular exited — code=${code ?? '?'}, signal=${signal ?? 'none'}]\n`
218
+ : '\n[shellular disconnected]\n';
219
+ outputBuffer += text;
220
+ broadcast({ type: 'output', text });
221
+ broadcast({ type: 'status', status: 'stopped' });
222
+ }
223
+ });
224
+
225
+ broadcast({ type: 'status', status: 'running' });
226
+ }
227
+
228
+ function stopShellular() {
229
+ if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
230
+ if (!shellularProc) return;
231
+ shellularProc.kill('SIGTERM');
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'); // disable nginx buffering on HF
241
+ res.flushHeaders();
242
+
243
+ send(res, { type: 'status', status: shellularProc ? 'running' : 'stopped' });
244
+
245
+ if (outputBuffer) {
246
+ send(res, { type: 'output', text: outputBuffer });
247
+ }
248
+
249
+ sseClients.add(res);
250
+ req.on('close', () => sseClients.delete(res));
251
+ });
252
+
253
+ // ── Control endpoints ──────────────────────────────────────────────────────────
254
+ app.post('/api/shellular/start', requireAuth, (_req, res) => {
255
+ startShellular();
256
+ res.json({ ok: true, running: !!shellularProc });
257
+ });
258
+
259
+ app.post('/api/shellular/stop', requireAuth, (_req, res) => {
260
+ stopShellular();
261
+ outputBuffer = '';
262
+ broadcast({ type: 'output', text: '' });
263
+ res.json({ ok: true });
264
+ });
265
+
266
+ app.post('/api/shellular/restart', requireAuth, (_req, res) => {
267
+ stopShellular();
268
+ outputBuffer = '';
269
+ broadcast({ type: 'clear' });
270
+ setTimeout(startShellular, 600);
271
+ res.json({ ok: true });
272
+ });
273
+
274
+ 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 &&
283
+ process.env.SHELLULAR_KEY &&
284
+ process.env.SHELLULAR_MACHINE_ID
285
+ );
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');
293
+ const configRaw = fs.readFileSync(path.join(shellularDir, 'config.json'), 'utf-8');
294
+ const { hostId, machineId } = JSON.parse(configRaw);
295
+ const keyFile = path.join(shellularDir, `shellular-${machineId}.e2ee`);
296
+ const keyB64 = fs.readFileSync(keyFile).toString('base64');
297
+ res.json({ hostId, machineId, keyB64 });
298
+ } catch {
299
+ res.status(404).json({ error: 'Not registered yet.' });
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');
309
+ const configRaw = fs.readFileSync(path.join(shellularDir, 'config.json'), 'utf-8');
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.' });
317
+ }
318
+ });
319
+
320
+ // ── Start ──────────────────────────────────────────────────────────────────────
321
+ app.listen(PORT, '0.0.0.0', () => {
322
+ console.log(`Shellular Web UI → http://0.0.0.0:${PORT}`);
323
+ });
package-lock.json ADDED
@@ -0,0 +1,776 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "shellular-web",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "shellular-web",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "express": "^4.19.2"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ }
16
+ },
17
+ "node_modules/accepts": {
18
+ "version": "1.3.8",
19
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
20
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
21
+ "dependencies": {
22
+ "mime-types": "~2.1.34",
23
+ "negotiator": "0.6.3"
24
+ },
25
+ "engines": {
26
+ "node": ">= 0.6"
27
+ }
28
+ },
29
+ "node_modules/array-flatten": {
30
+ "version": "1.1.1",
31
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
32
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
33
+ },
34
+ "node_modules/body-parser": {
35
+ "version": "1.20.5",
36
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
37
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
38
+ "dependencies": {
39
+ "bytes": "~3.1.2",
40
+ "content-type": "~1.0.5",
41
+ "debug": "2.6.9",
42
+ "depd": "2.0.0",
43
+ "destroy": "~1.2.0",
44
+ "http-errors": "~2.0.1",
45
+ "iconv-lite": "~0.4.24",
46
+ "on-finished": "~2.4.1",
47
+ "qs": "~6.15.1",
48
+ "raw-body": "~2.5.3",
49
+ "type-is": "~1.6.18",
50
+ "unpipe": "~1.0.0"
51
+ },
52
+ "engines": {
53
+ "node": ">= 0.8",
54
+ "npm": "1.2.8000 || >= 1.4.16"
55
+ }
56
+ },
57
+ "node_modules/body-parser/node_modules/qs": {
58
+ "version": "6.15.1",
59
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
60
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
61
+ "dependencies": {
62
+ "side-channel": "^1.1.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=0.6"
66
+ },
67
+ "funding": {
68
+ "url": "https://github.com/sponsors/ljharb"
69
+ }
70
+ },
71
+ "node_modules/bytes": {
72
+ "version": "3.1.2",
73
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
74
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
75
+ "engines": {
76
+ "node": ">= 0.8"
77
+ }
78
+ },
79
+ "node_modules/call-bind-apply-helpers": {
80
+ "version": "1.0.2",
81
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
82
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
83
+ "dependencies": {
84
+ "es-errors": "^1.3.0",
85
+ "function-bind": "^1.1.2"
86
+ },
87
+ "engines": {
88
+ "node": ">= 0.4"
89
+ }
90
+ },
91
+ "node_modules/call-bound": {
92
+ "version": "1.0.4",
93
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
94
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
95
+ "dependencies": {
96
+ "call-bind-apply-helpers": "^1.0.2",
97
+ "get-intrinsic": "^1.3.0"
98
+ },
99
+ "engines": {
100
+ "node": ">= 0.4"
101
+ },
102
+ "funding": {
103
+ "url": "https://github.com/sponsors/ljharb"
104
+ }
105
+ },
106
+ "node_modules/content-disposition": {
107
+ "version": "0.5.4",
108
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
109
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
110
+ "dependencies": {
111
+ "safe-buffer": "5.2.1"
112
+ },
113
+ "engines": {
114
+ "node": ">= 0.6"
115
+ }
116
+ },
117
+ "node_modules/content-type": {
118
+ "version": "1.0.5",
119
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
120
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
121
+ "engines": {
122
+ "node": ">= 0.6"
123
+ }
124
+ },
125
+ "node_modules/cookie": {
126
+ "version": "0.7.2",
127
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
128
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
129
+ "engines": {
130
+ "node": ">= 0.6"
131
+ }
132
+ },
133
+ "node_modules/cookie-signature": {
134
+ "version": "1.0.7",
135
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
136
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
137
+ },
138
+ "node_modules/debug": {
139
+ "version": "2.6.9",
140
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
141
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
142
+ "dependencies": {
143
+ "ms": "2.0.0"
144
+ }
145
+ },
146
+ "node_modules/depd": {
147
+ "version": "2.0.0",
148
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
149
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
150
+ "engines": {
151
+ "node": ">= 0.8"
152
+ }
153
+ },
154
+ "node_modules/destroy": {
155
+ "version": "1.2.0",
156
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
157
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
158
+ "engines": {
159
+ "node": ">= 0.8",
160
+ "npm": "1.2.8000 || >= 1.4.16"
161
+ }
162
+ },
163
+ "node_modules/dunder-proto": {
164
+ "version": "1.0.1",
165
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
166
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
167
+ "dependencies": {
168
+ "call-bind-apply-helpers": "^1.0.1",
169
+ "es-errors": "^1.3.0",
170
+ "gopd": "^1.2.0"
171
+ },
172
+ "engines": {
173
+ "node": ">= 0.4"
174
+ }
175
+ },
176
+ "node_modules/ee-first": {
177
+ "version": "1.1.1",
178
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
179
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
180
+ },
181
+ "node_modules/encodeurl": {
182
+ "version": "2.0.0",
183
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
184
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
185
+ "engines": {
186
+ "node": ">= 0.8"
187
+ }
188
+ },
189
+ "node_modules/es-define-property": {
190
+ "version": "1.0.1",
191
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
192
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
193
+ "engines": {
194
+ "node": ">= 0.4"
195
+ }
196
+ },
197
+ "node_modules/es-errors": {
198
+ "version": "1.3.0",
199
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
200
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
201
+ "engines": {
202
+ "node": ">= 0.4"
203
+ }
204
+ },
205
+ "node_modules/es-object-atoms": {
206
+ "version": "1.1.1",
207
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
208
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
209
+ "dependencies": {
210
+ "es-errors": "^1.3.0"
211
+ },
212
+ "engines": {
213
+ "node": ">= 0.4"
214
+ }
215
+ },
216
+ "node_modules/escape-html": {
217
+ "version": "1.0.3",
218
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
219
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
220
+ },
221
+ "node_modules/etag": {
222
+ "version": "1.8.1",
223
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
224
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
225
+ "engines": {
226
+ "node": ">= 0.6"
227
+ }
228
+ },
229
+ "node_modules/express": {
230
+ "version": "4.22.1",
231
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
232
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
233
+ "dependencies": {
234
+ "accepts": "~1.3.8",
235
+ "array-flatten": "1.1.1",
236
+ "body-parser": "~1.20.3",
237
+ "content-disposition": "~0.5.4",
238
+ "content-type": "~1.0.4",
239
+ "cookie": "~0.7.1",
240
+ "cookie-signature": "~1.0.6",
241
+ "debug": "2.6.9",
242
+ "depd": "2.0.0",
243
+ "encodeurl": "~2.0.0",
244
+ "escape-html": "~1.0.3",
245
+ "etag": "~1.8.1",
246
+ "finalhandler": "~1.3.1",
247
+ "fresh": "~0.5.2",
248
+ "http-errors": "~2.0.0",
249
+ "merge-descriptors": "1.0.3",
250
+ "methods": "~1.1.2",
251
+ "on-finished": "~2.4.1",
252
+ "parseurl": "~1.3.3",
253
+ "path-to-regexp": "~0.1.12",
254
+ "proxy-addr": "~2.0.7",
255
+ "qs": "~6.14.0",
256
+ "range-parser": "~1.2.1",
257
+ "safe-buffer": "5.2.1",
258
+ "send": "~0.19.0",
259
+ "serve-static": "~1.16.2",
260
+ "setprototypeof": "1.2.0",
261
+ "statuses": "~2.0.1",
262
+ "type-is": "~1.6.18",
263
+ "utils-merge": "1.0.1",
264
+ "vary": "~1.1.2"
265
+ },
266
+ "engines": {
267
+ "node": ">= 0.10.0"
268
+ },
269
+ "funding": {
270
+ "type": "opencollective",
271
+ "url": "https://opencollective.com/express"
272
+ }
273
+ },
274
+ "node_modules/finalhandler": {
275
+ "version": "1.3.2",
276
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
277
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
278
+ "dependencies": {
279
+ "debug": "2.6.9",
280
+ "encodeurl": "~2.0.0",
281
+ "escape-html": "~1.0.3",
282
+ "on-finished": "~2.4.1",
283
+ "parseurl": "~1.3.3",
284
+ "statuses": "~2.0.2",
285
+ "unpipe": "~1.0.0"
286
+ },
287
+ "engines": {
288
+ "node": ">= 0.8"
289
+ }
290
+ },
291
+ "node_modules/forwarded": {
292
+ "version": "0.2.0",
293
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
294
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
295
+ "engines": {
296
+ "node": ">= 0.6"
297
+ }
298
+ },
299
+ "node_modules/fresh": {
300
+ "version": "0.5.2",
301
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
302
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
303
+ "engines": {
304
+ "node": ">= 0.6"
305
+ }
306
+ },
307
+ "node_modules/function-bind": {
308
+ "version": "1.1.2",
309
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
310
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
311
+ "funding": {
312
+ "url": "https://github.com/sponsors/ljharb"
313
+ }
314
+ },
315
+ "node_modules/get-intrinsic": {
316
+ "version": "1.3.0",
317
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
318
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
319
+ "dependencies": {
320
+ "call-bind-apply-helpers": "^1.0.2",
321
+ "es-define-property": "^1.0.1",
322
+ "es-errors": "^1.3.0",
323
+ "es-object-atoms": "^1.1.1",
324
+ "function-bind": "^1.1.2",
325
+ "get-proto": "^1.0.1",
326
+ "gopd": "^1.2.0",
327
+ "has-symbols": "^1.1.0",
328
+ "hasown": "^2.0.2",
329
+ "math-intrinsics": "^1.1.0"
330
+ },
331
+ "engines": {
332
+ "node": ">= 0.4"
333
+ },
334
+ "funding": {
335
+ "url": "https://github.com/sponsors/ljharb"
336
+ }
337
+ },
338
+ "node_modules/get-proto": {
339
+ "version": "1.0.1",
340
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
341
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
342
+ "dependencies": {
343
+ "dunder-proto": "^1.0.1",
344
+ "es-object-atoms": "^1.0.0"
345
+ },
346
+ "engines": {
347
+ "node": ">= 0.4"
348
+ }
349
+ },
350
+ "node_modules/gopd": {
351
+ "version": "1.2.0",
352
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
353
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
354
+ "engines": {
355
+ "node": ">= 0.4"
356
+ },
357
+ "funding": {
358
+ "url": "https://github.com/sponsors/ljharb"
359
+ }
360
+ },
361
+ "node_modules/has-symbols": {
362
+ "version": "1.1.0",
363
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
364
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
365
+ "engines": {
366
+ "node": ">= 0.4"
367
+ },
368
+ "funding": {
369
+ "url": "https://github.com/sponsors/ljharb"
370
+ }
371
+ },
372
+ "node_modules/hasown": {
373
+ "version": "2.0.3",
374
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
375
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
376
+ "dependencies": {
377
+ "function-bind": "^1.1.2"
378
+ },
379
+ "engines": {
380
+ "node": ">= 0.4"
381
+ }
382
+ },
383
+ "node_modules/http-errors": {
384
+ "version": "2.0.1",
385
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
386
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
387
+ "dependencies": {
388
+ "depd": "~2.0.0",
389
+ "inherits": "~2.0.4",
390
+ "setprototypeof": "~1.2.0",
391
+ "statuses": "~2.0.2",
392
+ "toidentifier": "~1.0.1"
393
+ },
394
+ "engines": {
395
+ "node": ">= 0.8"
396
+ },
397
+ "funding": {
398
+ "type": "opencollective",
399
+ "url": "https://opencollective.com/express"
400
+ }
401
+ },
402
+ "node_modules/iconv-lite": {
403
+ "version": "0.4.24",
404
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
405
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
406
+ "dependencies": {
407
+ "safer-buffer": ">= 2.1.2 < 3"
408
+ },
409
+ "engines": {
410
+ "node": ">=0.10.0"
411
+ }
412
+ },
413
+ "node_modules/inherits": {
414
+ "version": "2.0.4",
415
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
416
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
417
+ },
418
+ "node_modules/ipaddr.js": {
419
+ "version": "1.9.1",
420
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
421
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
422
+ "engines": {
423
+ "node": ">= 0.10"
424
+ }
425
+ },
426
+ "node_modules/math-intrinsics": {
427
+ "version": "1.1.0",
428
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
429
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
430
+ "engines": {
431
+ "node": ">= 0.4"
432
+ }
433
+ },
434
+ "node_modules/media-typer": {
435
+ "version": "0.3.0",
436
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
437
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
438
+ "engines": {
439
+ "node": ">= 0.6"
440
+ }
441
+ },
442
+ "node_modules/merge-descriptors": {
443
+ "version": "1.0.3",
444
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
445
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
446
+ "funding": {
447
+ "url": "https://github.com/sponsors/sindresorhus"
448
+ }
449
+ },
450
+ "node_modules/methods": {
451
+ "version": "1.1.2",
452
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
453
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
454
+ "engines": {
455
+ "node": ">= 0.6"
456
+ }
457
+ },
458
+ "node_modules/mime": {
459
+ "version": "1.6.0",
460
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
461
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
462
+ "bin": {
463
+ "mime": "cli.js"
464
+ },
465
+ "engines": {
466
+ "node": ">=4"
467
+ }
468
+ },
469
+ "node_modules/mime-db": {
470
+ "version": "1.52.0",
471
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
472
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
473
+ "engines": {
474
+ "node": ">= 0.6"
475
+ }
476
+ },
477
+ "node_modules/mime-types": {
478
+ "version": "2.1.35",
479
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
480
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
481
+ "dependencies": {
482
+ "mime-db": "1.52.0"
483
+ },
484
+ "engines": {
485
+ "node": ">= 0.6"
486
+ }
487
+ },
488
+ "node_modules/ms": {
489
+ "version": "2.0.0",
490
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
491
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
492
+ },
493
+ "node_modules/negotiator": {
494
+ "version": "0.6.3",
495
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
496
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
497
+ "engines": {
498
+ "node": ">= 0.6"
499
+ }
500
+ },
501
+ "node_modules/object-inspect": {
502
+ "version": "1.13.4",
503
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
504
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
505
+ "engines": {
506
+ "node": ">= 0.4"
507
+ },
508
+ "funding": {
509
+ "url": "https://github.com/sponsors/ljharb"
510
+ }
511
+ },
512
+ "node_modules/on-finished": {
513
+ "version": "2.4.1",
514
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
515
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
516
+ "dependencies": {
517
+ "ee-first": "1.1.1"
518
+ },
519
+ "engines": {
520
+ "node": ">= 0.8"
521
+ }
522
+ },
523
+ "node_modules/parseurl": {
524
+ "version": "1.3.3",
525
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
526
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
527
+ "engines": {
528
+ "node": ">= 0.8"
529
+ }
530
+ },
531
+ "node_modules/path-to-regexp": {
532
+ "version": "0.1.13",
533
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
534
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="
535
+ },
536
+ "node_modules/proxy-addr": {
537
+ "version": "2.0.7",
538
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
539
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
540
+ "dependencies": {
541
+ "forwarded": "0.2.0",
542
+ "ipaddr.js": "1.9.1"
543
+ },
544
+ "engines": {
545
+ "node": ">= 0.10"
546
+ }
547
+ },
548
+ "node_modules/qs": {
549
+ "version": "6.14.2",
550
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
551
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
552
+ "dependencies": {
553
+ "side-channel": "^1.1.0"
554
+ },
555
+ "engines": {
556
+ "node": ">=0.6"
557
+ },
558
+ "funding": {
559
+ "url": "https://github.com/sponsors/ljharb"
560
+ }
561
+ },
562
+ "node_modules/range-parser": {
563
+ "version": "1.2.1",
564
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
565
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
566
+ "engines": {
567
+ "node": ">= 0.6"
568
+ }
569
+ },
570
+ "node_modules/raw-body": {
571
+ "version": "2.5.3",
572
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
573
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
574
+ "dependencies": {
575
+ "bytes": "~3.1.2",
576
+ "http-errors": "~2.0.1",
577
+ "iconv-lite": "~0.4.24",
578
+ "unpipe": "~1.0.0"
579
+ },
580
+ "engines": {
581
+ "node": ">= 0.8"
582
+ }
583
+ },
584
+ "node_modules/safe-buffer": {
585
+ "version": "5.2.1",
586
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
587
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
588
+ "funding": [
589
+ {
590
+ "type": "github",
591
+ "url": "https://github.com/sponsors/feross"
592
+ },
593
+ {
594
+ "type": "patreon",
595
+ "url": "https://www.patreon.com/feross"
596
+ },
597
+ {
598
+ "type": "consulting",
599
+ "url": "https://feross.org/support"
600
+ }
601
+ ]
602
+ },
603
+ "node_modules/safer-buffer": {
604
+ "version": "2.1.2",
605
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
606
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
607
+ },
608
+ "node_modules/send": {
609
+ "version": "0.19.2",
610
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
611
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
612
+ "dependencies": {
613
+ "debug": "2.6.9",
614
+ "depd": "2.0.0",
615
+ "destroy": "1.2.0",
616
+ "encodeurl": "~2.0.0",
617
+ "escape-html": "~1.0.3",
618
+ "etag": "~1.8.1",
619
+ "fresh": "~0.5.2",
620
+ "http-errors": "~2.0.1",
621
+ "mime": "1.6.0",
622
+ "ms": "2.1.3",
623
+ "on-finished": "~2.4.1",
624
+ "range-parser": "~1.2.1",
625
+ "statuses": "~2.0.2"
626
+ },
627
+ "engines": {
628
+ "node": ">= 0.8.0"
629
+ }
630
+ },
631
+ "node_modules/send/node_modules/ms": {
632
+ "version": "2.1.3",
633
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
634
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
635
+ },
636
+ "node_modules/serve-static": {
637
+ "version": "1.16.3",
638
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
639
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
640
+ "dependencies": {
641
+ "encodeurl": "~2.0.0",
642
+ "escape-html": "~1.0.3",
643
+ "parseurl": "~1.3.3",
644
+ "send": "~0.19.1"
645
+ },
646
+ "engines": {
647
+ "node": ">= 0.8.0"
648
+ }
649
+ },
650
+ "node_modules/setprototypeof": {
651
+ "version": "1.2.0",
652
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
653
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
654
+ },
655
+ "node_modules/side-channel": {
656
+ "version": "1.1.0",
657
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
658
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
659
+ "dependencies": {
660
+ "es-errors": "^1.3.0",
661
+ "object-inspect": "^1.13.3",
662
+ "side-channel-list": "^1.0.0",
663
+ "side-channel-map": "^1.0.1",
664
+ "side-channel-weakmap": "^1.0.2"
665
+ },
666
+ "engines": {
667
+ "node": ">= 0.4"
668
+ },
669
+ "funding": {
670
+ "url": "https://github.com/sponsors/ljharb"
671
+ }
672
+ },
673
+ "node_modules/side-channel-list": {
674
+ "version": "1.0.1",
675
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
676
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
677
+ "dependencies": {
678
+ "es-errors": "^1.3.0",
679
+ "object-inspect": "^1.13.4"
680
+ },
681
+ "engines": {
682
+ "node": ">= 0.4"
683
+ },
684
+ "funding": {
685
+ "url": "https://github.com/sponsors/ljharb"
686
+ }
687
+ },
688
+ "node_modules/side-channel-map": {
689
+ "version": "1.0.1",
690
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
691
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
692
+ "dependencies": {
693
+ "call-bound": "^1.0.2",
694
+ "es-errors": "^1.3.0",
695
+ "get-intrinsic": "^1.2.5",
696
+ "object-inspect": "^1.13.3"
697
+ },
698
+ "engines": {
699
+ "node": ">= 0.4"
700
+ },
701
+ "funding": {
702
+ "url": "https://github.com/sponsors/ljharb"
703
+ }
704
+ },
705
+ "node_modules/side-channel-weakmap": {
706
+ "version": "1.0.2",
707
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
708
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
709
+ "dependencies": {
710
+ "call-bound": "^1.0.2",
711
+ "es-errors": "^1.3.0",
712
+ "get-intrinsic": "^1.2.5",
713
+ "object-inspect": "^1.13.3",
714
+ "side-channel-map": "^1.0.1"
715
+ },
716
+ "engines": {
717
+ "node": ">= 0.4"
718
+ },
719
+ "funding": {
720
+ "url": "https://github.com/sponsors/ljharb"
721
+ }
722
+ },
723
+ "node_modules/statuses": {
724
+ "version": "2.0.2",
725
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
726
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
727
+ "engines": {
728
+ "node": ">= 0.8"
729
+ }
730
+ },
731
+ "node_modules/toidentifier": {
732
+ "version": "1.0.1",
733
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
734
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
735
+ "engines": {
736
+ "node": ">=0.6"
737
+ }
738
+ },
739
+ "node_modules/type-is": {
740
+ "version": "1.6.18",
741
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
742
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
743
+ "dependencies": {
744
+ "media-typer": "0.3.0",
745
+ "mime-types": "~2.1.24"
746
+ },
747
+ "engines": {
748
+ "node": ">= 0.6"
749
+ }
750
+ },
751
+ "node_modules/unpipe": {
752
+ "version": "1.0.0",
753
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
754
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
755
+ "engines": {
756
+ "node": ">= 0.8"
757
+ }
758
+ },
759
+ "node_modules/utils-merge": {
760
+ "version": "1.0.1",
761
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
762
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
763
+ "engines": {
764
+ "node": ">= 0.4.0"
765
+ }
766
+ },
767
+ "node_modules/vary": {
768
+ "version": "1.1.2",
769
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
770
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
771
+ "engines": {
772
+ "node": ">= 0.8"
773
+ }
774
+ }
775
+ }
776
+ }
package.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "shellular-web",
3
+ "version": "1.0.0",
4
+ "description": "Web UI for Shellular — scan QR code to connect your VPS remotely",
5
+ "type": "module",
6
+ "main": "app.js",
7
+ "scripts": {
8
+ "start": "node app.js",
9
+ "dev": "node --watch app.js"
10
+ },
11
+ "dependencies": {
12
+ "express": "^4.19.2"
13
+ },
14
+ "engines": {
15
+ "node": ">=20"
16
+ }
17
+ }