nikcli-remote 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5ANLFHJV.js +1145 -0
- package/dist/chunk-GI5RMYH6.js +37 -0
- package/dist/index.cjs +4683 -6082
- package/dist/index.d.cts +98 -60
- package/dist/index.d.ts +98 -60
- package/dist/index.js +228 -7
- package/dist/{localtunnel-XT32JGNN.js → localtunnel-6DCQIYU6.js} +1 -1
- package/dist/server-VOW4RWJA.js +7 -0
- package/package.json +1 -1
- package/src/index.ts +57 -12
- package/src/server.ts +113 -108
- package/src/tunnel.ts +24 -0
- package/dist/chunk-3IFHAOGG.js +0 -2747
- package/dist/chunk-MCKGQKYU.js +0 -15
- package/dist/server-MBJQBTJF.js +0 -7
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__esm,
|
|
3
|
+
__export,
|
|
4
|
+
__toCommonJS
|
|
5
|
+
} from "./chunk-GI5RMYH6.js";
|
|
6
|
+
|
|
7
|
+
// src/web-client.ts
|
|
8
|
+
var web_client_exports = {};
|
|
9
|
+
__export(web_client_exports, {
|
|
10
|
+
getWebClient: () => getWebClient
|
|
11
|
+
});
|
|
12
|
+
function getWebClient() {
|
|
13
|
+
return `<!DOCTYPE html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="UTF-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
18
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
19
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
20
|
+
<meta name="theme-color" content="#0d1117">
|
|
21
|
+
<title>NikCLI Remote</title>
|
|
22
|
+
<style>
|
|
23
|
+
:root {
|
|
24
|
+
--bg: #0d1117;
|
|
25
|
+
--bg-secondary: #161b22;
|
|
26
|
+
--fg: #e6edf3;
|
|
27
|
+
--fg-muted: #8b949e;
|
|
28
|
+
--accent: #58a6ff;
|
|
29
|
+
--success: #3fb950;
|
|
30
|
+
--warning: #d29922;
|
|
31
|
+
--error: #f85149;
|
|
32
|
+
--border: #30363d;
|
|
33
|
+
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
* {
|
|
37
|
+
box-sizing: border-box;
|
|
38
|
+
margin: 0;
|
|
39
|
+
padding: 0;
|
|
40
|
+
-webkit-tap-highlight-color: transparent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
html, body {
|
|
44
|
+
height: 100%;
|
|
45
|
+
background: var(--bg);
|
|
46
|
+
color: var(--fg);
|
|
47
|
+
font-family: var(--font-mono);
|
|
48
|
+
font-size: 14px;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
touch-action: manipulation;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#app {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
height: 100%;
|
|
57
|
+
height: 100dvh;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Header */
|
|
61
|
+
#header {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
padding: 12px 16px;
|
|
66
|
+
background: var(--bg-secondary);
|
|
67
|
+
border-bottom: 1px solid var(--border);
|
|
68
|
+
flex-shrink: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#header h1 {
|
|
72
|
+
font-size: 16px;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
color: var(--accent);
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#header h1::before {
|
|
81
|
+
content: '';
|
|
82
|
+
width: 10px;
|
|
83
|
+
height: 10px;
|
|
84
|
+
background: var(--accent);
|
|
85
|
+
border-radius: 2px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#status {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 6px;
|
|
92
|
+
font-size: 12px;
|
|
93
|
+
color: var(--fg-muted);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#status-dot {
|
|
97
|
+
width: 8px;
|
|
98
|
+
height: 8px;
|
|
99
|
+
border-radius: 50%;
|
|
100
|
+
background: var(--error);
|
|
101
|
+
transition: background 0.3s;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#status-dot.connected {
|
|
105
|
+
background: var(--success);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#status-dot.connecting {
|
|
109
|
+
background: var(--warning);
|
|
110
|
+
animation: pulse 1s infinite;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@keyframes pulse {
|
|
114
|
+
0%, 100% { opacity: 1; }
|
|
115
|
+
50% { opacity: 0.5; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Terminal */
|
|
119
|
+
#terminal-container {
|
|
120
|
+
flex: 1;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
position: relative;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#terminal {
|
|
126
|
+
height: 100%;
|
|
127
|
+
padding: 12px;
|
|
128
|
+
overflow-y: auto;
|
|
129
|
+
overflow-x: hidden;
|
|
130
|
+
font-size: 13px;
|
|
131
|
+
line-height: 1.5;
|
|
132
|
+
white-space: pre-wrap;
|
|
133
|
+
word-break: break-all;
|
|
134
|
+
-webkit-overflow-scrolling: touch;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#terminal::-webkit-scrollbar {
|
|
138
|
+
width: 6px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#terminal::-webkit-scrollbar-track {
|
|
142
|
+
background: var(--bg);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#terminal::-webkit-scrollbar-thumb {
|
|
146
|
+
background: var(--border);
|
|
147
|
+
border-radius: 3px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.cursor {
|
|
151
|
+
display: inline-block;
|
|
152
|
+
width: 8px;
|
|
153
|
+
height: 16px;
|
|
154
|
+
background: var(--fg);
|
|
155
|
+
animation: blink 1s step-end infinite;
|
|
156
|
+
vertical-align: text-bottom;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@keyframes blink {
|
|
160
|
+
50% { opacity: 0; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Notifications */
|
|
164
|
+
#notifications {
|
|
165
|
+
position: fixed;
|
|
166
|
+
top: 60px;
|
|
167
|
+
left: 12px;
|
|
168
|
+
right: 12px;
|
|
169
|
+
z-index: 1000;
|
|
170
|
+
pointer-events: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.notification {
|
|
174
|
+
background: var(--bg-secondary);
|
|
175
|
+
border: 1px solid var(--border);
|
|
176
|
+
border-radius: 8px;
|
|
177
|
+
padding: 12px 16px;
|
|
178
|
+
margin-bottom: 8px;
|
|
179
|
+
animation: slideIn 0.3s ease;
|
|
180
|
+
pointer-events: auto;
|
|
181
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.notification.success { border-left: 3px solid var(--success); }
|
|
185
|
+
.notification.error { border-left: 3px solid var(--error); }
|
|
186
|
+
.notification.warning { border-left: 3px solid var(--warning); }
|
|
187
|
+
.notification.info { border-left: 3px solid var(--accent); }
|
|
188
|
+
|
|
189
|
+
.notification h4 {
|
|
190
|
+
font-size: 14px;
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
margin-bottom: 4px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.notification p {
|
|
196
|
+
font-size: 12px;
|
|
197
|
+
color: var(--fg-muted);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@keyframes slideIn {
|
|
201
|
+
from { transform: translateY(-20px); opacity: 0; }
|
|
202
|
+
to { transform: translateY(0); opacity: 1; }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Quick Keys */
|
|
206
|
+
#quickkeys {
|
|
207
|
+
display: grid;
|
|
208
|
+
grid-template-columns: repeat(6, 1fr);
|
|
209
|
+
gap: 6px;
|
|
210
|
+
padding: 8px 12px;
|
|
211
|
+
background: var(--bg-secondary);
|
|
212
|
+
border-top: 1px solid var(--border);
|
|
213
|
+
flex-shrink: 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.qkey {
|
|
217
|
+
background: var(--bg);
|
|
218
|
+
border: 1px solid var(--border);
|
|
219
|
+
border-radius: 6px;
|
|
220
|
+
padding: 10px 4px;
|
|
221
|
+
color: var(--fg);
|
|
222
|
+
font-size: 11px;
|
|
223
|
+
font-family: var(--font-mono);
|
|
224
|
+
text-align: center;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
user-select: none;
|
|
227
|
+
transition: background 0.1s, transform 0.1s;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.qkey:active {
|
|
231
|
+
background: var(--border);
|
|
232
|
+
transform: scale(0.95);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.qkey.wide {
|
|
236
|
+
grid-column: span 2;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.qkey.accent {
|
|
240
|
+
background: var(--accent);
|
|
241
|
+
border-color: var(--accent);
|
|
242
|
+
color: #fff;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* Input */
|
|
246
|
+
#input-container {
|
|
247
|
+
padding: 12px;
|
|
248
|
+
background: var(--bg-secondary);
|
|
249
|
+
border-top: 1px solid var(--border);
|
|
250
|
+
flex-shrink: 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#input-row {
|
|
254
|
+
display: flex;
|
|
255
|
+
gap: 8px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#input {
|
|
259
|
+
flex: 1;
|
|
260
|
+
background: var(--bg);
|
|
261
|
+
border: 1px solid var(--border);
|
|
262
|
+
border-radius: 8px;
|
|
263
|
+
padding: 12px 14px;
|
|
264
|
+
color: var(--fg);
|
|
265
|
+
font-family: var(--font-mono);
|
|
266
|
+
font-size: 16px;
|
|
267
|
+
outline: none;
|
|
268
|
+
transition: border-color 0.2s;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#input:focus {
|
|
272
|
+
border-color: var(--accent);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#input::placeholder {
|
|
276
|
+
color: var(--fg-muted);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#send {
|
|
280
|
+
background: var(--accent);
|
|
281
|
+
color: #fff;
|
|
282
|
+
border: none;
|
|
283
|
+
border-radius: 8px;
|
|
284
|
+
padding: 12px 20px;
|
|
285
|
+
font-size: 14px;
|
|
286
|
+
font-weight: 600;
|
|
287
|
+
font-family: var(--font-mono);
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
transition: opacity 0.2s, transform 0.1s;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#send:active {
|
|
293
|
+
opacity: 0.8;
|
|
294
|
+
transform: scale(0.98);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Auth Screen */
|
|
298
|
+
#auth-screen {
|
|
299
|
+
position: fixed;
|
|
300
|
+
inset: 0;
|
|
301
|
+
background: var(--bg);
|
|
302
|
+
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
304
|
+
align-items: center;
|
|
305
|
+
justify-content: center;
|
|
306
|
+
gap: 20px;
|
|
307
|
+
z-index: 2000;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#auth-screen.hidden {
|
|
311
|
+
display: none;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.spinner {
|
|
315
|
+
width: 40px;
|
|
316
|
+
height: 40px;
|
|
317
|
+
border: 3px solid var(--border);
|
|
318
|
+
border-top-color: var(--accent);
|
|
319
|
+
border-radius: 50%;
|
|
320
|
+
animation: spin 1s linear infinite;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@keyframes spin {
|
|
324
|
+
to { transform: rotate(360deg); }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#auth-screen p {
|
|
328
|
+
color: var(--fg-muted);
|
|
329
|
+
font-size: 14px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#auth-screen .error {
|
|
333
|
+
color: var(--error);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* Safe area padding for notched devices */
|
|
337
|
+
@supports (padding: env(safe-area-inset-bottom)) {
|
|
338
|
+
#input-container {
|
|
339
|
+
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Landscape adjustments */
|
|
344
|
+
@media (max-height: 500px) {
|
|
345
|
+
#quickkeys {
|
|
346
|
+
grid-template-columns: repeat(12, 1fr);
|
|
347
|
+
padding: 6px 8px;
|
|
348
|
+
}
|
|
349
|
+
.qkey {
|
|
350
|
+
padding: 8px 2px;
|
|
351
|
+
font-size: 10px;
|
|
352
|
+
}
|
|
353
|
+
#terminal {
|
|
354
|
+
font-size: 12px;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
</style>
|
|
358
|
+
</head>
|
|
359
|
+
<body>
|
|
360
|
+
<div id="app">
|
|
361
|
+
<div id="auth-screen">
|
|
362
|
+
<div class="spinner"></div>
|
|
363
|
+
<p id="auth-status">Connecting to NikCLI...</p>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<header id="header">
|
|
367
|
+
<h1>NikCLI Remote</h1>
|
|
368
|
+
<div id="status">
|
|
369
|
+
<span id="status-dot" class="connecting"></span>
|
|
370
|
+
<span id="status-text">Connecting</span>
|
|
371
|
+
</div>
|
|
372
|
+
</header>
|
|
373
|
+
|
|
374
|
+
<div id="terminal-container">
|
|
375
|
+
<div id="terminal"></div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<div id="notifications"></div>
|
|
379
|
+
|
|
380
|
+
<div id="quickkeys">
|
|
381
|
+
<button class="qkey" data-key="\\t">Tab</button>
|
|
382
|
+
<button class="qkey" data-key="\\x1b[A">\u2191</button>
|
|
383
|
+
<button class="qkey" data-key="\\x1b[B">\u2193</button>
|
|
384
|
+
<button class="qkey" data-key="\\x1b[D">\u2190</button>
|
|
385
|
+
<button class="qkey" data-key="\\x1b[C">\u2192</button>
|
|
386
|
+
<button class="qkey" data-key="\\x1b">Esc</button>
|
|
387
|
+
<button class="qkey" data-key="\\x03">^C</button>
|
|
388
|
+
<button class="qkey" data-key="\\x04">^D</button>
|
|
389
|
+
<button class="qkey" data-key="\\x1a">^Z</button>
|
|
390
|
+
<button class="qkey" data-key="\\x0c">^L</button>
|
|
391
|
+
<button class="qkey wide accent" data-key="\\r">Enter \u23CE</button>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<div id="input-container">
|
|
395
|
+
<div id="input-row">
|
|
396
|
+
<input type="text" id="input" placeholder="Type command..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
|
397
|
+
<button id="send">Send</button>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<script>
|
|
403
|
+
(function() {
|
|
404
|
+
'use strict';
|
|
405
|
+
|
|
406
|
+
// Parse URL params
|
|
407
|
+
const params = new URLSearchParams(location.search);
|
|
408
|
+
const token = params.get('t');
|
|
409
|
+
const sessionId = params.get('s');
|
|
410
|
+
|
|
411
|
+
// DOM elements
|
|
412
|
+
const terminal = document.getElementById('terminal');
|
|
413
|
+
const input = document.getElementById('input');
|
|
414
|
+
const sendBtn = document.getElementById('send');
|
|
415
|
+
const statusDot = document.getElementById('status-dot');
|
|
416
|
+
const statusText = document.getElementById('status-text');
|
|
417
|
+
const authScreen = document.getElementById('auth-screen');
|
|
418
|
+
const authStatus = document.getElementById('auth-status');
|
|
419
|
+
const notifications = document.getElementById('notifications');
|
|
420
|
+
|
|
421
|
+
// State
|
|
422
|
+
let ws = null;
|
|
423
|
+
let reconnectAttempts = 0;
|
|
424
|
+
const maxReconnectAttempts = 5;
|
|
425
|
+
let terminalEnabled = true;
|
|
426
|
+
|
|
427
|
+
// Connect to WebSocket
|
|
428
|
+
function connect() {
|
|
429
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
430
|
+
ws = new WebSocket(protocol + '//' + location.host);
|
|
431
|
+
|
|
432
|
+
ws.onopen = function() {
|
|
433
|
+
setStatus('connecting', 'Authenticating...');
|
|
434
|
+
ws.send(JSON.stringify({ type: 'auth', token: token }));
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
ws.onmessage = function(event) {
|
|
438
|
+
try {
|
|
439
|
+
const msg = JSON.parse(event.data);
|
|
440
|
+
handleMessage(msg);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error('Parse error:', e);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
ws.onclose = function() {
|
|
447
|
+
setStatus('disconnected', 'Disconnected');
|
|
448
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
449
|
+
reconnectAttempts++;
|
|
450
|
+
const delay = Math.min(2000 * reconnectAttempts, 10000);
|
|
451
|
+
setTimeout(connect, delay);
|
|
452
|
+
} else {
|
|
453
|
+
authStatus.textContent = 'Connection failed. Refresh to retry.';
|
|
454
|
+
authStatus.classList.add('error');
|
|
455
|
+
authScreen.classList.remove('hidden');
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
ws.onerror = function() {
|
|
460
|
+
console.error('WebSocket error');
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Handle incoming message
|
|
465
|
+
function handleMessage(msg) {
|
|
466
|
+
switch (msg.type) {
|
|
467
|
+
case 'auth:required':
|
|
468
|
+
// Already sent auth on open
|
|
469
|
+
break;
|
|
470
|
+
|
|
471
|
+
case 'auth:success':
|
|
472
|
+
authScreen.classList.add('hidden');
|
|
473
|
+
setStatus('connected', 'Connected');
|
|
474
|
+
reconnectAttempts = 0;
|
|
475
|
+
terminalEnabled = msg.payload?.terminalEnabled !== false;
|
|
476
|
+
if (terminalEnabled) {
|
|
477
|
+
appendOutput('\\x1b[32mConnected to NikCLI\\x1b[0m\\n\\n');
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
|
|
481
|
+
case 'auth:failed':
|
|
482
|
+
authStatus.textContent = 'Authentication failed';
|
|
483
|
+
authStatus.classList.add('error');
|
|
484
|
+
break;
|
|
485
|
+
|
|
486
|
+
case 'terminal:output':
|
|
487
|
+
if (msg.payload?.data) {
|
|
488
|
+
appendOutput(msg.payload.data);
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case 'terminal:exit':
|
|
493
|
+
appendOutput('\\n\\x1b[33m[Process exited with code ' + (msg.payload?.code || 0) + ']\\x1b[0m\\n');
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
case 'notification':
|
|
497
|
+
showNotification(msg.payload);
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
case 'session:end':
|
|
501
|
+
appendOutput('\\n\\x1b[31m[Session ended]\\x1b[0m\\n');
|
|
502
|
+
setStatus('disconnected', 'Session ended');
|
|
503
|
+
break;
|
|
504
|
+
|
|
505
|
+
case 'pong':
|
|
506
|
+
// Heartbeat response
|
|
507
|
+
break;
|
|
508
|
+
|
|
509
|
+
default:
|
|
510
|
+
console.log('Unknown message:', msg.type);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Append text to terminal with ANSI support
|
|
515
|
+
function appendOutput(text) {
|
|
516
|
+
// Simple ANSI to HTML conversion
|
|
517
|
+
const html = ansiToHtml(text);
|
|
518
|
+
terminal.innerHTML += html;
|
|
519
|
+
terminal.scrollTop = terminal.scrollHeight;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Basic ANSI to HTML
|
|
523
|
+
function ansiToHtml(text) {
|
|
524
|
+
const ansiColors = {
|
|
525
|
+
'30': '#6e7681', '31': '#f85149', '32': '#3fb950', '33': '#d29922',
|
|
526
|
+
'34': '#58a6ff', '35': '#bc8cff', '36': '#76e3ea', '37': '#e6edf3',
|
|
527
|
+
'90': '#6e7681', '91': '#f85149', '92': '#3fb950', '93': '#d29922',
|
|
528
|
+
'94': '#58a6ff', '95': '#bc8cff', '96': '#76e3ea', '97': '#ffffff'
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
let result = '';
|
|
532
|
+
let currentStyle = '';
|
|
533
|
+
|
|
534
|
+
const parts = text.split(/\\x1b\\[([0-9;]+)m/);
|
|
535
|
+
for (let i = 0; i < parts.length; i++) {
|
|
536
|
+
if (i % 2 === 0) {
|
|
537
|
+
// Text content
|
|
538
|
+
result += escapeHtml(parts[i]);
|
|
539
|
+
} else {
|
|
540
|
+
// ANSI code
|
|
541
|
+
const codes = parts[i].split(';');
|
|
542
|
+
for (const code of codes) {
|
|
543
|
+
if (code === '0') {
|
|
544
|
+
if (currentStyle) {
|
|
545
|
+
result += '</span>';
|
|
546
|
+
currentStyle = '';
|
|
547
|
+
}
|
|
548
|
+
} else if (code === '1') {
|
|
549
|
+
currentStyle = 'font-weight:bold;';
|
|
550
|
+
result += '<span style="' + currentStyle + '">';
|
|
551
|
+
} else if (ansiColors[code]) {
|
|
552
|
+
if (currentStyle) result += '</span>';
|
|
553
|
+
currentStyle = 'color:' + ansiColors[code] + ';';
|
|
554
|
+
result += '<span style="' + currentStyle + '">';
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (currentStyle) result += '</span>';
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Escape HTML
|
|
565
|
+
function escapeHtml(text) {
|
|
566
|
+
return text
|
|
567
|
+
.replace(/&/g, '&')
|
|
568
|
+
.replace(/</g, '<')
|
|
569
|
+
.replace(/>/g, '>')
|
|
570
|
+
.replace(/"/g, '"')
|
|
571
|
+
.replace(/\\n/g, '<br>')
|
|
572
|
+
.replace(/ /g, ' ');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Set connection status
|
|
576
|
+
function setStatus(state, text) {
|
|
577
|
+
statusDot.className = state === 'connected' ? 'connected' :
|
|
578
|
+
state === 'connecting' ? 'connecting' : '';
|
|
579
|
+
statusText.textContent = text;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Send data to terminal
|
|
583
|
+
function send(data) {
|
|
584
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
585
|
+
ws.send(JSON.stringify({ type: 'terminal:input', data: data }));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Show notification
|
|
590
|
+
function showNotification(n) {
|
|
591
|
+
if (!n) return;
|
|
592
|
+
const el = document.createElement('div');
|
|
593
|
+
el.className = 'notification ' + (n.type || 'info');
|
|
594
|
+
el.innerHTML = '<h4>' + escapeHtml(n.title || 'Notification') + '</h4>' +
|
|
595
|
+
'<p>' + escapeHtml(n.body || '') + '</p>';
|
|
596
|
+
notifications.appendChild(el);
|
|
597
|
+
setTimeout(function() { el.remove(); }, 5000);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Event: Send button
|
|
601
|
+
sendBtn.onclick = function() {
|
|
602
|
+
if (input.value) {
|
|
603
|
+
send(input.value + '\\r');
|
|
604
|
+
input.value = '';
|
|
605
|
+
}
|
|
606
|
+
input.focus();
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// Event: Enter key in input
|
|
610
|
+
input.onkeydown = function(e) {
|
|
611
|
+
if (e.key === 'Enter') {
|
|
612
|
+
e.preventDefault();
|
|
613
|
+
sendBtn.click();
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// Event: Quick keys
|
|
618
|
+
document.querySelectorAll('.qkey').forEach(function(btn) {
|
|
619
|
+
btn.onclick = function() {
|
|
620
|
+
const key = btn.dataset.key;
|
|
621
|
+
const decoded = key
|
|
622
|
+
.replace(/\\\\x([0-9a-f]{2})/gi, function(_, hex) {
|
|
623
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
624
|
+
})
|
|
625
|
+
.replace(/\\\\t/g, '\\t')
|
|
626
|
+
.replace(/\\\\r/g, '\\r')
|
|
627
|
+
.replace(/\\\\n/g, '\\n');
|
|
628
|
+
send(decoded);
|
|
629
|
+
input.focus();
|
|
630
|
+
};
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Heartbeat
|
|
634
|
+
setInterval(function() {
|
|
635
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
636
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
637
|
+
}
|
|
638
|
+
}, 25000);
|
|
639
|
+
|
|
640
|
+
// Handle resize
|
|
641
|
+
function sendResize() {
|
|
642
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
643
|
+
const cols = Math.floor(terminal.clientWidth / 8);
|
|
644
|
+
const rows = Math.floor(terminal.clientHeight / 18);
|
|
645
|
+
ws.send(JSON.stringify({ type: 'terminal:resize', cols: cols, rows: rows }));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
window.addEventListener('resize', sendResize);
|
|
650
|
+
setTimeout(sendResize, 1000);
|
|
651
|
+
|
|
652
|
+
// Start connection
|
|
653
|
+
if (token) {
|
|
654
|
+
connect();
|
|
655
|
+
} else {
|
|
656
|
+
authStatus.textContent = 'Invalid session URL';
|
|
657
|
+
authStatus.classList.add('error');
|
|
658
|
+
}
|
|
659
|
+
})();
|
|
660
|
+
</script>
|
|
661
|
+
</body>
|
|
662
|
+
</html>`;
|
|
663
|
+
}
|
|
664
|
+
var init_web_client = __esm({
|
|
665
|
+
"src/web-client.ts"() {
|
|
666
|
+
"use strict";
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// src/server.ts
|
|
671
|
+
import { EventEmitter } from "events";
|
|
672
|
+
import { createServer } from "http";
|
|
673
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
674
|
+
import crypto from "crypto";
|
|
675
|
+
import os from "os";
|
|
676
|
+
|
|
677
|
+
// src/types.ts
|
|
678
|
+
var DEFAULT_CONFIG = {
|
|
679
|
+
port: 0,
|
|
680
|
+
host: "0.0.0.0",
|
|
681
|
+
enableTunnel: true,
|
|
682
|
+
tunnelProvider: "localtunnel",
|
|
683
|
+
maxConnections: 5,
|
|
684
|
+
heartbeatInterval: 3e4,
|
|
685
|
+
shell: "/bin/bash",
|
|
686
|
+
cols: 80,
|
|
687
|
+
rows: 24,
|
|
688
|
+
enableTerminal: true,
|
|
689
|
+
sessionTimeout: 0
|
|
690
|
+
};
|
|
691
|
+
var MessageTypes = {
|
|
692
|
+
// Auth
|
|
693
|
+
AUTH_REQUIRED: "auth:required",
|
|
694
|
+
AUTH: "auth",
|
|
695
|
+
AUTH_SUCCESS: "auth:success",
|
|
696
|
+
AUTH_FAILED: "auth:failed",
|
|
697
|
+
// Terminal
|
|
698
|
+
TERMINAL_OUTPUT: "terminal:output",
|
|
699
|
+
TERMINAL_INPUT: "terminal:input",
|
|
700
|
+
TERMINAL_RESIZE: "terminal:resize",
|
|
701
|
+
TERMINAL_EXIT: "terminal:exit",
|
|
702
|
+
TERMINAL_CLEAR: "terminal:clear",
|
|
703
|
+
// Notifications
|
|
704
|
+
NOTIFICATION: "notification",
|
|
705
|
+
// Heartbeat
|
|
706
|
+
PING: "ping",
|
|
707
|
+
PONG: "pong",
|
|
708
|
+
// Session
|
|
709
|
+
SESSION_INFO: "session:info",
|
|
710
|
+
SESSION_END: "session:end",
|
|
711
|
+
// Commands (NikCLI specific)
|
|
712
|
+
COMMAND: "command",
|
|
713
|
+
COMMAND_RESULT: "command:result",
|
|
714
|
+
// Agent events
|
|
715
|
+
AGENT_START: "agent:start",
|
|
716
|
+
AGENT_PROGRESS: "agent:progress",
|
|
717
|
+
AGENT_COMPLETE: "agent:complete",
|
|
718
|
+
AGENT_ERROR: "agent:error"
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// src/server.ts
|
|
722
|
+
var RemoteServer = class extends EventEmitter {
|
|
723
|
+
config;
|
|
724
|
+
httpServer = null;
|
|
725
|
+
wss = null;
|
|
726
|
+
clients = /* @__PURE__ */ new Map();
|
|
727
|
+
session = null;
|
|
728
|
+
heartbeatTimer = null;
|
|
729
|
+
sessionTimeoutTimer = null;
|
|
730
|
+
isRunning = false;
|
|
731
|
+
sessionSecret;
|
|
732
|
+
// stdin/stdout proxy state
|
|
733
|
+
originalStdoutWrite = null;
|
|
734
|
+
originalStdinOn = null;
|
|
735
|
+
constructor(config = {}) {
|
|
736
|
+
super();
|
|
737
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
738
|
+
this.sessionSecret = config.sessionSecret || this.generateSecret();
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Generate a random secret
|
|
742
|
+
*/
|
|
743
|
+
generateSecret() {
|
|
744
|
+
return crypto.randomBytes(16).toString("hex");
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Start the remote server - creates WebSocket server that proxies stdin/stdout
|
|
748
|
+
*/
|
|
749
|
+
async start(options = {}) {
|
|
750
|
+
if (this.isRunning) {
|
|
751
|
+
throw new Error("Server already running");
|
|
752
|
+
}
|
|
753
|
+
const sessionId = this.generateSessionId();
|
|
754
|
+
this.httpServer = createServer((req, res) => this.handleHttpRequest(req, res));
|
|
755
|
+
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
756
|
+
this.setupWebSocketHandlers();
|
|
757
|
+
const port = await new Promise((resolve, reject) => {
|
|
758
|
+
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
759
|
+
const addr = this.httpServer.address();
|
|
760
|
+
resolve(typeof addr === "object" ? addr?.port || 0 : 0);
|
|
761
|
+
});
|
|
762
|
+
this.httpServer.on("error", reject);
|
|
763
|
+
});
|
|
764
|
+
const localIp = this.getLocalIP();
|
|
765
|
+
const localUrl = `http://${localIp}:${port}`;
|
|
766
|
+
this.session = {
|
|
767
|
+
id: sessionId,
|
|
768
|
+
name: options.name || `nikcli-${sessionId}`,
|
|
769
|
+
qrCode: "",
|
|
770
|
+
qrUrl: `${localUrl}?s=${sessionId}&t=${this.sessionSecret}`,
|
|
771
|
+
localUrl,
|
|
772
|
+
status: "waiting",
|
|
773
|
+
connectedDevices: [],
|
|
774
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
775
|
+
lastActivity: /* @__PURE__ */ new Date(),
|
|
776
|
+
port
|
|
777
|
+
};
|
|
778
|
+
if (options.processForStreaming) {
|
|
779
|
+
this.setupStdioProxy(options.processForStreaming.stdout, options.processForStreaming.stdin);
|
|
780
|
+
}
|
|
781
|
+
this.startHeartbeat();
|
|
782
|
+
if (this.config.sessionTimeout > 0) {
|
|
783
|
+
this.startSessionTimeout();
|
|
784
|
+
}
|
|
785
|
+
this.session.status = "waiting";
|
|
786
|
+
this.isRunning = true;
|
|
787
|
+
this.emit("started", this.session);
|
|
788
|
+
return this.session;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Setup stdin/stdout proxy to forward to WebSocket clients
|
|
792
|
+
*/
|
|
793
|
+
setupStdioProxy(stdout, stdin) {
|
|
794
|
+
const originalWrite = stdout.write.bind(stdout);
|
|
795
|
+
stdout.write = ((data, encoding, cb) => {
|
|
796
|
+
const result = originalWrite(data, encoding, cb);
|
|
797
|
+
const text = data instanceof Buffer ? data.toString() : data;
|
|
798
|
+
const cleaned = this.cleanOutputForMobile(text);
|
|
799
|
+
this.broadcastToAll({ type: MessageTypes.TERMINAL_OUTPUT, payload: { data: cleaned } });
|
|
800
|
+
return result;
|
|
801
|
+
});
|
|
802
|
+
this.wss?.on("connection", (ws) => {
|
|
803
|
+
ws.on("message", (rawData) => {
|
|
804
|
+
try {
|
|
805
|
+
const msg = JSON.parse(rawData.toString());
|
|
806
|
+
if (msg.type === MessageTypes.TERMINAL_INPUT && msg.data) {
|
|
807
|
+
const inputData = Buffer.from(msg.data);
|
|
808
|
+
stdin.emit("data", inputData);
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Clean output for mobile display - remove ANSI codes and TUI artifacts
|
|
817
|
+
*/
|
|
818
|
+
cleanOutputForMobile(text) {
|
|
819
|
+
let cleaned = text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\r/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
820
|
+
return cleaned;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Stop the server
|
|
824
|
+
*/
|
|
825
|
+
async stop() {
|
|
826
|
+
if (!this.isRunning) return;
|
|
827
|
+
this.isRunning = false;
|
|
828
|
+
if (this.originalStdoutWrite && this.session) {
|
|
829
|
+
}
|
|
830
|
+
if (this.heartbeatTimer) {
|
|
831
|
+
clearInterval(this.heartbeatTimer);
|
|
832
|
+
this.heartbeatTimer = null;
|
|
833
|
+
}
|
|
834
|
+
if (this.sessionTimeoutTimer) {
|
|
835
|
+
clearTimeout(this.sessionTimeoutTimer);
|
|
836
|
+
this.sessionTimeoutTimer = null;
|
|
837
|
+
}
|
|
838
|
+
this.broadcastToAll({ type: MessageTypes.SESSION_END, payload: {} });
|
|
839
|
+
for (const client of this.clients.values()) {
|
|
840
|
+
client.ws.close(1e3, "Server shutting down");
|
|
841
|
+
}
|
|
842
|
+
this.clients.clear();
|
|
843
|
+
if (this.wss) {
|
|
844
|
+
this.wss.close();
|
|
845
|
+
this.wss = null;
|
|
846
|
+
}
|
|
847
|
+
if (this.httpServer) {
|
|
848
|
+
await new Promise((resolve) => {
|
|
849
|
+
this.httpServer.close(() => resolve());
|
|
850
|
+
});
|
|
851
|
+
this.httpServer = null;
|
|
852
|
+
}
|
|
853
|
+
if (this.session) {
|
|
854
|
+
this.session.status = "stopped";
|
|
855
|
+
}
|
|
856
|
+
this.emit("stopped");
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Broadcast message to all authenticated clients
|
|
860
|
+
*/
|
|
861
|
+
broadcastToAll(message) {
|
|
862
|
+
const data = JSON.stringify({
|
|
863
|
+
type: message.type,
|
|
864
|
+
payload: message.payload,
|
|
865
|
+
timestamp: message.timestamp || Date.now()
|
|
866
|
+
});
|
|
867
|
+
for (const client of this.clients.values()) {
|
|
868
|
+
if (client.authenticated && client.ws.readyState === WebSocket.OPEN) {
|
|
869
|
+
client.ws.send(data);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Public broadcast method for compatibility
|
|
875
|
+
*/
|
|
876
|
+
broadcast(message) {
|
|
877
|
+
this.broadcastToAll(message);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Send notification to clients
|
|
881
|
+
*/
|
|
882
|
+
notify(notification) {
|
|
883
|
+
this.broadcastToAll({
|
|
884
|
+
type: MessageTypes.NOTIFICATION,
|
|
885
|
+
payload: notification
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Get current session
|
|
890
|
+
*/
|
|
891
|
+
getSession() {
|
|
892
|
+
return this.session;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Check if server is running
|
|
896
|
+
*/
|
|
897
|
+
isActive() {
|
|
898
|
+
return this.isRunning && this.session?.status !== "stopped";
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Get connected client count
|
|
902
|
+
*/
|
|
903
|
+
getConnectedCount() {
|
|
904
|
+
let count = 0;
|
|
905
|
+
for (const client of this.clients.values()) {
|
|
906
|
+
if (client.authenticated) count++;
|
|
907
|
+
}
|
|
908
|
+
return count;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Write data to all connected clients (for manual output streaming)
|
|
912
|
+
*/
|
|
913
|
+
writeToClients(data) {
|
|
914
|
+
this.broadcastToAll({ type: MessageTypes.TERMINAL_OUTPUT, payload: { data } });
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Alias for writeToClients - for compatibility
|
|
918
|
+
*/
|
|
919
|
+
writeToTerminal(data) {
|
|
920
|
+
this.writeToClients(data);
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Resize terminal (for compatibility - not used in direct streaming mode)
|
|
924
|
+
*/
|
|
925
|
+
resizeTerminal(cols, rows) {
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Setup WebSocket handlers
|
|
929
|
+
*/
|
|
930
|
+
setupWebSocketHandlers() {
|
|
931
|
+
this.wss.on("connection", (ws, req) => {
|
|
932
|
+
const clientId = this.generateClientId();
|
|
933
|
+
const client = {
|
|
934
|
+
id: clientId,
|
|
935
|
+
ws,
|
|
936
|
+
authenticated: false,
|
|
937
|
+
device: {
|
|
938
|
+
id: clientId,
|
|
939
|
+
userAgent: req.headers["user-agent"],
|
|
940
|
+
ip: req.socket.remoteAddress,
|
|
941
|
+
connectedAt: /* @__PURE__ */ new Date(),
|
|
942
|
+
lastActivity: /* @__PURE__ */ new Date()
|
|
943
|
+
},
|
|
944
|
+
lastPing: Date.now()
|
|
945
|
+
};
|
|
946
|
+
if (this.clients.size >= this.config.maxConnections) {
|
|
947
|
+
ws.close(1013, "Max connections reached");
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
this.clients.set(clientId, client);
|
|
951
|
+
ws.send(JSON.stringify({ type: MessageTypes.AUTH_REQUIRED, timestamp: Date.now() }));
|
|
952
|
+
ws.on("message", (data) => {
|
|
953
|
+
try {
|
|
954
|
+
const message = JSON.parse(data.toString());
|
|
955
|
+
this.handleClientMessage(client, message);
|
|
956
|
+
} catch {
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
ws.on("close", () => {
|
|
960
|
+
this.clients.delete(clientId);
|
|
961
|
+
if (this.session && client.authenticated) {
|
|
962
|
+
this.session.connectedDevices = this.session.connectedDevices.filter(
|
|
963
|
+
(d) => d.id !== clientId
|
|
964
|
+
);
|
|
965
|
+
if (this.session.connectedDevices.length === 0) {
|
|
966
|
+
this.session.status = "waiting";
|
|
967
|
+
}
|
|
968
|
+
this.emit("client:disconnected", client.device);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
ws.on("error", (error) => {
|
|
972
|
+
this.emit("client:error", clientId, error);
|
|
973
|
+
});
|
|
974
|
+
ws.on("pong", () => {
|
|
975
|
+
client.lastPing = Date.now();
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Handle client message
|
|
981
|
+
*/
|
|
982
|
+
handleClientMessage(client, message) {
|
|
983
|
+
client.device.lastActivity = /* @__PURE__ */ new Date();
|
|
984
|
+
if (this.session) {
|
|
985
|
+
this.session.lastActivity = /* @__PURE__ */ new Date();
|
|
986
|
+
}
|
|
987
|
+
if (this.config.sessionTimeout > 0) {
|
|
988
|
+
this.resetSessionTimeout();
|
|
989
|
+
}
|
|
990
|
+
switch (message.type) {
|
|
991
|
+
case MessageTypes.AUTH:
|
|
992
|
+
this.handleAuth(client, message.token);
|
|
993
|
+
break;
|
|
994
|
+
case MessageTypes.TERMINAL_INPUT:
|
|
995
|
+
break;
|
|
996
|
+
case MessageTypes.TERMINAL_RESIZE:
|
|
997
|
+
this.emit("message", client, message);
|
|
998
|
+
break;
|
|
999
|
+
case MessageTypes.PING:
|
|
1000
|
+
client.ws.send(JSON.stringify({ type: MessageTypes.PONG, timestamp: Date.now() }));
|
|
1001
|
+
break;
|
|
1002
|
+
default:
|
|
1003
|
+
this.emit("message", client, message);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Handle authentication
|
|
1008
|
+
*/
|
|
1009
|
+
handleAuth(client, token) {
|
|
1010
|
+
if (token === this.sessionSecret) {
|
|
1011
|
+
client.authenticated = true;
|
|
1012
|
+
if (this.session) {
|
|
1013
|
+
this.session.connectedDevices.push(client.device);
|
|
1014
|
+
this.session.status = "connected";
|
|
1015
|
+
}
|
|
1016
|
+
client.ws.send(
|
|
1017
|
+
JSON.stringify({
|
|
1018
|
+
type: MessageTypes.AUTH_SUCCESS,
|
|
1019
|
+
payload: {
|
|
1020
|
+
sessionId: this.session?.id
|
|
1021
|
+
},
|
|
1022
|
+
timestamp: Date.now()
|
|
1023
|
+
})
|
|
1024
|
+
);
|
|
1025
|
+
this.emit("client:connected", client.device);
|
|
1026
|
+
} else {
|
|
1027
|
+
client.ws.send(JSON.stringify({ type: MessageTypes.AUTH_FAILED, timestamp: Date.now() }));
|
|
1028
|
+
setTimeout(() => client.ws.close(1008, "Authentication failed"), 100);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Handle HTTP request
|
|
1033
|
+
*/
|
|
1034
|
+
handleHttpRequest(req, res) {
|
|
1035
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
1036
|
+
const path = url.pathname;
|
|
1037
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1038
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
1039
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1040
|
+
if (req.method === "OPTIONS") {
|
|
1041
|
+
res.writeHead(204);
|
|
1042
|
+
res.end();
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (path === "/" || path === "/index.html") {
|
|
1046
|
+
const { getWebClient: getWebClient2 } = (init_web_client(), __toCommonJS(web_client_exports));
|
|
1047
|
+
res.writeHead(200, {
|
|
1048
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1049
|
+
"Content-Security-Policy": "default-src 'self' 'unsafe-inline' 'unsafe-eval' https: data: ws: wss:"
|
|
1050
|
+
});
|
|
1051
|
+
res.end(getWebClient2());
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (path === "/health") {
|
|
1055
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1056
|
+
res.end(JSON.stringify({ status: "ok", session: this.session?.id }));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (path === "/api/session") {
|
|
1060
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1061
|
+
res.end(
|
|
1062
|
+
JSON.stringify({
|
|
1063
|
+
id: this.session?.id,
|
|
1064
|
+
name: this.session?.name,
|
|
1065
|
+
status: this.session?.status,
|
|
1066
|
+
connectedDevices: this.session?.connectedDevices.length
|
|
1067
|
+
})
|
|
1068
|
+
);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1072
|
+
res.end("Not Found");
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Start heartbeat
|
|
1076
|
+
*/
|
|
1077
|
+
startHeartbeat() {
|
|
1078
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1079
|
+
const now = Date.now();
|
|
1080
|
+
for (const [id, client] of this.clients) {
|
|
1081
|
+
if (now - client.lastPing > this.config.heartbeatInterval * 2) {
|
|
1082
|
+
client.ws.terminate();
|
|
1083
|
+
this.clients.delete(id);
|
|
1084
|
+
} else if (client.ws.readyState === WebSocket.OPEN) {
|
|
1085
|
+
client.ws.ping();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}, this.config.heartbeatInterval);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Start session timeout
|
|
1092
|
+
*/
|
|
1093
|
+
startSessionTimeout() {
|
|
1094
|
+
this.sessionTimeoutTimer = setTimeout(() => {
|
|
1095
|
+
if (this.session?.connectedDevices.length === 0) {
|
|
1096
|
+
this.stop();
|
|
1097
|
+
}
|
|
1098
|
+
}, this.config.sessionTimeout);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Reset session timeout
|
|
1102
|
+
*/
|
|
1103
|
+
resetSessionTimeout() {
|
|
1104
|
+
if (this.sessionTimeoutTimer) {
|
|
1105
|
+
clearTimeout(this.sessionTimeoutTimer);
|
|
1106
|
+
}
|
|
1107
|
+
if (this.config.sessionTimeout > 0) {
|
|
1108
|
+
this.startSessionTimeout();
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Get local IP
|
|
1113
|
+
*/
|
|
1114
|
+
getLocalIP() {
|
|
1115
|
+
const interfaces = os.networkInterfaces();
|
|
1116
|
+
for (const name of Object.keys(interfaces)) {
|
|
1117
|
+
for (const iface of interfaces[name] || []) {
|
|
1118
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
1119
|
+
return iface.address;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return "127.0.0.1";
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Generate session ID
|
|
1127
|
+
*/
|
|
1128
|
+
generateSessionId() {
|
|
1129
|
+
return crypto.randomBytes(4).toString("hex");
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Generate client ID
|
|
1133
|
+
*/
|
|
1134
|
+
generateClientId() {
|
|
1135
|
+
return "c_" + crypto.randomBytes(4).toString("hex");
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
export {
|
|
1140
|
+
DEFAULT_CONFIG,
|
|
1141
|
+
MessageTypes,
|
|
1142
|
+
getWebClient,
|
|
1143
|
+
init_web_client,
|
|
1144
|
+
RemoteServer
|
|
1145
|
+
};
|