@livedesk/client 0.1.234 → 0.1.236
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/bin/livedesk-client-node.js +82 -190
- package/bin/livedesk-client.js +111 -482
- package/package.json +6 -6
- package/src/runtime/client-runtime-server.js +1826 -1906
- package/src/runtime/fast-runtime-repair.js +0 -300
- package/src/security/device-credential-store.js +0 -219
- package/src/security/secure-direct-client.js +0 -224
|
@@ -1,1046 +1,986 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
|
-
import { accessSync, chmodSync, constants as fsConstants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statfsSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { delimiter, dirname, join, parse, posix, resolve } from 'node:path';
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { accessSync, chmodSync, constants as fsConstants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statfsSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { delimiter, dirname, join, parse, posix, resolve } from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
|
-
import { createRuntimeManager, normalizeRuntimeAuthSession, runtimeRoleError, RuntimeState } from '../../../runtime-core/src/index.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'/api/
|
|
39
|
-
'/api/
|
|
40
|
-
'/api/
|
|
41
|
-
'/api/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
||
|
|
94
|
-
||
|
|
95
|
-
||
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
||
|
|
101
|
-
||
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
let
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
let
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
?
|
|
253
|
-
:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
*
|
|
380
|
-
* -
|
|
381
|
-
*
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
let
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
if (record.
|
|
438
|
-
record.
|
|
439
|
-
record.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
record.
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
record.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
record.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
destroyChildRedirectedIo(record.child);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
record.outputDrainTimer
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
record.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return { receivedBytes: 0, sentBytes: 0 };
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (platform === '
|
|
689
|
-
const output = await commandOwner.capture('netstat
|
|
690
|
-
return String(output || '').trim()
|
|
691
|
-
? { ok: true, value:
|
|
692
|
-
: { ok: false, value: { receivedBytes: 0, sentBytes: 0 }, error: 'netstat-unavailable' };
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
if (platform === '
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
:
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
['
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
return origins;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
function readSavedSession() {
|
|
956
|
-
if (process.env.LIVEDESK_DESKTOP_HOST === '1') return null;
|
|
957
|
-
try {
|
|
7
|
+
import { createRuntimeManager, normalizeRuntimeAuthSession, runtimeRoleError, RuntimeState } from '../../../runtime-core/src/index.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
10
|
+
const DEFAULT_PORT = 5179;
|
|
11
|
+
const CLIENT_RUNTIME_BIND_TIMEOUT_MS = 10_000;
|
|
12
|
+
const CLIENT_DIAGNOSTIC_COMMAND_FORCE_KILL_MS = 150;
|
|
13
|
+
const CLIENT_DIAGNOSTIC_COMMAND_DRAIN_MS = 750;
|
|
14
|
+
const CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS = 1_500;
|
|
15
|
+
const CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS = 15_000;
|
|
16
|
+
const CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS = 60_000;
|
|
17
|
+
const CLIENT_DIAGNOSTIC_HARDWARE_TTL_MS = 300_000;
|
|
18
|
+
const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
19
|
+
const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngvjziawki.supabase.co';
|
|
20
|
+
const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
|
|
21
|
+
const CLIENT_STATE_DIR = process.env.LIVEDESK_STATE_DIR || (process.env.LIVEDESK_UNIFIED_RUNTIME === '1' ? join(os.homedir(), '.livedesk') : join(os.homedir(), '.livedesk-client'));
|
|
22
|
+
const CLIENT_AUTH_PATH = join(CLIENT_STATE_DIR, 'auth.json');
|
|
23
|
+
const DEFAULT_TRUSTED_WEB_ORIGINS = Object.freeze([
|
|
24
|
+
'https://livedesk.pages.dev',
|
|
25
|
+
'http://127.0.0.1:5173',
|
|
26
|
+
'http://localhost:5173',
|
|
27
|
+
'http://127.0.0.1:4173',
|
|
28
|
+
'http://localhost:4173'
|
|
29
|
+
]);
|
|
30
|
+
const LIVE_DESK_PAGES_PREVIEW_HOST = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.livedesk\.pages\.dev$/;
|
|
31
|
+
const READ_ONLY_CLIENT_API_PATHS = new Set([
|
|
32
|
+
'/api/health',
|
|
33
|
+
'/api/runtime',
|
|
34
|
+
'/api/runtime/status',
|
|
35
|
+
'/api/runtime/events',
|
|
36
|
+
'/api/client/status',
|
|
37
|
+
'/api/client/computer',
|
|
38
|
+
'/api/client/system',
|
|
39
|
+
'/api/client/permissions',
|
|
40
|
+
'/api/client/diagnostics',
|
|
41
|
+
'/api/auth/status'
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function normalizeString(value, maxLength = 1024) {
|
|
45
|
+
return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeAccountAvatarUrl(value) {
|
|
49
|
+
const candidate = normalizeString(value, 2048);
|
|
50
|
+
if (!candidate) return '';
|
|
51
|
+
try {
|
|
52
|
+
const url = new URL(candidate);
|
|
53
|
+
return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString() : '';
|
|
54
|
+
} catch {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readAccessTokenProfile(accessToken) {
|
|
60
|
+
const token = normalizeString(accessToken, 8192);
|
|
61
|
+
const payloadSegment = token.split('.')[1] || '';
|
|
62
|
+
if (!payloadSegment) return {};
|
|
63
|
+
try {
|
|
64
|
+
const payload = JSON.parse(Buffer.from(payloadSegment, 'base64url').toString('utf8'));
|
|
65
|
+
return payload && typeof payload === 'object' ? payload : {};
|
|
66
|
+
} catch {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readClientAccountProfile(sessionOrUser) {
|
|
72
|
+
const tokenProfile = readAccessTokenProfile(sessionOrUser?.access_token);
|
|
73
|
+
const user = sessionOrUser?.user && typeof sessionOrUser.user === 'object'
|
|
74
|
+
? sessionOrUser.user
|
|
75
|
+
: sessionOrUser && typeof sessionOrUser === 'object'
|
|
76
|
+
? sessionOrUser
|
|
77
|
+
: {};
|
|
78
|
+
const metadata = user.user_metadata && typeof user.user_metadata === 'object'
|
|
79
|
+
? user.user_metadata
|
|
80
|
+
: {};
|
|
81
|
+
const tokenMetadata = tokenProfile.user_metadata && typeof tokenProfile.user_metadata === 'object'
|
|
82
|
+
? tokenProfile.user_metadata
|
|
83
|
+
: {};
|
|
84
|
+
const email = normalizeString(user.email || tokenProfile.email, 320);
|
|
85
|
+
const name = normalizeString(
|
|
86
|
+
metadata.full_name
|
|
87
|
+
|| metadata.name
|
|
88
|
+
|| metadata.preferred_username
|
|
89
|
+
|| tokenMetadata.full_name
|
|
90
|
+
|| tokenMetadata.name
|
|
91
|
+
|| tokenMetadata.preferred_username
|
|
92
|
+
|| user.name
|
|
93
|
+
|| tokenProfile.name
|
|
94
|
+
|| email
|
|
95
|
+
|| user.id,
|
|
96
|
+
160
|
|
97
|
+
);
|
|
98
|
+
const avatarUrl = normalizeAccountAvatarUrl(
|
|
99
|
+
metadata.avatar_url
|
|
100
|
+
|| metadata.picture
|
|
101
|
+
|| tokenMetadata.avatar_url
|
|
102
|
+
|| tokenMetadata.picture
|
|
103
|
+
|| user.avatar_url
|
|
104
|
+
|| user.picture
|
|
105
|
+
);
|
|
106
|
+
return { email, name, avatarUrl };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mergeClientAccountProfile(session, sourceUser) {
|
|
110
|
+
const profile = readClientAccountProfile(sourceUser);
|
|
111
|
+
const existingUserMetadata = session?.user?.user_metadata && typeof session.user.user_metadata === 'object'
|
|
112
|
+
? session.user.user_metadata
|
|
113
|
+
: {};
|
|
114
|
+
const userMetadata = {
|
|
115
|
+
...existingUserMetadata,
|
|
116
|
+
...(profile.name ? { full_name: profile.name } : {}),
|
|
117
|
+
...(profile.avatarUrl ? { avatar_url: profile.avatarUrl } : {})
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
...session,
|
|
121
|
+
user: {
|
|
122
|
+
...(session?.user || {}),
|
|
123
|
+
...(Object.keys(userMetadata).length > 0 ? { user_metadata: userMetadata } : {})
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function hydrateClientAccountProfile(session) {
|
|
129
|
+
const accessToken = normalizeString(session?.access_token, 8192);
|
|
130
|
+
if (!accessToken) return session;
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetch(`${SUPABASE_URL.replace(/\/+$/, '')}/auth/v1/user`, {
|
|
133
|
+
headers: {
|
|
134
|
+
apikey: SUPABASE_PUBLISHABLE_KEY,
|
|
135
|
+
Authorization: `Bearer ${accessToken}`,
|
|
136
|
+
Accept: 'application/json'
|
|
137
|
+
},
|
|
138
|
+
signal: AbortSignal.timeout(5_000)
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) return session;
|
|
141
|
+
const verifiedUser = await response.json().catch(() => null);
|
|
142
|
+
return verifiedUser ? mergeClientAccountProfile(session, verifiedUser) : session;
|
|
143
|
+
} catch {
|
|
144
|
+
return session;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizePort(value, fallback = DEFAULT_PORT) {
|
|
149
|
+
const port = Number(value);
|
|
150
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : fallback;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeSlotNumber(value) {
|
|
154
|
+
const slotNumber = Number(String(value ?? '').trim());
|
|
155
|
+
return Number.isInteger(slotNumber) && slotNumber >= 1 && slotNumber <= 999
|
|
156
|
+
? slotNumber
|
|
157
|
+
: 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeRoleVersion(value) {
|
|
161
|
+
const roleVersion = Number(String(value ?? '').trim());
|
|
162
|
+
return Number.isInteger(roleVersion) && roleVersion >= 0 ? roleVersion : 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readCpuTotals() {
|
|
166
|
+
return os.cpus().reduce((totals, cpu) => {
|
|
167
|
+
const times = Object.values(cpu.times || {}).map(Number);
|
|
168
|
+
totals.idle += Number(cpu.times?.idle || 0);
|
|
169
|
+
totals.total += times.reduce((sum, value) => sum + value, 0);
|
|
170
|
+
return totals;
|
|
171
|
+
}, { idle: 0, total: 0 });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function finiteNumber(value) {
|
|
175
|
+
if (value === null
|
|
176
|
+
|| value === undefined
|
|
177
|
+
|| typeof value === 'boolean'
|
|
178
|
+
|| (typeof value === 'string' && value.trim() === '')) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const number = Number(value);
|
|
182
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function percentFromRatio(value) {
|
|
186
|
+
const number = finiteNumber(value);
|
|
187
|
+
if (number === null) return null;
|
|
188
|
+
return Math.round(Math.max(0, Math.min(100, number)) * 10) / 10;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function destroyChildRedirectedIo(child) {
|
|
192
|
+
for (const stream of [child?.stdin, child?.stdout, child?.stderr]) {
|
|
193
|
+
try { stream?.destroy(); } catch { /* an exact owned stream already closed */ }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sanitizeDiagnosticCommandName(value) {
|
|
198
|
+
return normalizeString(value, 200) || 'unknown-diagnostic-command';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function defaultDiagnosticMonotonicNow() {
|
|
202
|
+
return Number(process.hrtime.bigint() / 1_000_000n);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Contract:
|
|
207
|
+
* - A probe is idle until an explicit diagnostic request calls read().
|
|
208
|
+
* - Every successful, unavailable, empty, or failed result is retained for at
|
|
209
|
+
* least 15 monotonic seconds measured from refresh completion.
|
|
210
|
+
* - Concurrent readers share one refresh promise. A failed tool therefore
|
|
211
|
+
* cannot respawn once per status heartbeat.
|
|
212
|
+
*/
|
|
213
|
+
export function createClientDiagnosticProbeCache(options = {}) {
|
|
214
|
+
if (typeof options.refresh !== 'function') {
|
|
215
|
+
throw new TypeError('client-diagnostic-probe-refresh-required');
|
|
216
|
+
}
|
|
217
|
+
const probeId = normalizeString(options.probeId, 80) || 'client-diagnostic-probe';
|
|
218
|
+
const monotonicNow = typeof options.monotonicNow === 'function'
|
|
219
|
+
? options.monotonicNow
|
|
220
|
+
: defaultDiagnosticMonotonicNow;
|
|
221
|
+
const ttlMs = Math.max(
|
|
222
|
+
CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS,
|
|
223
|
+
Number(options.ttlMs) || CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS
|
|
224
|
+
);
|
|
225
|
+
const negativeTtlMs = Math.max(
|
|
226
|
+
CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS,
|
|
227
|
+
Number(options.negativeTtlMs) || CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS
|
|
228
|
+
);
|
|
229
|
+
const fallbackValue = options.fallbackValue;
|
|
230
|
+
let initialized = false;
|
|
231
|
+
let sampledAtMonotonicMs = 0;
|
|
232
|
+
let generation = 0;
|
|
233
|
+
let refreshCount = 0;
|
|
234
|
+
let cacheHitCount = 0;
|
|
235
|
+
let sharedReadCount = 0;
|
|
236
|
+
let record = {
|
|
237
|
+
ok: false,
|
|
238
|
+
value: fallbackValue,
|
|
239
|
+
error: 'not-requested'
|
|
240
|
+
};
|
|
241
|
+
let inFlight = null;
|
|
242
|
+
|
|
243
|
+
const snapshot = () => {
|
|
244
|
+
const now = Number(monotonicNow());
|
|
245
|
+
const ageMs = initialized && Number.isFinite(now)
|
|
246
|
+
? Math.max(0, now - sampledAtMonotonicMs)
|
|
247
|
+
: null;
|
|
248
|
+
return {
|
|
249
|
+
probeId,
|
|
250
|
+
initialized,
|
|
251
|
+
inFlight: Boolean(inFlight),
|
|
252
|
+
ok: initialized ? record.ok === true : false,
|
|
253
|
+
error: initialized ? normalizeString(record.error, 160) : 'not-requested',
|
|
254
|
+
ttlMs,
|
|
255
|
+
negativeTtlMs,
|
|
256
|
+
ageMs,
|
|
257
|
+
generation,
|
|
258
|
+
refreshCount,
|
|
259
|
+
cacheHitCount,
|
|
260
|
+
sharedReadCount
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const read = () => {
|
|
265
|
+
const now = Number(monotonicNow());
|
|
266
|
+
const activeTtlMs = record.ok === true ? ttlMs : negativeTtlMs;
|
|
267
|
+
if (initialized
|
|
268
|
+
&& Number.isFinite(now)
|
|
269
|
+
&& Math.max(0, now - sampledAtMonotonicMs) < activeTtlMs) {
|
|
270
|
+
cacheHitCount += 1;
|
|
271
|
+
return Promise.resolve({
|
|
272
|
+
...record,
|
|
273
|
+
cached: true,
|
|
274
|
+
shared: false,
|
|
275
|
+
generation
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (inFlight) {
|
|
279
|
+
sharedReadCount += 1;
|
|
280
|
+
return inFlight.then(result => ({ ...result, shared: true }));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
inFlight = (async () => {
|
|
284
|
+
let next;
|
|
285
|
+
try {
|
|
286
|
+
const candidate = await options.refresh();
|
|
287
|
+
next = candidate && typeof candidate === 'object'
|
|
288
|
+
? {
|
|
289
|
+
ok: candidate.ok === true,
|
|
290
|
+
value: candidate.value === undefined ? fallbackValue : candidate.value,
|
|
291
|
+
error: candidate.ok === true ? '' : normalizeString(candidate.error, 160) || 'probe-unavailable'
|
|
292
|
+
}
|
|
293
|
+
: {
|
|
294
|
+
ok: false,
|
|
295
|
+
value: fallbackValue,
|
|
296
|
+
error: 'probe-invalid-result'
|
|
297
|
+
};
|
|
298
|
+
} catch (error) {
|
|
299
|
+
next = {
|
|
300
|
+
ok: false,
|
|
301
|
+
value: fallbackValue,
|
|
302
|
+
error: normalizeString(error?.message || error, 160) || 'probe-failed'
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
record = next;
|
|
306
|
+
initialized = true;
|
|
307
|
+
generation += 1;
|
|
308
|
+
refreshCount += 1;
|
|
309
|
+
sampledAtMonotonicMs = Number(monotonicNow());
|
|
310
|
+
return {
|
|
311
|
+
...record,
|
|
312
|
+
cached: false,
|
|
313
|
+
shared: false,
|
|
314
|
+
generation
|
|
315
|
+
};
|
|
316
|
+
})().finally(() => {
|
|
317
|
+
inFlight = null;
|
|
318
|
+
});
|
|
319
|
+
return inFlight;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
read,
|
|
324
|
+
getSnapshot: snapshot
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isExecutablePath(path) {
|
|
329
|
+
try {
|
|
330
|
+
accessSync(path, fsConstants.X_OK);
|
|
331
|
+
return true;
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function resolveNvidiaSmiCommand(options = {}) {
|
|
338
|
+
const platform = normalizeString(options.platform || process.platform, 20);
|
|
339
|
+
const executablePath = normalizeString(
|
|
340
|
+
options.explicitPath ?? process.env.LIVEDESK_NVIDIA_SMI_PATH,
|
|
341
|
+
1000
|
|
342
|
+
);
|
|
343
|
+
const executableCheck = typeof options.isExecutable === 'function'
|
|
344
|
+
? options.isExecutable
|
|
345
|
+
: isExecutablePath;
|
|
346
|
+
if (platform === 'win32') {
|
|
347
|
+
return executablePath || 'nvidia-smi.exe';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const pathValue = String(options.pathValue ?? process.env.PATH ?? '');
|
|
351
|
+
const pathDelimiter = normalizeString(options.pathDelimiter, 4) || delimiter;
|
|
352
|
+
const joinExecutablePath = platform === 'win32' ? join : posix.join;
|
|
353
|
+
const candidates = [
|
|
354
|
+
executablePath,
|
|
355
|
+
...pathValue
|
|
356
|
+
.split(pathDelimiter)
|
|
357
|
+
.map(directory => directory.trim())
|
|
358
|
+
.filter(Boolean)
|
|
359
|
+
.map(directory => joinExecutablePath(directory, 'nvidia-smi')),
|
|
360
|
+
...(platform === 'linux' ? ['/usr/bin/nvidia-smi', '/usr/local/bin/nvidia-smi'] : [])
|
|
361
|
+
].filter(Boolean);
|
|
362
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
363
|
+
try {
|
|
364
|
+
if (executableCheck(candidate) === true) return candidate;
|
|
365
|
+
} catch {
|
|
366
|
+
// An unreadable PATH entry is negative probe evidence.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return '';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Contract:
|
|
374
|
+
* - Owns only ChildProcess objects spawned through this instance.
|
|
375
|
+
* - Identity is ownerId + commandId + the immutable ChildProcess handle; a PID
|
|
376
|
+
* is diagnostic evidence and is never rediscovered to authorize termination.
|
|
377
|
+
* - A command deadline and owner close both abort, terminate, drain redirected
|
|
378
|
+
* stdio, and wait for the exact child's terminal `close` event.
|
|
379
|
+
* - Concurrent identical probes share the same command/output/terminal owner.
|
|
380
|
+
* - Output, close, and active-ledger retention are bounded independently so an
|
|
381
|
+
* inherited pipe can never keep the Client event loop alive without evidence.
|
|
382
|
+
*/
|
|
383
|
+
export function createClientDiagnosticCommandOwner(options = {}) {
|
|
384
|
+
const execFileImpl = typeof options.execFileImpl === 'function' ? options.execFileImpl : execFile;
|
|
385
|
+
const ownerId = normalizeString(options.ownerId, 160)
|
|
386
|
+
|| `client-diagnostics-${process.pid}-${randomBytes(8).toString('hex')}`;
|
|
387
|
+
const forceKillDelayMs = Math.max(
|
|
388
|
+
10,
|
|
389
|
+
Number(options.forceKillDelayMs) || CLIENT_DIAGNOSTIC_COMMAND_FORCE_KILL_MS
|
|
390
|
+
);
|
|
391
|
+
const commandDrainMs = Math.max(
|
|
392
|
+
forceKillDelayMs + 10,
|
|
393
|
+
Number(options.commandDrainMs) || CLIENT_DIAGNOSTIC_COMMAND_DRAIN_MS
|
|
394
|
+
);
|
|
395
|
+
const active = new Map();
|
|
396
|
+
const activeByKey = new Map();
|
|
397
|
+
let commandSequence = 0;
|
|
398
|
+
let spawnedCount = 0;
|
|
399
|
+
let joinedCount = 0;
|
|
400
|
+
let timedOutCount = 0;
|
|
401
|
+
let sharedRequestCount = 0;
|
|
402
|
+
let closePromise = null;
|
|
403
|
+
let closed = false;
|
|
404
|
+
|
|
405
|
+
const snapshotRecord = record => ({
|
|
406
|
+
ownerId,
|
|
407
|
+
commandId: record.commandId,
|
|
408
|
+
pid: record.pid,
|
|
409
|
+
command: record.command,
|
|
410
|
+
startedAt: record.startedAt,
|
|
411
|
+
terminationReason: record.terminationReason || ''
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const getSnapshot = () => ({
|
|
415
|
+
ownerId,
|
|
416
|
+
closed,
|
|
417
|
+
spawnedCount,
|
|
418
|
+
joinedCount,
|
|
419
|
+
timedOutCount,
|
|
420
|
+
sharedRequestCount,
|
|
421
|
+
activeCount: active.size,
|
|
422
|
+
redirectedStreamCount: [...active.values()].reduce(
|
|
423
|
+
(count, record) => count + [record.child?.stdin, record.child?.stdout, record.child?.stderr]
|
|
424
|
+
.filter(stream => stream && !stream.destroyed).length,
|
|
425
|
+
0
|
|
426
|
+
),
|
|
427
|
+
active: [...active.values()].map(snapshotRecord)
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const settleOutput = (record, output = '') => {
|
|
431
|
+
if (record.outputSettled) return;
|
|
432
|
+
record.outputSettled = true;
|
|
433
|
+
record.resolveOutput(String(output || ''));
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const markTerminal = record => {
|
|
437
|
+
if (record.terminal) return;
|
|
438
|
+
record.terminal = true;
|
|
439
|
+
clearTimeout(record.commandTimer);
|
|
440
|
+
clearTimeout(record.forceKillTimer);
|
|
441
|
+
clearTimeout(record.outputDrainTimer);
|
|
442
|
+
destroyChildRedirectedIo(record.child);
|
|
443
|
+
active.delete(record.commandId);
|
|
444
|
+
if (activeByKey.get(record.commandKey) === record) {
|
|
445
|
+
activeByKey.delete(record.commandKey);
|
|
446
|
+
}
|
|
447
|
+
joinedCount += 1;
|
|
448
|
+
settleOutput(record, record.error ? '' : record.stdout);
|
|
449
|
+
record.resolveTerminal();
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const terminateOwnedRecord = (record, reason) => {
|
|
453
|
+
if (!record || record.terminal || record.terminationRequested) return;
|
|
454
|
+
record.terminationRequested = true;
|
|
455
|
+
record.terminationReason = normalizeString(reason, 80) || 'diagnostic-owner-close';
|
|
456
|
+
if (record.terminationReason === 'command-timeout') timedOutCount += 1;
|
|
457
|
+
clearTimeout(record.commandTimer);
|
|
458
|
+
try { record.abortController.abort(new Error(record.terminationReason)); } catch { /* already aborted */ }
|
|
459
|
+
try { record.child?.kill('SIGTERM'); } catch { /* exact child already exited */ }
|
|
460
|
+
record.forceKillTimer = setTimeout(() => {
|
|
461
|
+
if (record.terminal) return;
|
|
462
|
+
try { record.child?.kill('SIGKILL'); } catch { /* exact child already exited */ }
|
|
463
|
+
// An inherited writer can keep execFile's callback pending even after the
|
|
464
|
+
// root exits. Closing our exact pipe ends the Client-side handle owner.
|
|
465
|
+
destroyChildRedirectedIo(record.child);
|
|
466
|
+
}, forceKillDelayMs);
|
|
467
|
+
record.forceKillTimer.unref?.();
|
|
468
|
+
record.outputDrainTimer = setTimeout(() => {
|
|
469
|
+
if (record.terminal) return;
|
|
470
|
+
settleOutput(record, '');
|
|
471
|
+
destroyChildRedirectedIo(record.child);
|
|
472
|
+
record.child?.unref?.();
|
|
473
|
+
}, commandDrainMs);
|
|
474
|
+
record.outputDrainTimer.unref?.();
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const capture = (command, args = [], timeout = 3500) => {
|
|
478
|
+
if (closed) return Promise.resolve('');
|
|
479
|
+
const normalizedArgs = Array.isArray(args) ? args : [];
|
|
480
|
+
const commandKey = JSON.stringify([
|
|
481
|
+
String(command || ''),
|
|
482
|
+
...normalizedArgs.map(value => String(value ?? ''))
|
|
483
|
+
]);
|
|
484
|
+
const sharedRecord = activeByKey.get(commandKey);
|
|
485
|
+
if (sharedRecord) {
|
|
486
|
+
sharedRequestCount += 1;
|
|
487
|
+
return sharedRecord.outputPromise;
|
|
488
|
+
}
|
|
489
|
+
const commandId = `${ownerId}:${++commandSequence}`;
|
|
490
|
+
const abortController = new AbortController();
|
|
491
|
+
let resolveOutput;
|
|
492
|
+
let resolveTerminal;
|
|
493
|
+
const outputPromise = new Promise(resolve => { resolveOutput = resolve; });
|
|
494
|
+
const terminalPromise = new Promise(resolve => { resolveTerminal = resolve; });
|
|
495
|
+
const record = {
|
|
496
|
+
ownerId,
|
|
497
|
+
commandId,
|
|
498
|
+
commandKey,
|
|
499
|
+
command: sanitizeDiagnosticCommandName(command),
|
|
500
|
+
pid: 0,
|
|
501
|
+
startedAt: new Date().toISOString(),
|
|
502
|
+
child: null,
|
|
503
|
+
abortController,
|
|
504
|
+
resolveOutput,
|
|
505
|
+
resolveTerminal,
|
|
506
|
+
outputPromise,
|
|
507
|
+
terminalPromise,
|
|
508
|
+
stdout: '',
|
|
509
|
+
error: null,
|
|
510
|
+
outputSettled: false,
|
|
511
|
+
terminal: false,
|
|
512
|
+
terminationRequested: false,
|
|
513
|
+
terminationReason: '',
|
|
514
|
+
commandTimer: null,
|
|
515
|
+
forceKillTimer: null,
|
|
516
|
+
outputDrainTimer: null
|
|
517
|
+
};
|
|
518
|
+
active.set(commandId, record);
|
|
519
|
+
activeByKey.set(commandKey, record);
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
record.child = execFileImpl(command, normalizedArgs, {
|
|
523
|
+
encoding: 'utf8',
|
|
524
|
+
maxBuffer: 512 * 1024,
|
|
525
|
+
windowsHide: true,
|
|
526
|
+
signal: abortController.signal
|
|
527
|
+
}, (error, stdout) => {
|
|
528
|
+
record.error = error || null;
|
|
529
|
+
record.stdout = error ? '' : String(stdout || '');
|
|
530
|
+
settleOutput(record, record.stdout);
|
|
531
|
+
});
|
|
532
|
+
record.pid = Number(record.child?.pid || 0);
|
|
533
|
+
spawnedCount += 1;
|
|
534
|
+
record.child.once('close', () => markTerminal(record));
|
|
535
|
+
record.child.once('error', error => {
|
|
536
|
+
record.error = record.error || error;
|
|
537
|
+
});
|
|
538
|
+
const commandTimeoutMs = Math.max(10, Number(timeout) || 3500);
|
|
539
|
+
record.commandTimer = setTimeout(
|
|
540
|
+
() => terminateOwnedRecord(record, 'command-timeout'),
|
|
541
|
+
commandTimeoutMs
|
|
542
|
+
);
|
|
543
|
+
record.commandTimer.unref?.();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
record.error = error;
|
|
546
|
+
settleOutput(record, '');
|
|
547
|
+
markTerminal(record);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return outputPromise;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const close = (timeoutMs = CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS) => {
|
|
554
|
+
if (closePromise) return closePromise;
|
|
555
|
+
closed = true;
|
|
556
|
+
const boundedTimeoutMs = Math.max(100, Number(timeoutMs) || CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS);
|
|
557
|
+
closePromise = (async () => {
|
|
558
|
+
const records = [...active.values()];
|
|
559
|
+
for (const record of records) terminateOwnedRecord(record, 'diagnostic-owner-close');
|
|
560
|
+
if (records.length > 0) {
|
|
561
|
+
let deadlineTimer;
|
|
562
|
+
await Promise.race([
|
|
563
|
+
Promise.allSettled(records.map(record => record.terminalPromise)),
|
|
564
|
+
new Promise(resolve => {
|
|
565
|
+
deadlineTimer = setTimeout(resolve, boundedTimeoutMs);
|
|
566
|
+
})
|
|
567
|
+
]);
|
|
568
|
+
clearTimeout(deadlineTimer);
|
|
569
|
+
}
|
|
570
|
+
for (const record of active.values()) {
|
|
571
|
+
settleOutput(record, '');
|
|
572
|
+
try { record.child?.kill('SIGKILL'); } catch { /* exact child already exited */ }
|
|
573
|
+
destroyChildRedirectedIo(record.child);
|
|
574
|
+
record.child?.unref?.();
|
|
575
|
+
}
|
|
576
|
+
const snapshot = getSnapshot();
|
|
577
|
+
return {
|
|
578
|
+
ok: snapshot.activeCount === 0 && snapshot.redirectedStreamCount === 0,
|
|
579
|
+
...snapshot
|
|
580
|
+
};
|
|
581
|
+
})();
|
|
582
|
+
return closePromise;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
ownerId,
|
|
587
|
+
capture,
|
|
588
|
+
close,
|
|
589
|
+
getSnapshot
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function parseNvidiaSmiOutput(output) {
|
|
594
|
+
return String(output || '')
|
|
595
|
+
.split(/\r?\n/)
|
|
596
|
+
.map(line => line.trim())
|
|
597
|
+
.filter(Boolean)
|
|
598
|
+
.map(line => {
|
|
599
|
+
const [name = '', usage = '', totalMemoryMiB = '', usedMemoryMiB = ''] = line.split(',').map(value => value.trim());
|
|
600
|
+
const memoryTotalBytes = finiteNumber(totalMemoryMiB);
|
|
601
|
+
const memoryUsedBytes = finiteNumber(usedMemoryMiB);
|
|
602
|
+
return {
|
|
603
|
+
name: normalizeString(name, 160),
|
|
604
|
+
usagePercent: percentFromRatio(usage),
|
|
605
|
+
memoryTotalBytes: memoryTotalBytes === null ? null : Math.round(memoryTotalBytes * 1024 ** 2),
|
|
606
|
+
memoryUsedBytes: memoryUsedBytes === null ? null : Math.round(memoryUsedBytes * 1024 ** 2)
|
|
607
|
+
};
|
|
608
|
+
})
|
|
609
|
+
.filter(gpu => gpu.name);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function parseCounter(value) {
|
|
613
|
+
const number = Number(String(value || '').replaceAll(',', ''));
|
|
614
|
+
return Number.isFinite(number) && number >= 0 ? number : 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function parseWindowsNetworkOutput(output) {
|
|
618
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
619
|
+
const match = line.match(/(?:^|\s)(\d[\d,]*)\s+(\d[\d,]*)\s*$/);
|
|
620
|
+
if (match) return { receivedBytes: parseCounter(match[1]), sentBytes: parseCounter(match[2]) };
|
|
621
|
+
}
|
|
622
|
+
return { receivedBytes: 0, sentBytes: 0 };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function parseMacNetworkOutput(output) {
|
|
626
|
+
const lines = String(output || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
627
|
+
const header = lines.find(line => /\bIbytes\b/i.test(line) && /\bObytes\b/i.test(line));
|
|
628
|
+
if (!header) return { receivedBytes: 0, sentBytes: 0 };
|
|
629
|
+
const columns = header.split(/\s+/);
|
|
630
|
+
const receivedIndex = columns.findIndex(column => column.toLowerCase() === 'ibytes');
|
|
631
|
+
const sentIndex = columns.findIndex(column => column.toLowerCase() === 'obytes');
|
|
632
|
+
const byInterface = new Map();
|
|
633
|
+
for (const line of lines.slice(lines.indexOf(header) + 1)) {
|
|
634
|
+
const fields = line.split(/\s+/);
|
|
635
|
+
const name = normalizeString(fields[0], 80);
|
|
636
|
+
if (!name || name === 'lo0' || fields.length <= Math.max(receivedIndex, sentIndex)) continue;
|
|
637
|
+
const receivedBytes = parseCounter(fields[receivedIndex]);
|
|
638
|
+
const sentBytes = parseCounter(fields[sentIndex]);
|
|
639
|
+
const previous = byInterface.get(name) || { receivedBytes: 0, sentBytes: 0 };
|
|
640
|
+
byInterface.set(name, {
|
|
641
|
+
receivedBytes: Math.max(previous.receivedBytes, receivedBytes),
|
|
642
|
+
sentBytes: Math.max(previous.sentBytes, sentBytes)
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
return [...byInterface.values()].reduce((total, current) => ({
|
|
646
|
+
receivedBytes: total.receivedBytes + current.receivedBytes,
|
|
647
|
+
sentBytes: total.sentBytes + current.sentBytes
|
|
648
|
+
}), { receivedBytes: 0, sentBytes: 0 });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function parseLinuxNetworkOutput(output) {
|
|
652
|
+
let receivedBytes = 0;
|
|
653
|
+
let sentBytes = 0;
|
|
654
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
655
|
+
const separator = line.indexOf(':');
|
|
656
|
+
if (separator < 0) continue;
|
|
657
|
+
const name = line.slice(0, separator).trim();
|
|
658
|
+
if (!name || name === 'lo') continue;
|
|
659
|
+
const fields = line.slice(separator + 1).trim().split(/\s+/);
|
|
660
|
+
receivedBytes += parseCounter(fields[0]);
|
|
661
|
+
sentBytes += parseCounter(fields[8]);
|
|
662
|
+
}
|
|
663
|
+
return { receivedBytes, sentBytes };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function activeNetworkInterfaces() {
|
|
667
|
+
const interfaces = [];
|
|
668
|
+
for (const [name, addresses] of Object.entries(os.networkInterfaces())) {
|
|
669
|
+
for (const address of addresses || []) {
|
|
670
|
+
if (address.internal) continue;
|
|
671
|
+
interfaces.push({
|
|
672
|
+
name: normalizeString(name, 80),
|
|
673
|
+
address: normalizeString(address.address, 100),
|
|
674
|
+
family: normalizeString(address.family, 20)
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return interfaces;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function readNetworkTotalsProbe(commandOwner, platform = process.platform) {
|
|
682
|
+
if (platform === 'win32') {
|
|
683
|
+
const output = await commandOwner.capture('netstat.exe', ['-e'], 2500);
|
|
684
|
+
return String(output || '').trim()
|
|
685
|
+
? { ok: true, value: parseWindowsNetworkOutput(output) }
|
|
686
|
+
: { ok: false, value: { receivedBytes: 0, sentBytes: 0 }, error: 'netstat-unavailable' };
|
|
687
|
+
}
|
|
688
|
+
if (platform === 'darwin') {
|
|
689
|
+
const output = await commandOwner.capture('netstat', ['-ibn'], 2500);
|
|
690
|
+
return String(output || '').trim()
|
|
691
|
+
? { ok: true, value: parseMacNetworkOutput(output) }
|
|
692
|
+
: { ok: false, value: { receivedBytes: 0, sentBytes: 0 }, error: 'netstat-unavailable' };
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
return {
|
|
696
|
+
ok: true,
|
|
697
|
+
value: parseLinuxNetworkOutput(readFileSync('/proc/net/dev', 'utf8'))
|
|
698
|
+
};
|
|
699
|
+
} catch {
|
|
700
|
+
return {
|
|
701
|
+
ok: false,
|
|
702
|
+
value: { receivedBytes: 0, sentBytes: 0 },
|
|
703
|
+
error: 'network-counters-unavailable'
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function normalizeGpu(value) {
|
|
709
|
+
if (!value || typeof value !== 'object') return null;
|
|
710
|
+
const name = normalizeString(value.name, 160);
|
|
711
|
+
if (!name) return null;
|
|
712
|
+
return {
|
|
713
|
+
name,
|
|
714
|
+
usagePercent: percentFromRatio(value.usagePercent),
|
|
715
|
+
memoryTotalBytes: finiteNumber(value.memoryTotalBytes),
|
|
716
|
+
memoryUsedBytes: finiteNumber(value.memoryUsedBytes)
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function normalizeGpuIdentityName(value) {
|
|
721
|
+
return normalizeString(value, 160)
|
|
722
|
+
.normalize('NFKC')
|
|
723
|
+
.replace(/\((?:r|tm)\)/giu, '')
|
|
724
|
+
.replace(/[®™]/gu, '')
|
|
725
|
+
.toLocaleLowerCase('en-US')
|
|
726
|
+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
727
|
+
.trim();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export function mergeGpuSnapshots(platformGpus = [], telemetryGpus = []) {
|
|
731
|
+
const merged = [];
|
|
732
|
+
const indexesByIdentity = new Map();
|
|
733
|
+
for (const value of Array.isArray(platformGpus) ? platformGpus : []) {
|
|
734
|
+
const gpu = normalizeGpu(value);
|
|
735
|
+
const identity = normalizeGpuIdentityName(gpu?.name);
|
|
736
|
+
if (!gpu || !identity) continue;
|
|
737
|
+
const index = merged.length;
|
|
738
|
+
merged.push(gpu);
|
|
739
|
+
const indexes = indexesByIdentity.get(identity) || [];
|
|
740
|
+
indexes.push(index);
|
|
741
|
+
indexesByIdentity.set(identity, indexes);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const matchedIndexes = new Set();
|
|
745
|
+
for (const value of Array.isArray(telemetryGpus) ? telemetryGpus : []) {
|
|
746
|
+
const gpu = normalizeGpu(value);
|
|
747
|
+
const identity = normalizeGpuIdentityName(gpu?.name);
|
|
748
|
+
if (!gpu || !identity) continue;
|
|
749
|
+
const existingIndex = (indexesByIdentity.get(identity) || [])
|
|
750
|
+
.find(index => !matchedIndexes.has(index));
|
|
751
|
+
if (existingIndex === undefined) {
|
|
752
|
+
merged.push(gpu);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
matchedIndexes.add(existingIndex);
|
|
756
|
+
const existing = merged[existingIndex];
|
|
757
|
+
merged[existingIndex] = {
|
|
758
|
+
...existing,
|
|
759
|
+
usagePercent: gpu.usagePercent ?? existing.usagePercent,
|
|
760
|
+
memoryTotalBytes: gpu.memoryTotalBytes ?? existing.memoryTotalBytes,
|
|
761
|
+
memoryUsedBytes: gpu.memoryUsedBytes ?? existing.memoryUsedBytes
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
return merged;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function normalizeDisk(value) {
|
|
768
|
+
if (!value || typeof value !== 'object') return null;
|
|
769
|
+
const mount = normalizeString(value.mount, 80);
|
|
770
|
+
const totalBytes = finiteNumber(value.totalBytes);
|
|
771
|
+
const freeBytes = finiteNumber(value.freeBytes);
|
|
772
|
+
if (!mount || totalBytes === null || totalBytes <= 0 || freeBytes === null) return null;
|
|
773
|
+
const boundedFreeBytes = Math.min(totalBytes, freeBytes);
|
|
774
|
+
const usedBytes = Math.max(0, totalBytes - boundedFreeBytes);
|
|
775
|
+
return {
|
|
776
|
+
name: normalizeString(value.name, 120),
|
|
777
|
+
mount,
|
|
778
|
+
totalBytes,
|
|
779
|
+
freeBytes: boundedFreeBytes,
|
|
780
|
+
usedBytes,
|
|
781
|
+
usedPercent: Math.round((usedBytes / totalBytes) * 1000) / 10
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export function parseWindowsHardwareOutput(output) {
|
|
786
|
+
try {
|
|
787
|
+
const payload = JSON.parse(String(output || '').trim());
|
|
788
|
+
const rawGpus = Array.isArray(payload?.gpus) ? payload.gpus : payload?.gpus ? [payload.gpus] : [];
|
|
789
|
+
const rawDisks = Array.isArray(payload?.disks) ? payload.disks : payload?.disks ? [payload.disks] : [];
|
|
790
|
+
return {
|
|
791
|
+
gpus: rawGpus.map(normalizeGpu).filter(Boolean),
|
|
792
|
+
disks: rawDisks.map(normalizeDisk).filter(Boolean)
|
|
793
|
+
};
|
|
794
|
+
} catch {
|
|
795
|
+
return { gpus: [], disks: [] };
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function readRootDiskSnapshot() {
|
|
800
|
+
try {
|
|
801
|
+
const mount = parse(os.homedir()).root || '/';
|
|
802
|
+
const stats = statfsSync(mount, { bigint: true });
|
|
803
|
+
return normalizeDisk({
|
|
804
|
+
name: process.platform === 'darwin' ? 'Macintosh HD' : 'System',
|
|
805
|
+
mount,
|
|
806
|
+
totalBytes: Number(stats.bsize * stats.blocks),
|
|
807
|
+
freeBytes: Number(stats.bsize * stats.bavail)
|
|
808
|
+
});
|
|
809
|
+
} catch {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function readFilesystemDiskSnapshot(mount, name) {
|
|
815
|
+
try {
|
|
816
|
+
const stats = statfsSync(mount, { bigint: true });
|
|
817
|
+
return normalizeDisk({
|
|
818
|
+
name,
|
|
819
|
+
mount,
|
|
820
|
+
totalBytes: Number(stats.bsize * stats.blocks),
|
|
821
|
+
freeBytes: Number(stats.bsize * stats.bavail)
|
|
822
|
+
});
|
|
823
|
+
} catch {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export function readMacDiskSnapshots(options = {}) {
|
|
829
|
+
const rootMount = String(options.rootMount || '/');
|
|
830
|
+
const volumesRoot = String(options.volumesRoot || '/Volumes');
|
|
831
|
+
const disks = [];
|
|
832
|
+
const seenPaths = new Set();
|
|
833
|
+
const addDisk = (mount, name) => {
|
|
834
|
+
try {
|
|
835
|
+
const resolvedPath = realpathSync(mount);
|
|
836
|
+
if (seenPaths.has(resolvedPath)) return;
|
|
837
|
+
const disk = readFilesystemDiskSnapshot(mount, name);
|
|
838
|
+
if (!disk) return;
|
|
839
|
+
seenPaths.add(resolvedPath);
|
|
840
|
+
disks.push(disk);
|
|
841
|
+
} catch {
|
|
842
|
+
// A removable volume may disappear while the snapshot is being read.
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
addDisk(rootMount, 'Macintosh HD');
|
|
847
|
+
try {
|
|
848
|
+
for (const entry of readdirSync(volumesRoot, { withFileTypes: true })) {
|
|
849
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
850
|
+
addDisk(join(volumesRoot, entry.name), entry.name);
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
// /Volumes can be unavailable in restricted environments; keep the root disk.
|
|
854
|
+
}
|
|
855
|
+
return disks;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function readWindowsHardwareSnapshot(commandOwner) {
|
|
859
|
+
const script = [
|
|
860
|
+
'$ErrorActionPreference="Stop"',
|
|
861
|
+
'$OutputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false)',
|
|
862
|
+
'$physicalDriveIds=@(Get-CimInstance Win32_DiskDrive | ForEach-Object { Get-CimAssociatedInstance -InputObject $_ -Association Win32_DiskDriveToDiskPartition | ForEach-Object { Get-CimAssociatedInstance -InputObject $_ -Association Win32_LogicalDiskToPartition } } | Select-Object -ExpandProperty DeviceID -Unique)',
|
|
863
|
+
'$gpus=@(Get-CimInstance Win32_VideoController | Where-Object { $_.Name } | ForEach-Object { [pscustomobject]@{ name=[string]$_.Name; usagePercent=$null; memoryTotalBytes=if ($_.AdapterRAM) { [double]$_.AdapterRAM } else { $null }; memoryUsedBytes=$null } })',
|
|
864
|
+
'$disks=@(Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | Where-Object { $physicalDriveIds -contains $_.DeviceID } | Sort-Object DeviceID | ForEach-Object { [pscustomobject]@{ name=[string]$_.VolumeName; mount=[string]$_.DeviceID; totalBytes=[double]$_.Size; freeBytes=[double]$_.FreeSpace } })',
|
|
865
|
+
'[pscustomobject]@{ gpus=$gpus; disks=$disks } | ConvertTo-Json -Compress -Depth 5'
|
|
866
|
+
].join('; ');
|
|
867
|
+
return parseWindowsHardwareOutput(await commandOwner.capture('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], 5000));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function parseMacGpuOutput(output) {
|
|
871
|
+
try {
|
|
872
|
+
const payload = JSON.parse(String(output || '').trim());
|
|
873
|
+
return (Array.isArray(payload?.SPDisplaysDataType) ? payload.SPDisplaysDataType : [])
|
|
874
|
+
.map(item => normalizeGpu({ name: item?.sppci_model || item?._name }))
|
|
875
|
+
.filter(Boolean);
|
|
876
|
+
} catch {
|
|
877
|
+
return [];
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async function collectPlatformHardwareProbe(commandOwner, platform = process.platform) {
|
|
882
|
+
if (platform === 'win32') {
|
|
883
|
+
const snapshot = await readWindowsHardwareSnapshot(commandOwner);
|
|
884
|
+
return snapshot.gpus.length > 0 || snapshot.disks.length > 0
|
|
885
|
+
? { ok: true, value: snapshot }
|
|
886
|
+
: { ok: false, value: snapshot, error: 'windows-hardware-unavailable' };
|
|
887
|
+
}
|
|
888
|
+
if (platform === 'darwin') {
|
|
889
|
+
const output = await commandOwner.capture('system_profiler', ['SPDisplaysDataType', '-json'], 5000);
|
|
890
|
+
const snapshot = {
|
|
891
|
+
gpus: parseMacGpuOutput(output),
|
|
892
|
+
disks: readMacDiskSnapshots()
|
|
893
|
+
};
|
|
894
|
+
return String(output || '').trim()
|
|
895
|
+
? { ok: true, value: snapshot }
|
|
896
|
+
: { ok: false, value: snapshot, error: 'system-profiler-unavailable' };
|
|
897
|
+
}
|
|
898
|
+
const disk = readRootDiskSnapshot();
|
|
899
|
+
return {
|
|
900
|
+
ok: Boolean(disk),
|
|
901
|
+
value: { gpus: [], disks: disk ? [disk] : [] },
|
|
902
|
+
error: disk ? '' : 'root-disk-unavailable'
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function collectNvidiaGpuProbe(commandOwner, platform = process.platform) {
|
|
907
|
+
const command = resolveNvidiaSmiCommand({ platform });
|
|
908
|
+
if (!command) {
|
|
909
|
+
return { ok: false, value: [], error: 'nvidia-smi-unavailable' };
|
|
910
|
+
}
|
|
911
|
+
const output = await commandOwner.capture(
|
|
912
|
+
command,
|
|
913
|
+
['--query-gpu=name,utilization.gpu,memory.total,memory.used', '--format=csv,noheader,nounits'],
|
|
914
|
+
3000
|
|
915
|
+
);
|
|
916
|
+
const gpus = parseNvidiaSmiOutput(output);
|
|
917
|
+
return gpus.length > 0
|
|
918
|
+
? { ok: true, value: gpus }
|
|
919
|
+
: { ok: false, value: [], error: 'nvidia-smi-empty-or-failed' };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function normalizeOrigin(value) {
|
|
923
|
+
const candidate = normalizeString(value, 300);
|
|
924
|
+
if (!candidate) return '';
|
|
925
|
+
try {
|
|
926
|
+
const parsed = new URL(candidate);
|
|
927
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return '';
|
|
928
|
+
return parsed.origin.toLowerCase();
|
|
929
|
+
} catch {
|
|
930
|
+
return '';
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function createTrustedWebOrigins(port) {
|
|
935
|
+
const origins = new Set([
|
|
936
|
+
...DEFAULT_TRUSTED_WEB_ORIGINS,
|
|
937
|
+
`http://127.0.0.1:${port}`,
|
|
938
|
+
`http://localhost:${port}`
|
|
939
|
+
]);
|
|
940
|
+
for (const value of [process.env.LIVEDESK_WEB_ORIGINS, process.env.LIVEDESK_SITE_URL]) {
|
|
941
|
+
for (const candidate of String(value || '').split(/[\s,]+/)) {
|
|
942
|
+
const origin = normalizeOrigin(candidate);
|
|
943
|
+
if (origin) origins.add(origin);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return origins;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function readSavedSession() {
|
|
950
|
+
if (process.env.LIVEDESK_DESKTOP_HOST === '1') return null;
|
|
951
|
+
try {
|
|
958
952
|
const raw = JSON.parse(readFileSync(CLIENT_AUTH_PATH, 'utf8'))?.[CLIENT_AUTH_STORAGE_KEY];
|
|
959
|
-
const session = typeof raw === 'string' ? JSON.parse(raw) : null;
|
|
960
|
-
|
|
961
|
-
const plaintextRefreshToken = normalizeString(session.refresh_token, 8192);
|
|
962
|
-
const refreshToken = plaintextRefreshToken || (session.refresh_token_ref === OS_SECRET_REFERENCE
|
|
963
|
-
? CLIENT_REFRESH_SECRET_STORE.read()
|
|
964
|
-
: '');
|
|
965
|
-
if (!refreshToken) return null;
|
|
966
|
-
if (plaintextRefreshToken) {
|
|
967
|
-
if (!CLIENT_REFRESH_SECRET_STORE.write(plaintextRefreshToken)) {
|
|
968
|
-
writePrivateJsonAtomic(CLIENT_AUTH_PATH, {});
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
const migrated = { ...session, refresh_token_ref: OS_SECRET_REFERENCE };
|
|
972
|
-
delete migrated.refresh_token;
|
|
973
|
-
writePrivateJsonAtomic(CLIENT_AUTH_PATH, { [CLIENT_AUTH_STORAGE_KEY]: JSON.stringify(migrated) });
|
|
974
|
-
}
|
|
975
|
-
return { ...session, refresh_token: refreshToken };
|
|
953
|
+
const session = typeof raw === 'string' ? JSON.parse(raw) : null;
|
|
954
|
+
return session?.access_token ? session : null;
|
|
976
955
|
} catch {
|
|
977
956
|
return null;
|
|
978
957
|
}
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
function
|
|
982
|
-
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
function
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
if (!CLIENT_REFRESH_SECRET_STORE.write(session.refresh_token)) return false;
|
|
1006
|
-
const persisted = { ...session, refresh_token_ref: OS_SECRET_REFERENCE };
|
|
1007
|
-
delete persisted.refresh_token;
|
|
1008
|
-
writePrivateJsonAtomic(CLIENT_AUTH_PATH, { [CLIENT_AUTH_STORAGE_KEY]: JSON.stringify(persisted) });
|
|
1009
|
-
return true;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
function clearSavedSession() {
|
|
1013
|
-
CLIENT_REFRESH_SECRET_STORE.clear();
|
|
1014
|
-
writePrivateJsonAtomic(CLIENT_AUTH_PATH, {});
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
export async function revokeClientProviderSession(session, options = {}) {
|
|
1018
|
-
const accessToken = normalizeString(session?.access_token, 8192);
|
|
1019
|
-
if (!accessToken) return { ok: true, skipped: true, reason: 'no-access-token' };
|
|
1020
|
-
try {
|
|
1021
|
-
if (typeof options.revokeProviderSession === 'function') {
|
|
1022
|
-
const result = await options.revokeProviderSession(session);
|
|
1023
|
-
return result && typeof result === 'object' ? result : { ok: result !== false };
|
|
1024
|
-
}
|
|
1025
|
-
const fetchImpl = typeof options.fetchImpl === 'function' ? options.fetchImpl : fetch;
|
|
1026
|
-
const response = await fetchImpl(`${SUPABASE_URL.replace(/\/+$/, '')}/auth/v1/logout?scope=local`, {
|
|
1027
|
-
method: 'POST',
|
|
1028
|
-
headers: {
|
|
1029
|
-
apikey: SUPABASE_PUBLISHABLE_KEY,
|
|
1030
|
-
Authorization: `Bearer ${accessToken}`,
|
|
1031
|
-
Accept: 'application/json'
|
|
1032
|
-
},
|
|
1033
|
-
signal: AbortSignal.timeout(10_000)
|
|
1034
|
-
});
|
|
1035
|
-
if (response.ok) return { ok: true, revoked: true };
|
|
1036
|
-
if ([401, 403, 404].includes(response.status)) {
|
|
1037
|
-
return { ok: true, alreadyInvalid: true, status: response.status };
|
|
1038
|
-
}
|
|
1039
|
-
return { ok: false, error: `provider-session-revoke-failed:${response.status}` };
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
return { ok: false, error: normalizeString(error?.message || error, 240) || 'provider-session-revoke-failed' };
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function writePrivateJsonAtomic(path, value) {
|
|
961
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
962
|
+
const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
963
|
+
try {
|
|
964
|
+
writeFileSync(temporaryPath, JSON.stringify(value, null, 2), 'utf8');
|
|
965
|
+
try { chmodSync(temporaryPath, 0o600); } catch { /* Windows profile ACLs apply instead. */ }
|
|
966
|
+
renameSync(temporaryPath, path);
|
|
967
|
+
try { chmodSync(path, 0o600); } catch { /* Windows profile ACLs apply instead. */ }
|
|
968
|
+
} catch (error) {
|
|
969
|
+
try { rmSync(temporaryPath, { force: true }); } catch { /* best effort cleanup */ }
|
|
970
|
+
throw error;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function writeSavedSession(session) {
|
|
975
|
+
if (!session?.access_token || !session?.refresh_token) return false;
|
|
976
|
+
if (process.env.LIVEDESK_DESKTOP_HOST === '1') return true;
|
|
977
|
+
writePrivateJsonAtomic(CLIENT_AUTH_PATH, { [CLIENT_AUTH_STORAGE_KEY]: JSON.stringify(session) });
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function clearSavedSession() {
|
|
982
|
+
writePrivateJsonAtomic(CLIENT_AUTH_PATH, {});
|
|
983
|
+
}
|
|
1044
984
|
|
|
1045
985
|
function readBody(req) {
|
|
1046
986
|
return new Promise((resolveBody, reject) => {
|
|
@@ -1077,82 +1017,82 @@ function contentType(pathname) {
|
|
|
1077
1017
|
return 'text/html; charset=utf-8';
|
|
1078
1018
|
}
|
|
1079
1019
|
|
|
1080
|
-
function isLoopbackRequest(req) {
|
|
1081
|
-
const address = String(req.socket?.remoteAddress || '').replace(/^::ffff:/, '');
|
|
1082
|
-
return address === '127.0.0.1' || address === '::1' || address === 'localhost' || !address;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function isTrustedApiOrigin(value, trustedWebOrigins) {
|
|
1086
|
-
const rawOrigin = normalizeString(value, 300);
|
|
1087
|
-
if (rawOrigin === 'null') return true;
|
|
1088
|
-
const origin = normalizeOrigin(rawOrigin);
|
|
1089
|
-
if (!origin) return false;
|
|
1090
|
-
if (trustedWebOrigins.has(origin)) return true;
|
|
1091
|
-
try {
|
|
1092
|
-
const parsed = new URL(origin);
|
|
1093
|
-
// The unified local shell has existed at both an explicit runtime port and
|
|
1094
|
-
// the default loopback origin during Hub/Client module transitions. Treat
|
|
1095
|
-
// every HTTP loopback origin as local. The API still requires a per-process
|
|
1096
|
-
// CSRF token for mutations, and non-browser local processes already share
|
|
1097
|
-
// the same machine trust boundary.
|
|
1098
|
-
if (parsed.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname.toLowerCase())) {
|
|
1099
|
-
return true;
|
|
1100
|
-
}
|
|
1101
|
-
return parsed.protocol === 'https:' && LIVE_DESK_PAGES_PREVIEW_HOST.test(parsed.hostname);
|
|
1102
|
-
} catch {
|
|
1103
|
-
return false;
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
function isTrustedLocalRequest(req, host, port, options = {}) {
|
|
1108
|
-
if (!isLoopbackRequest(req)) return false;
|
|
1109
|
-
const requestHost = normalizeString(req.headers.host, 300).split(':')[0].toLowerCase();
|
|
1110
|
-
if (requestHost && requestHost !== host.toLowerCase() && requestHost !== 'localhost' && requestHost !== '127.0.0.1') return false;
|
|
1111
|
-
|
|
1112
|
-
// Chromium can mark modulepreload/stylesheet requests as cross-site, or
|
|
1113
|
-
// send Origin: null when the page was opened from a local shell. Static
|
|
1114
|
-
// assets are not privileged, and the local shell still needs to read the
|
|
1115
|
-
// loopback runtime API to determine the active role. Mutating API routes
|
|
1116
|
-
// remain protected by the CSRF token check below.
|
|
1117
|
-
if (options.allowStaticCrossSite === true) {
|
|
1118
|
-
return true;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const origin = normalizeString(req.headers.origin, 300);
|
|
1122
|
-
if (isTrustedApiOrigin(origin, options.trustedWebOrigins || createTrustedWebOrigins(port))) return true;
|
|
1123
|
-
if (origin) return false;
|
|
1124
|
-
// Chrome can label a loopback fetch as cross-site while omitting Origin.
|
|
1125
|
-
// Permit only explicitly read-only status routes in that shape. The response
|
|
1126
|
-
// still uses the loopback ACAO value, and all mutations continue to require
|
|
1127
|
-
// both a trusted Origin and the per-process CSRF token.
|
|
1128
|
-
if (options.allowReadOnlyCrossSite === true && ['GET', 'HEAD'].includes(req.method || '')) return true;
|
|
1129
|
-
const fetchSite = normalizeString(req.headers['sec-fetch-site'], 40).toLowerCase();
|
|
1130
|
-
return !fetchSite
|
|
1131
|
-
|| fetchSite === 'same-origin'
|
|
1132
|
-
|| fetchSite === 'same-site'
|
|
1133
|
-
|| fetchSite === 'none';
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
function resolveCorsOrigin(requestOrigin, port, trustedWebOrigins) {
|
|
1137
|
-
const origin = normalizeString(requestOrigin, 300);
|
|
1138
|
-
if (origin === 'null') return 'null';
|
|
1139
|
-
return origin && isTrustedApiOrigin(origin, trustedWebOrigins)
|
|
1140
|
-
? normalizeOrigin(origin)
|
|
1141
|
-
: `http://127.0.0.1:${port}`;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
function sendJson(res, status, payload, requestOrigin = '', port = DEFAULT_PORT, trustedWebOrigins = createTrustedWebOrigins(port)) {
|
|
1145
|
-
const body = JSON.stringify(payload);
|
|
1146
|
-
const allowedOrigin = resolveCorsOrigin(requestOrigin, port, trustedWebOrigins);
|
|
1147
|
-
res.writeHead(status, {
|
|
1148
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
1149
|
-
'Cache-Control': 'no-store',
|
|
1150
|
-
'Access-Control-Allow-Origin': allowedOrigin,
|
|
1151
|
-
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
1152
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-LiveDesk-CSRF',
|
|
1153
|
-
'Access-Control-Allow-Private-Network': 'true',
|
|
1154
|
-
Vary: 'Origin'
|
|
1155
|
-
});
|
|
1020
|
+
function isLoopbackRequest(req) {
|
|
1021
|
+
const address = String(req.socket?.remoteAddress || '').replace(/^::ffff:/, '');
|
|
1022
|
+
return address === '127.0.0.1' || address === '::1' || address === 'localhost' || !address;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function isTrustedApiOrigin(value, trustedWebOrigins) {
|
|
1026
|
+
const rawOrigin = normalizeString(value, 300);
|
|
1027
|
+
if (rawOrigin === 'null') return true;
|
|
1028
|
+
const origin = normalizeOrigin(rawOrigin);
|
|
1029
|
+
if (!origin) return false;
|
|
1030
|
+
if (trustedWebOrigins.has(origin)) return true;
|
|
1031
|
+
try {
|
|
1032
|
+
const parsed = new URL(origin);
|
|
1033
|
+
// The unified local shell has existed at both an explicit runtime port and
|
|
1034
|
+
// the default loopback origin during Hub/Client module transitions. Treat
|
|
1035
|
+
// every HTTP loopback origin as local. The API still requires a per-process
|
|
1036
|
+
// CSRF token for mutations, and non-browser local processes already share
|
|
1037
|
+
// the same machine trust boundary.
|
|
1038
|
+
if (parsed.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname.toLowerCase())) {
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
return parsed.protocol === 'https:' && LIVE_DESK_PAGES_PREVIEW_HOST.test(parsed.hostname);
|
|
1042
|
+
} catch {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function isTrustedLocalRequest(req, host, port, options = {}) {
|
|
1048
|
+
if (!isLoopbackRequest(req)) return false;
|
|
1049
|
+
const requestHost = normalizeString(req.headers.host, 300).split(':')[0].toLowerCase();
|
|
1050
|
+
if (requestHost && requestHost !== host.toLowerCase() && requestHost !== 'localhost' && requestHost !== '127.0.0.1') return false;
|
|
1051
|
+
|
|
1052
|
+
// Chromium can mark modulepreload/stylesheet requests as cross-site, or
|
|
1053
|
+
// send Origin: null when the page was opened from a local shell. Static
|
|
1054
|
+
// assets are not privileged, and the local shell still needs to read the
|
|
1055
|
+
// loopback runtime API to determine the active role. Mutating API routes
|
|
1056
|
+
// remain protected by the CSRF token check below.
|
|
1057
|
+
if (options.allowStaticCrossSite === true) {
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const origin = normalizeString(req.headers.origin, 300);
|
|
1062
|
+
if (isTrustedApiOrigin(origin, options.trustedWebOrigins || createTrustedWebOrigins(port))) return true;
|
|
1063
|
+
if (origin) return false;
|
|
1064
|
+
// Chrome can label a loopback fetch as cross-site while omitting Origin.
|
|
1065
|
+
// Permit only explicitly read-only status routes in that shape. The response
|
|
1066
|
+
// still uses the loopback ACAO value, and all mutations continue to require
|
|
1067
|
+
// both a trusted Origin and the per-process CSRF token.
|
|
1068
|
+
if (options.allowReadOnlyCrossSite === true && ['GET', 'HEAD'].includes(req.method || '')) return true;
|
|
1069
|
+
const fetchSite = normalizeString(req.headers['sec-fetch-site'], 40).toLowerCase();
|
|
1070
|
+
return !fetchSite
|
|
1071
|
+
|| fetchSite === 'same-origin'
|
|
1072
|
+
|| fetchSite === 'same-site'
|
|
1073
|
+
|| fetchSite === 'none';
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function resolveCorsOrigin(requestOrigin, port, trustedWebOrigins) {
|
|
1077
|
+
const origin = normalizeString(requestOrigin, 300);
|
|
1078
|
+
if (origin === 'null') return 'null';
|
|
1079
|
+
return origin && isTrustedApiOrigin(origin, trustedWebOrigins)
|
|
1080
|
+
? normalizeOrigin(origin)
|
|
1081
|
+
: `http://127.0.0.1:${port}`;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function sendJson(res, status, payload, requestOrigin = '', port = DEFAULT_PORT, trustedWebOrigins = createTrustedWebOrigins(port)) {
|
|
1085
|
+
const body = JSON.stringify(payload);
|
|
1086
|
+
const allowedOrigin = resolveCorsOrigin(requestOrigin, port, trustedWebOrigins);
|
|
1087
|
+
res.writeHead(status, {
|
|
1088
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
1089
|
+
'Cache-Control': 'no-store',
|
|
1090
|
+
'Access-Control-Allow-Origin': allowedOrigin,
|
|
1091
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
1092
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-LiveDesk-CSRF',
|
|
1093
|
+
'Access-Control-Allow-Private-Network': 'true',
|
|
1094
|
+
Vary: 'Origin'
|
|
1095
|
+
});
|
|
1156
1096
|
res.end(body);
|
|
1157
1097
|
}
|
|
1158
1098
|
|
|
@@ -1161,122 +1101,121 @@ function sendText(res, status, body, type = 'text/plain; charset=utf-8') {
|
|
|
1161
1101
|
res.end(body);
|
|
1162
1102
|
}
|
|
1163
1103
|
|
|
1164
|
-
export function createClientRuntimeServer(options = {}) {
|
|
1165
|
-
const host = normalizeString(options.host, 80) || DEFAULT_HOST;
|
|
1166
|
-
const port = normalizePort(options.port, DEFAULT_PORT);
|
|
1167
|
-
const initialChoice = options.initialChoice && typeof options.initialChoice === 'object'
|
|
1168
|
-
? options.initialChoice
|
|
1169
|
-
: null;
|
|
1170
|
-
const initialChoiceMessage = normalizeString(
|
|
1171
|
-
options.initialChoiceMessage,
|
|
1172
|
-
1000
|
|
1173
|
-
) || 'Existing LiveDesk credentials accepted. Starting the Client.';
|
|
1174
|
-
const trustedWebOrigins = createTrustedWebOrigins(port);
|
|
1175
|
-
const webDist = resolve(String(options.webDist || process.env.LIVEDESK_WEB_DIST || '').trim() || join(process.cwd(), 'apps', 'web', 'dist'));
|
|
1104
|
+
export function createClientRuntimeServer(options = {}) {
|
|
1105
|
+
const host = normalizeString(options.host, 80) || DEFAULT_HOST;
|
|
1106
|
+
const port = normalizePort(options.port, DEFAULT_PORT);
|
|
1107
|
+
const initialChoice = options.initialChoice && typeof options.initialChoice === 'object'
|
|
1108
|
+
? options.initialChoice
|
|
1109
|
+
: null;
|
|
1110
|
+
const initialChoiceMessage = normalizeString(
|
|
1111
|
+
options.initialChoiceMessage,
|
|
1112
|
+
1000
|
|
1113
|
+
) || 'Existing LiveDesk credentials accepted. Starting the Client.';
|
|
1114
|
+
const trustedWebOrigins = createTrustedWebOrigins(port);
|
|
1115
|
+
const webDist = resolve(String(options.webDist || process.env.LIVEDESK_WEB_DIST || '').trim() || join(process.cwd(), 'apps', 'web', 'dist'));
|
|
1176
1116
|
const deviceId = normalizeString(options.deviceId || process.env.LIVEDESK_DEVICE_ID, 160);
|
|
1177
|
-
const deviceName = normalizeString(options.deviceName || os.hostname(), 160) || os.hostname();
|
|
1178
|
-
const appVersion = normalizeString(options.appVersion || process.env.LIVEDESK_NPM_LAUNCHER_VERSION, 80) || 'dev';
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const
|
|
1186
|
-
const
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS
|
|
1193
|
-
|
|
1194
|
-
);
|
|
1195
|
-
let
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
};
|
|
1203
|
-
let
|
|
1204
|
-
let
|
|
1205
|
-
let
|
|
1206
|
-
let
|
|
1207
|
-
let
|
|
1208
|
-
let
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|| typeof diagnosticCommandOwner.
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
.
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
roleVersion: normalizeRoleVersion(options.roleVersion ?? process.env.LIVEDESK_ROLE_VERSION),
|
|
1117
|
+
const deviceName = normalizeString(options.deviceName || os.hostname(), 160) || os.hostname();
|
|
1118
|
+
const appVersion = normalizeString(options.appVersion || process.env.LIVEDESK_NPM_LAUNCHER_VERSION, 80) || 'dev';
|
|
1119
|
+
const savedSession = options.loadSavedSession === false
|
|
1120
|
+
? null
|
|
1121
|
+
: options.savedSession?.refresh_token
|
|
1122
|
+
? options.savedSession
|
|
1123
|
+
: readSavedSession();
|
|
1124
|
+
const savedSessionPersisted = Boolean(savedSession?.refresh_token);
|
|
1125
|
+
const savedAccountProfile = readClientAccountProfile(savedSession);
|
|
1126
|
+
const diagnosticPlatform = normalizeString(options.diagnosticPlatform || process.platform, 20);
|
|
1127
|
+
const diagnosticMonotonicNow = typeof options.diagnosticMonotonicNow === 'function'
|
|
1128
|
+
? options.diagnosticMonotonicNow
|
|
1129
|
+
: defaultDiagnosticMonotonicNow;
|
|
1130
|
+
const diagnosticProbeTtlMs = Math.max(
|
|
1131
|
+
CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS,
|
|
1132
|
+
Number(options.diagnosticProbeTtlMs) || CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS
|
|
1133
|
+
);
|
|
1134
|
+
let previousCpuTotals = readCpuTotals();
|
|
1135
|
+
let hardwareSnapshot = {
|
|
1136
|
+
gpus: [],
|
|
1137
|
+
disks: [],
|
|
1138
|
+
collectedAt: '',
|
|
1139
|
+
collecting: false,
|
|
1140
|
+
lastError: options.diagnosticsEnabled === false ? 'diagnostics-disabled' : 'not-requested'
|
|
1141
|
+
};
|
|
1142
|
+
let networkSnapshot = { interfaces: activeNetworkInterfaces(), receivedBytes: 0, sentBytes: 0, receiveBytesPerSecond: 0, sendBytesPerSecond: 0, sampledAt: '' };
|
|
1143
|
+
let previousNetworkTotals = null;
|
|
1144
|
+
let lastDiagnosticRequestAt = '';
|
|
1145
|
+
let lastDiagnosticCompletedAt = '';
|
|
1146
|
+
let diagnosticRefreshInFlight = null;
|
|
1147
|
+
let closed = false;
|
|
1148
|
+
let runtimeClosePromise = null;
|
|
1149
|
+
const diagnosticCommandOwner = options.diagnosticCommandOwner
|
|
1150
|
+
|| createClientDiagnosticCommandOwner();
|
|
1151
|
+
if (typeof diagnosticCommandOwner.capture !== 'function'
|
|
1152
|
+
|| typeof diagnosticCommandOwner.close !== 'function'
|
|
1153
|
+
|| typeof diagnosticCommandOwner.getSnapshot !== 'function') {
|
|
1154
|
+
throw new TypeError('invalid-client-diagnostic-command-owner');
|
|
1155
|
+
}
|
|
1156
|
+
const diagnosticTasks = new Set();
|
|
1157
|
+
const trackDiagnosticTask = operation => {
|
|
1158
|
+
if (closed) return Promise.resolve();
|
|
1159
|
+
const task = Promise.resolve()
|
|
1160
|
+
.then(operation)
|
|
1161
|
+
.finally(() => diagnosticTasks.delete(task));
|
|
1162
|
+
diagnosticTasks.add(task);
|
|
1163
|
+
return task;
|
|
1164
|
+
};
|
|
1165
|
+
const hardwareProbeCache = createClientDiagnosticProbeCache({
|
|
1166
|
+
probeId: 'platform-hardware',
|
|
1167
|
+
ttlMs: Math.max(CLIENT_DIAGNOSTIC_HARDWARE_TTL_MS, diagnosticProbeTtlMs),
|
|
1168
|
+
negativeTtlMs: CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS,
|
|
1169
|
+
monotonicNow: diagnosticMonotonicNow,
|
|
1170
|
+
fallbackValue: { gpus: [], disks: [] },
|
|
1171
|
+
refresh: () => collectPlatformHardwareProbe(diagnosticCommandOwner, diagnosticPlatform)
|
|
1172
|
+
});
|
|
1173
|
+
const nvidiaProbeCache = createClientDiagnosticProbeCache({
|
|
1174
|
+
probeId: 'nvidia-gpu',
|
|
1175
|
+
ttlMs: diagnosticProbeTtlMs,
|
|
1176
|
+
negativeTtlMs: CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS,
|
|
1177
|
+
monotonicNow: diagnosticMonotonicNow,
|
|
1178
|
+
fallbackValue: [],
|
|
1179
|
+
refresh: () => collectNvidiaGpuProbe(diagnosticCommandOwner, diagnosticPlatform)
|
|
1180
|
+
});
|
|
1181
|
+
const networkProbeCache = createClientDiagnosticProbeCache({
|
|
1182
|
+
probeId: 'network-totals',
|
|
1183
|
+
ttlMs: diagnosticProbeTtlMs,
|
|
1184
|
+
negativeTtlMs: CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS,
|
|
1185
|
+
monotonicNow: diagnosticMonotonicNow,
|
|
1186
|
+
fallbackValue: { receivedBytes: 0, sentBytes: 0 },
|
|
1187
|
+
refresh: () => readNetworkTotalsProbe(diagnosticCommandOwner, diagnosticPlatform)
|
|
1188
|
+
});
|
|
1189
|
+
const csrfToken = randomBytes(32).toString('hex');
|
|
1190
|
+
const state = {
|
|
1191
|
+
route: '/computer',
|
|
1192
|
+
manager: '',
|
|
1193
|
+
pairToken: '',
|
|
1194
|
+
slotNumber: normalizeSlotNumber(options.slot),
|
|
1195
|
+
assignedHubId: normalizeString(options.assignedHubId ?? process.env.LIVEDESK_ASSIGNED_HUB_ID, 160),
|
|
1196
|
+
endpointCandidates: [],
|
|
1197
|
+
roleVersion: normalizeRoleVersion(options.roleVersion ?? process.env.LIVEDESK_ROLE_VERSION),
|
|
1259
1198
|
connectedAt: '',
|
|
1260
1199
|
message: 'Sign in with Google or enter a Hub PIN to start this Client.',
|
|
1261
|
-
startup: false,
|
|
1262
|
-
agent: { requestedEngine: normalizeString(options.engine, 40), state: 'waiting', pid: null, engine: '' },
|
|
1263
|
-
videoAcceleration: options.videoAcceleration && typeof options.videoAcceleration === 'object'
|
|
1264
|
-
? { ...options.videoAcceleration, busy: false }
|
|
1265
|
-
: null,
|
|
1266
|
-
lastError: '',
|
|
1267
|
-
auth: {
|
|
1268
|
-
lastAttemptAt: '',
|
|
1269
|
-
lastResult: '',
|
|
1270
|
-
lastError: '',
|
|
1271
|
-
persisted: savedSessionPersisted,
|
|
1272
|
-
email: savedAccountProfile.email,
|
|
1273
|
-
name: savedAccountProfile.name,
|
|
1274
|
-
avatarUrl: savedAccountProfile.avatarUrl
|
|
1275
|
-
}
|
|
1200
|
+
startup: false,
|
|
1201
|
+
agent: { requestedEngine: normalizeString(options.engine, 40), state: 'waiting', pid: null, engine: '' },
|
|
1202
|
+
videoAcceleration: options.videoAcceleration && typeof options.videoAcceleration === 'object'
|
|
1203
|
+
? { ...options.videoAcceleration, busy: false }
|
|
1204
|
+
: null,
|
|
1205
|
+
lastError: '',
|
|
1206
|
+
auth: {
|
|
1207
|
+
lastAttemptAt: '',
|
|
1208
|
+
lastResult: '',
|
|
1209
|
+
lastError: '',
|
|
1210
|
+
persisted: savedSessionPersisted,
|
|
1211
|
+
email: savedAccountProfile.email,
|
|
1212
|
+
name: savedAccountProfile.name,
|
|
1213
|
+
avatarUrl: savedAccountProfile.avatarUrl
|
|
1214
|
+
}
|
|
1276
1215
|
};
|
|
1277
|
-
let completed = false;
|
|
1278
|
-
let loggedOut = false;
|
|
1279
|
-
let lastChoice = null;
|
|
1216
|
+
let completed = false;
|
|
1217
|
+
let loggedOut = false;
|
|
1218
|
+
let lastChoice = null;
|
|
1280
1219
|
let settleChoice;
|
|
1281
1220
|
let rejectChoice;
|
|
1282
1221
|
let server;
|
|
@@ -1287,219 +1226,219 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1287
1226
|
const runtime = createRuntimeManager({
|
|
1288
1227
|
role: 'client',
|
|
1289
1228
|
state: RuntimeState.WAITING_AUTH,
|
|
1290
|
-
authenticated: Boolean(savedSession?.access_token),
|
|
1291
|
-
userId: savedSession?.user?.id || '',
|
|
1229
|
+
authenticated: Boolean(savedSession?.access_token),
|
|
1230
|
+
userId: savedSession?.user?.id || '',
|
|
1292
1231
|
deviceId,
|
|
1293
1232
|
appVersion,
|
|
1294
1233
|
startedAt: new Date().toISOString(),
|
|
1295
1234
|
runtimePid: process.pid
|
|
1296
1235
|
});
|
|
1297
|
-
runtime.emit('runtime.waiting-auth', { role: 'client' });
|
|
1298
|
-
|
|
1299
|
-
const requestDiagnosticRefresh = () => {
|
|
1300
|
-
if (closed || options.diagnosticsEnabled === false) return Promise.resolve();
|
|
1301
|
-
if (diagnosticRefreshInFlight) return diagnosticRefreshInFlight;
|
|
1302
|
-
lastDiagnosticRequestAt = new Date().toISOString();
|
|
1303
|
-
hardwareSnapshot = { ...hardwareSnapshot, collecting: true };
|
|
1304
|
-
diagnosticRefreshInFlight = trackDiagnosticTask(async () => {
|
|
1305
|
-
const [platformResult, nvidiaResult, networkResult] = await Promise.all([
|
|
1306
|
-
hardwareProbeCache.read(),
|
|
1307
|
-
nvidiaProbeCache.read(),
|
|
1308
|
-
networkProbeCache.read()
|
|
1309
|
-
]);
|
|
1310
|
-
if (closed) return;
|
|
1311
|
-
|
|
1312
|
-
const platformSnapshot = platformResult.value && typeof platformResult.value === 'object'
|
|
1313
|
-
? platformResult.value
|
|
1314
|
-
: { gpus: [], disks: [] };
|
|
1315
|
-
const nvidiaGpus = Array.isArray(nvidiaResult.value) ? nvidiaResult.value : [];
|
|
1316
|
-
const errors = [platformResult, nvidiaResult, networkResult]
|
|
1317
|
-
.filter(result => result.ok !== true && result.error)
|
|
1318
|
-
.map(result => normalizeString(result.error, 160));
|
|
1319
|
-
hardwareSnapshot = {
|
|
1320
|
-
// Preserve the platform adapter inventory on hybrid-GPU computers. The
|
|
1321
|
-
// NVIDIA sampler enriches its matching adapter instead of replacing
|
|
1322
|
-
// every non-NVIDIA display adapter with the telemetry subset.
|
|
1323
|
-
gpus: mergeGpuSnapshots(platformSnapshot.gpus, nvidiaGpus),
|
|
1324
|
-
disks: Array.isArray(platformSnapshot.disks) ? platformSnapshot.disks : [],
|
|
1325
|
-
collectedAt: new Date().toISOString(),
|
|
1326
|
-
collecting: false,
|
|
1327
|
-
lastError: errors.join(', ')
|
|
1328
|
-
};
|
|
1329
|
-
|
|
1330
|
-
const totals = networkResult.value && typeof networkResult.value === 'object'
|
|
1331
|
-
? networkResult.value
|
|
1332
|
-
: { receivedBytes: 0, sentBytes: 0 };
|
|
1333
|
-
if (networkResult.cached !== true || !networkSnapshot.sampledAt) {
|
|
1334
|
-
const sampledAtMs = Date.now();
|
|
1335
|
-
const elapsedSeconds = previousNetworkTotals
|
|
1336
|
-
? Math.max(0.25, (sampledAtMs - previousNetworkTotals.sampledAtMs) / 1000)
|
|
1337
|
-
: 0;
|
|
1338
|
-
networkSnapshot = {
|
|
1339
|
-
interfaces: activeNetworkInterfaces(),
|
|
1340
|
-
receivedBytes: totals.receivedBytes,
|
|
1341
|
-
sentBytes: totals.sentBytes,
|
|
1342
|
-
receiveBytesPerSecond: elapsedSeconds > 0
|
|
1343
|
-
? Math.max(0, (totals.receivedBytes - previousNetworkTotals.receivedBytes) / elapsedSeconds)
|
|
1344
|
-
: 0,
|
|
1345
|
-
sendBytesPerSecond: elapsedSeconds > 0
|
|
1346
|
-
? Math.max(0, (totals.sentBytes - previousNetworkTotals.sentBytes) / elapsedSeconds)
|
|
1347
|
-
: 0,
|
|
1348
|
-
sampledAt: new Date(sampledAtMs).toISOString()
|
|
1349
|
-
};
|
|
1350
|
-
previousNetworkTotals = { ...totals, sampledAtMs };
|
|
1351
|
-
}
|
|
1352
|
-
lastDiagnosticCompletedAt = new Date().toISOString();
|
|
1353
|
-
runtime.emit('client.system.diagnostics', {
|
|
1354
|
-
gpuCount: hardwareSnapshot.gpus.length,
|
|
1355
|
-
diskCount: hardwareSnapshot.disks.length,
|
|
1356
|
-
platformHardwareGeneration: platformResult.generation,
|
|
1357
|
-
nvidiaGeneration: nvidiaResult.generation,
|
|
1358
|
-
networkGeneration: networkResult.generation
|
|
1359
|
-
});
|
|
1360
|
-
});
|
|
1361
|
-
void diagnosticRefreshInFlight.finally(() => {
|
|
1362
|
-
diagnosticRefreshInFlight = null;
|
|
1363
|
-
if (!closed && hardwareSnapshot.collecting) {
|
|
1364
|
-
hardwareSnapshot = {
|
|
1365
|
-
...hardwareSnapshot,
|
|
1366
|
-
collecting: false,
|
|
1367
|
-
lastError: hardwareSnapshot.lastError || 'diagnostic-refresh-failed'
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
});
|
|
1371
|
-
return diagnosticRefreshInFlight;
|
|
1372
|
-
};
|
|
1373
|
-
|
|
1374
|
-
const recordAuthAttempt = (result, error = '') => {
|
|
1375
|
-
const message = normalizeString(error, 240);
|
|
1376
|
-
state.auth = { ...state.auth, lastAttemptAt: new Date().toISOString(), lastResult: normalizeString(result, 80), lastError: message };
|
|
1377
|
-
state.lastError = message;
|
|
1378
|
-
if (message) state.message = `Client authentication needs attention: ${message}`;
|
|
1379
|
-
runtime.emit('client.auth.session', { result: state.auth.lastResult, error: message });
|
|
1380
|
-
};
|
|
1381
|
-
|
|
1382
|
-
const complete = (choice, message = 'Client credentials accepted. Finding the Hub.') => {
|
|
1383
|
-
lastChoice = choice || lastChoice;
|
|
1384
|
-
if (completed) {
|
|
1385
|
-
if (choice?.session?.access_token) {
|
|
1386
|
-
const accountProfile = readClientAccountProfile(choice.session);
|
|
1387
|
-
runtime.setAuthenticated(true, choice.session.user?.id || '');
|
|
1388
|
-
state.auth = {
|
|
1389
|
-
...state.auth,
|
|
1390
|
-
lastAttemptAt: new Date().toISOString(),
|
|
1391
|
-
lastResult: 'accepted',
|
|
1392
|
-
lastError: '',
|
|
1393
|
-
persisted: true,
|
|
1394
|
-
email: accountProfile.email || state.auth.email,
|
|
1395
|
-
name: accountProfile.name || state.auth.name,
|
|
1396
|
-
avatarUrl: accountProfile.avatarUrl || state.auth.avatarUrl
|
|
1397
|
-
};
|
|
1398
|
-
state.lastError = '';
|
|
1399
|
-
if (state.message.startsWith('Client authentication needs attention:')) {
|
|
1400
|
-
state.message = state.agent.state === 'running'
|
|
1401
|
-
? 'Connected to the LiveDesk Hub.'
|
|
1402
|
-
: normalizeString(message, 1000) || 'Client credentials accepted. Finding the Hub.';
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1236
|
+
runtime.emit('runtime.waiting-auth', { role: 'client' });
|
|
1237
|
+
|
|
1238
|
+
const requestDiagnosticRefresh = () => {
|
|
1239
|
+
if (closed || options.diagnosticsEnabled === false) return Promise.resolve();
|
|
1240
|
+
if (diagnosticRefreshInFlight) return diagnosticRefreshInFlight;
|
|
1241
|
+
lastDiagnosticRequestAt = new Date().toISOString();
|
|
1242
|
+
hardwareSnapshot = { ...hardwareSnapshot, collecting: true };
|
|
1243
|
+
diagnosticRefreshInFlight = trackDiagnosticTask(async () => {
|
|
1244
|
+
const [platformResult, nvidiaResult, networkResult] = await Promise.all([
|
|
1245
|
+
hardwareProbeCache.read(),
|
|
1246
|
+
nvidiaProbeCache.read(),
|
|
1247
|
+
networkProbeCache.read()
|
|
1248
|
+
]);
|
|
1249
|
+
if (closed) return;
|
|
1250
|
+
|
|
1251
|
+
const platformSnapshot = platformResult.value && typeof platformResult.value === 'object'
|
|
1252
|
+
? platformResult.value
|
|
1253
|
+
: { gpus: [], disks: [] };
|
|
1254
|
+
const nvidiaGpus = Array.isArray(nvidiaResult.value) ? nvidiaResult.value : [];
|
|
1255
|
+
const errors = [platformResult, nvidiaResult, networkResult]
|
|
1256
|
+
.filter(result => result.ok !== true && result.error)
|
|
1257
|
+
.map(result => normalizeString(result.error, 160));
|
|
1258
|
+
hardwareSnapshot = {
|
|
1259
|
+
// Preserve the platform adapter inventory on hybrid-GPU computers. The
|
|
1260
|
+
// NVIDIA sampler enriches its matching adapter instead of replacing
|
|
1261
|
+
// every non-NVIDIA display adapter with the telemetry subset.
|
|
1262
|
+
gpus: mergeGpuSnapshots(platformSnapshot.gpus, nvidiaGpus),
|
|
1263
|
+
disks: Array.isArray(platformSnapshot.disks) ? platformSnapshot.disks : [],
|
|
1264
|
+
collectedAt: new Date().toISOString(),
|
|
1265
|
+
collecting: false,
|
|
1266
|
+
lastError: errors.join(', ')
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
const totals = networkResult.value && typeof networkResult.value === 'object'
|
|
1270
|
+
? networkResult.value
|
|
1271
|
+
: { receivedBytes: 0, sentBytes: 0 };
|
|
1272
|
+
if (networkResult.cached !== true || !networkSnapshot.sampledAt) {
|
|
1273
|
+
const sampledAtMs = Date.now();
|
|
1274
|
+
const elapsedSeconds = previousNetworkTotals
|
|
1275
|
+
? Math.max(0.25, (sampledAtMs - previousNetworkTotals.sampledAtMs) / 1000)
|
|
1276
|
+
: 0;
|
|
1277
|
+
networkSnapshot = {
|
|
1278
|
+
interfaces: activeNetworkInterfaces(),
|
|
1279
|
+
receivedBytes: totals.receivedBytes,
|
|
1280
|
+
sentBytes: totals.sentBytes,
|
|
1281
|
+
receiveBytesPerSecond: elapsedSeconds > 0
|
|
1282
|
+
? Math.max(0, (totals.receivedBytes - previousNetworkTotals.receivedBytes) / elapsedSeconds)
|
|
1283
|
+
: 0,
|
|
1284
|
+
sendBytesPerSecond: elapsedSeconds > 0
|
|
1285
|
+
? Math.max(0, (totals.sentBytes - previousNetworkTotals.sentBytes) / elapsedSeconds)
|
|
1286
|
+
: 0,
|
|
1287
|
+
sampledAt: new Date(sampledAtMs).toISOString()
|
|
1288
|
+
};
|
|
1289
|
+
previousNetworkTotals = { ...totals, sampledAtMs };
|
|
1290
|
+
}
|
|
1291
|
+
lastDiagnosticCompletedAt = new Date().toISOString();
|
|
1292
|
+
runtime.emit('client.system.diagnostics', {
|
|
1293
|
+
gpuCount: hardwareSnapshot.gpus.length,
|
|
1294
|
+
diskCount: hardwareSnapshot.disks.length,
|
|
1295
|
+
platformHardwareGeneration: platformResult.generation,
|
|
1296
|
+
nvidiaGeneration: nvidiaResult.generation,
|
|
1297
|
+
networkGeneration: networkResult.generation
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
void diagnosticRefreshInFlight.finally(() => {
|
|
1301
|
+
diagnosticRefreshInFlight = null;
|
|
1302
|
+
if (!closed && hardwareSnapshot.collecting) {
|
|
1303
|
+
hardwareSnapshot = {
|
|
1304
|
+
...hardwareSnapshot,
|
|
1305
|
+
collecting: false,
|
|
1306
|
+
lastError: hardwareSnapshot.lastError || 'diagnostic-refresh-failed'
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
return diagnosticRefreshInFlight;
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
const recordAuthAttempt = (result, error = '') => {
|
|
1314
|
+
const message = normalizeString(error, 240);
|
|
1315
|
+
state.auth = { ...state.auth, lastAttemptAt: new Date().toISOString(), lastResult: normalizeString(result, 80), lastError: message };
|
|
1316
|
+
state.lastError = message;
|
|
1317
|
+
if (message) state.message = `Client authentication needs attention: ${message}`;
|
|
1318
|
+
runtime.emit('client.auth.session', { result: state.auth.lastResult, error: message });
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
const complete = (choice, message = 'Client credentials accepted. Finding the Hub.') => {
|
|
1322
|
+
lastChoice = choice || lastChoice;
|
|
1323
|
+
if (completed) {
|
|
1324
|
+
if (choice?.session?.access_token) {
|
|
1325
|
+
const accountProfile = readClientAccountProfile(choice.session);
|
|
1326
|
+
runtime.setAuthenticated(true, choice.session.user?.id || '');
|
|
1327
|
+
state.auth = {
|
|
1328
|
+
...state.auth,
|
|
1329
|
+
lastAttemptAt: new Date().toISOString(),
|
|
1330
|
+
lastResult: 'accepted',
|
|
1331
|
+
lastError: '',
|
|
1332
|
+
persisted: true,
|
|
1333
|
+
email: accountProfile.email || state.auth.email,
|
|
1334
|
+
name: accountProfile.name || state.auth.name,
|
|
1335
|
+
avatarUrl: accountProfile.avatarUrl || state.auth.avatarUrl
|
|
1336
|
+
};
|
|
1337
|
+
state.lastError = '';
|
|
1338
|
+
if (state.message.startsWith('Client authentication needs attention:')) {
|
|
1339
|
+
state.message = state.agent.state === 'running'
|
|
1340
|
+
? 'Connected to the LiveDesk Hub.'
|
|
1341
|
+
: normalizeString(message, 1000) || 'Client credentials accepted. Finding the Hub.';
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1407
1346
|
completed = true;
|
|
1408
|
-
loggedOut = false;
|
|
1409
|
-
state.connectedAt = new Date().toISOString();
|
|
1410
|
-
state.manager = normalizeString(choice?.manager, 256);
|
|
1411
|
-
if (Object.prototype.hasOwnProperty.call(choice || {}, 'hubDeviceId')) {
|
|
1412
|
-
state.assignedHubId = normalizeString(choice?.hubDeviceId, 160);
|
|
1413
|
-
}
|
|
1414
|
-
state.endpointCandidates = Array.isArray(choice?.endpointCandidates)
|
|
1415
|
-
? choice.endpointCandidates.map(value => normalizeString(value, 256)).filter(Boolean)
|
|
1416
|
-
: [];
|
|
1417
|
-
state.message = message;
|
|
1418
|
-
state.lastError = '';
|
|
1419
|
-
const accountProfile = readClientAccountProfile(choice?.session);
|
|
1420
|
-
state.auth = {
|
|
1421
|
-
...state.auth,
|
|
1422
|
-
lastAttemptAt: state.connectedAt,
|
|
1423
|
-
lastResult: 'accepted',
|
|
1424
|
-
lastError: '',
|
|
1425
|
-
persisted: Boolean(choice?.session?.refresh_token) || state.auth.persisted,
|
|
1426
|
-
email: accountProfile.email || state.auth.email,
|
|
1427
|
-
name: accountProfile.name || state.auth.name,
|
|
1428
|
-
avatarUrl: accountProfile.avatarUrl || state.auth.avatarUrl
|
|
1429
|
-
};
|
|
1347
|
+
loggedOut = false;
|
|
1348
|
+
state.connectedAt = new Date().toISOString();
|
|
1349
|
+
state.manager = normalizeString(choice?.manager, 256);
|
|
1350
|
+
if (Object.prototype.hasOwnProperty.call(choice || {}, 'hubDeviceId')) {
|
|
1351
|
+
state.assignedHubId = normalizeString(choice?.hubDeviceId, 160);
|
|
1352
|
+
}
|
|
1353
|
+
state.endpointCandidates = Array.isArray(choice?.endpointCandidates)
|
|
1354
|
+
? choice.endpointCandidates.map(value => normalizeString(value, 256)).filter(Boolean)
|
|
1355
|
+
: [];
|
|
1356
|
+
state.message = message;
|
|
1357
|
+
state.lastError = '';
|
|
1358
|
+
const accountProfile = readClientAccountProfile(choice?.session);
|
|
1359
|
+
state.auth = {
|
|
1360
|
+
...state.auth,
|
|
1361
|
+
lastAttemptAt: state.connectedAt,
|
|
1362
|
+
lastResult: 'accepted',
|
|
1363
|
+
lastError: '',
|
|
1364
|
+
persisted: Boolean(choice?.session?.refresh_token) || state.auth.persisted,
|
|
1365
|
+
email: accountProfile.email || state.auth.email,
|
|
1366
|
+
name: accountProfile.name || state.auth.name,
|
|
1367
|
+
avatarUrl: accountProfile.avatarUrl || state.auth.avatarUrl
|
|
1368
|
+
};
|
|
1430
1369
|
runtime.setAuthenticated(Boolean(choice?.session?.access_token), choice?.session?.user?.id || '');
|
|
1431
1370
|
runtime.update({ state: RuntimeState.RESOLVING_ROLE }, 'credentials-accepted');
|
|
1432
1371
|
settleChoice?.(choice);
|
|
1433
1372
|
settleChoice = null;
|
|
1434
1373
|
};
|
|
1435
1374
|
|
|
1436
|
-
const getState = () => {
|
|
1437
|
-
const snapshot = runtime.getSnapshot();
|
|
1438
|
-
const currentCpuTotals = readCpuTotals();
|
|
1439
|
-
const cpuTotalDelta = Math.max(0, currentCpuTotals.total - previousCpuTotals.total);
|
|
1440
|
-
const cpuIdleDelta = Math.max(0, currentCpuTotals.idle - previousCpuTotals.idle);
|
|
1441
|
-
const cpuUsagePercent = cpuTotalDelta > 0 ? Math.max(0, Math.min(100, (1 - cpuIdleDelta / cpuTotalDelta) * 100)) : 0;
|
|
1442
|
-
previousCpuTotals = currentCpuTotals;
|
|
1443
|
-
const totalMemoryBytes = os.totalmem();
|
|
1444
|
-
const freeMemoryBytes = os.freemem();
|
|
1445
|
-
return {
|
|
1375
|
+
const getState = () => {
|
|
1376
|
+
const snapshot = runtime.getSnapshot();
|
|
1377
|
+
const currentCpuTotals = readCpuTotals();
|
|
1378
|
+
const cpuTotalDelta = Math.max(0, currentCpuTotals.total - previousCpuTotals.total);
|
|
1379
|
+
const cpuIdleDelta = Math.max(0, currentCpuTotals.idle - previousCpuTotals.idle);
|
|
1380
|
+
const cpuUsagePercent = cpuTotalDelta > 0 ? Math.max(0, Math.min(100, (1 - cpuIdleDelta / cpuTotalDelta) * 100)) : 0;
|
|
1381
|
+
previousCpuTotals = currentCpuTotals;
|
|
1382
|
+
const totalMemoryBytes = os.totalmem();
|
|
1383
|
+
const freeMemoryBytes = os.freemem();
|
|
1384
|
+
return {
|
|
1446
1385
|
ok: true,
|
|
1447
1386
|
role: 'client',
|
|
1448
1387
|
state: snapshot.state,
|
|
1449
1388
|
deviceId,
|
|
1450
1389
|
deviceName,
|
|
1451
1390
|
runtimeStarted: completed,
|
|
1452
|
-
authenticated: snapshot.authenticated,
|
|
1453
|
-
userId: snapshot.userId || '',
|
|
1454
|
-
userEmail: state.auth.email || '',
|
|
1455
|
-
userName: state.auth.name || '',
|
|
1456
|
-
userAvatarUrl: state.auth.avatarUrl || '',
|
|
1457
|
-
appVersion,
|
|
1458
|
-
runtimeId: `client-${snapshot.runtimePid}-${snapshot.startedAt}`,
|
|
1459
|
-
runtimePid: snapshot.runtimePid,
|
|
1391
|
+
authenticated: snapshot.authenticated,
|
|
1392
|
+
userId: snapshot.userId || '',
|
|
1393
|
+
userEmail: state.auth.email || '',
|
|
1394
|
+
userName: state.auth.name || '',
|
|
1395
|
+
userAvatarUrl: state.auth.avatarUrl || '',
|
|
1396
|
+
appVersion,
|
|
1397
|
+
runtimeId: `client-${snapshot.runtimePid}-${snapshot.startedAt}`,
|
|
1398
|
+
runtimePid: snapshot.runtimePid,
|
|
1460
1399
|
startedAt: snapshot.startedAt,
|
|
1461
|
-
route: state.route,
|
|
1462
|
-
manager: state.manager,
|
|
1463
|
-
slotNumber: state.slotNumber,
|
|
1464
|
-
assignedHubId: state.assignedHubId,
|
|
1465
|
-
endpointCandidates: state.endpointCandidates,
|
|
1400
|
+
route: state.route,
|
|
1401
|
+
manager: state.manager,
|
|
1402
|
+
slotNumber: state.slotNumber,
|
|
1403
|
+
assignedHubId: state.assignedHubId,
|
|
1404
|
+
endpointCandidates: state.endpointCandidates,
|
|
1466
1405
|
roleVersion: state.roleVersion,
|
|
1467
1406
|
connectedAt: state.connectedAt,
|
|
1468
|
-
connectionState: state.agent.state === 'running' ? 'connected' : completed ? 'connecting' : 'waiting-auth',
|
|
1469
|
-
needsAuth: !snapshot.authenticated,
|
|
1470
|
-
statusLabel: loggedOut ? 'Signed out' : state.agent.state === 'running' ? 'Agent running' : completed ? 'Finding Hub' : 'Waiting for auth',
|
|
1471
|
-
message: state.message,
|
|
1472
|
-
lastError: state.lastError,
|
|
1473
|
-
auth: { ...state.auth },
|
|
1474
|
-
startup: state.startup,
|
|
1475
|
-
agent: { ...state.agent, pid: runtime.getSnapshot().agentPid },
|
|
1476
|
-
videoAcceleration: state.videoAcceleration ? { ...state.videoAcceleration } : null,
|
|
1477
|
-
system: {
|
|
1478
|
-
platform: process.platform,
|
|
1479
|
-
arch: process.arch,
|
|
1480
|
-
osVersion: os.release(),
|
|
1481
|
-
pid: process.pid,
|
|
1482
|
-
cpuUsagePercent: Math.round(cpuUsagePercent * 10) / 10,
|
|
1483
|
-
cpuLogicalCores: Math.max(1, os.cpus().length),
|
|
1484
|
-
memoryTotalBytes: totalMemoryBytes,
|
|
1485
|
-
memoryUsedBytes: Math.max(0, totalMemoryBytes - freeMemoryBytes),
|
|
1486
|
-
uptimeSeconds: Math.max(0, Math.floor(os.uptime())),
|
|
1487
|
-
gpus: hardwareSnapshot.gpus,
|
|
1488
|
-
disks: hardwareSnapshot.disks,
|
|
1489
|
-
network: networkSnapshot,
|
|
1490
|
-
hardwareCollectedAt: hardwareSnapshot.collectedAt,
|
|
1491
|
-
hardwareCollecting: hardwareSnapshot.collecting,
|
|
1492
|
-
hardwareLastError: hardwareSnapshot.lastError || '',
|
|
1493
|
-
diagnosticProbes: {
|
|
1494
|
-
requestDriven: true,
|
|
1495
|
-
minimumTtlMs: CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS,
|
|
1496
|
-
requestedAt: lastDiagnosticRequestAt,
|
|
1497
|
-
completedAt: lastDiagnosticCompletedAt,
|
|
1498
|
-
platformHardware: hardwareProbeCache.getSnapshot(),
|
|
1499
|
-
nvidiaGpu: nvidiaProbeCache.getSnapshot(),
|
|
1500
|
-
networkTotals: networkProbeCache.getSnapshot()
|
|
1501
|
-
}
|
|
1502
|
-
},
|
|
1407
|
+
connectionState: state.agent.state === 'running' ? 'connected' : completed ? 'connecting' : 'waiting-auth',
|
|
1408
|
+
needsAuth: !snapshot.authenticated,
|
|
1409
|
+
statusLabel: loggedOut ? 'Signed out' : state.agent.state === 'running' ? 'Agent running' : completed ? 'Finding Hub' : 'Waiting for auth',
|
|
1410
|
+
message: state.message,
|
|
1411
|
+
lastError: state.lastError,
|
|
1412
|
+
auth: { ...state.auth },
|
|
1413
|
+
startup: state.startup,
|
|
1414
|
+
agent: { ...state.agent, pid: runtime.getSnapshot().agentPid },
|
|
1415
|
+
videoAcceleration: state.videoAcceleration ? { ...state.videoAcceleration } : null,
|
|
1416
|
+
system: {
|
|
1417
|
+
platform: process.platform,
|
|
1418
|
+
arch: process.arch,
|
|
1419
|
+
osVersion: os.release(),
|
|
1420
|
+
pid: process.pid,
|
|
1421
|
+
cpuUsagePercent: Math.round(cpuUsagePercent * 10) / 10,
|
|
1422
|
+
cpuLogicalCores: Math.max(1, os.cpus().length),
|
|
1423
|
+
memoryTotalBytes: totalMemoryBytes,
|
|
1424
|
+
memoryUsedBytes: Math.max(0, totalMemoryBytes - freeMemoryBytes),
|
|
1425
|
+
uptimeSeconds: Math.max(0, Math.floor(os.uptime())),
|
|
1426
|
+
gpus: hardwareSnapshot.gpus,
|
|
1427
|
+
disks: hardwareSnapshot.disks,
|
|
1428
|
+
network: networkSnapshot,
|
|
1429
|
+
hardwareCollectedAt: hardwareSnapshot.collectedAt,
|
|
1430
|
+
hardwareCollecting: hardwareSnapshot.collecting,
|
|
1431
|
+
hardwareLastError: hardwareSnapshot.lastError || '',
|
|
1432
|
+
diagnosticProbes: {
|
|
1433
|
+
requestDriven: true,
|
|
1434
|
+
minimumTtlMs: CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS,
|
|
1435
|
+
requestedAt: lastDiagnosticRequestAt,
|
|
1436
|
+
completedAt: lastDiagnosticCompletedAt,
|
|
1437
|
+
platformHardware: hardwareProbeCache.getSnapshot(),
|
|
1438
|
+
nvidiaGpu: nvidiaProbeCache.getSnapshot(),
|
|
1439
|
+
networkTotals: networkProbeCache.getSnapshot()
|
|
1440
|
+
}
|
|
1441
|
+
},
|
|
1503
1442
|
permissions: {
|
|
1504
1443
|
screenCapture: state.agent.state === 'running',
|
|
1505
1444
|
remoteInput: state.agent.state === 'running',
|
|
@@ -1528,385 +1467,366 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1528
1467
|
}
|
|
1529
1468
|
};
|
|
1530
1469
|
|
|
1531
|
-
const handleRequest = async (req, res) => {
|
|
1532
|
-
const requestUrl = new URL(req.url || '/', `http://${host}:${port}`);
|
|
1533
|
-
const pathname = requestUrl.pathname;
|
|
1534
|
-
const requestOrigin = normalizeString(req.headers.origin, 300);
|
|
1535
|
-
const respondJson = (status, payload) => sendJson(res, status, payload, requestOrigin, port, trustedWebOrigins);
|
|
1536
|
-
const isStaticAppRequest = !pathname.startsWith('/api/');
|
|
1537
|
-
const allowReadOnlyCrossSite = READ_ONLY_CLIENT_API_PATHS.has(pathname);
|
|
1538
|
-
if (!isTrustedLocalRequest(req, host, port, { allowStaticCrossSite: isStaticAppRequest, allowReadOnlyCrossSite, trustedWebOrigins })) {
|
|
1539
|
-
runtime.emit('client.http.rejected', {
|
|
1540
|
-
method: normalizeString(req.method, 12),
|
|
1541
|
-
pathname: normalizeString(pathname, 160),
|
|
1542
|
-
reason: 'untrusted-local-origin',
|
|
1543
|
-
origin: requestOrigin,
|
|
1544
|
-
fetchSite: normalizeString(req.headers['sec-fetch-site'], 40)
|
|
1545
|
-
});
|
|
1546
|
-
if (pathname === '/api/auth/session') recordAuthAttempt('rejected', 'untrusted-local-origin');
|
|
1547
|
-
respondJson(403, { ok: false, error: 'untrusted-local-origin' });
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
if (req.method === 'OPTIONS') {
|
|
1551
|
-
res.writeHead(204, {
|
|
1552
|
-
'Access-Control-Allow-Origin': resolveCorsOrigin(requestOrigin, port, trustedWebOrigins),
|
|
1553
|
-
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
1554
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-LiveDesk-CSRF',
|
|
1555
|
-
'Access-Control-Allow-Private-Network': 'true',
|
|
1556
|
-
Vary: 'Origin'
|
|
1557
|
-
});
|
|
1558
|
-
res.end();
|
|
1559
|
-
return;
|
|
1470
|
+
const handleRequest = async (req, res) => {
|
|
1471
|
+
const requestUrl = new URL(req.url || '/', `http://${host}:${port}`);
|
|
1472
|
+
const pathname = requestUrl.pathname;
|
|
1473
|
+
const requestOrigin = normalizeString(req.headers.origin, 300);
|
|
1474
|
+
const respondJson = (status, payload) => sendJson(res, status, payload, requestOrigin, port, trustedWebOrigins);
|
|
1475
|
+
const isStaticAppRequest = !pathname.startsWith('/api/');
|
|
1476
|
+
const allowReadOnlyCrossSite = READ_ONLY_CLIENT_API_PATHS.has(pathname);
|
|
1477
|
+
if (!isTrustedLocalRequest(req, host, port, { allowStaticCrossSite: isStaticAppRequest, allowReadOnlyCrossSite, trustedWebOrigins })) {
|
|
1478
|
+
runtime.emit('client.http.rejected', {
|
|
1479
|
+
method: normalizeString(req.method, 12),
|
|
1480
|
+
pathname: normalizeString(pathname, 160),
|
|
1481
|
+
reason: 'untrusted-local-origin',
|
|
1482
|
+
origin: requestOrigin,
|
|
1483
|
+
fetchSite: normalizeString(req.headers['sec-fetch-site'], 40)
|
|
1484
|
+
});
|
|
1485
|
+
if (pathname === '/api/auth/session') recordAuthAttempt('rejected', 'untrusted-local-origin');
|
|
1486
|
+
respondJson(403, { ok: false, error: 'untrusted-local-origin' });
|
|
1487
|
+
return;
|
|
1560
1488
|
}
|
|
1561
|
-
if (
|
|
1562
|
-
|
|
1563
|
-
|
|
1489
|
+
if (req.method === 'OPTIONS') {
|
|
1490
|
+
res.writeHead(204, {
|
|
1491
|
+
'Access-Control-Allow-Origin': resolveCorsOrigin(requestOrigin, port, trustedWebOrigins),
|
|
1492
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
1493
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-LiveDesk-CSRF',
|
|
1494
|
+
'Access-Control-Allow-Private-Network': 'true',
|
|
1495
|
+
Vary: 'Origin'
|
|
1496
|
+
});
|
|
1497
|
+
res.end();
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method || '') && req.headers['x-livedesk-csrf'] !== csrfToken) {
|
|
1501
|
+
if (pathname === '/api/auth/session') recordAuthAttempt('rejected', 'csrf-token-required');
|
|
1502
|
+
respondJson(403, { ok: false, error: 'csrf-token-required' });
|
|
1564
1503
|
return;
|
|
1565
1504
|
}
|
|
1566
1505
|
if (pathname === '/api/health') {
|
|
1567
|
-
respondJson(200, { ok: true, product: 'LiveDesk', role: 'client', timestamp: new Date().toISOString() });
|
|
1506
|
+
respondJson(200, { ok: true, product: 'LiveDesk', role: 'client', timestamp: new Date().toISOString() });
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
if (pathname === '/api/client/diagnostics') {
|
|
1510
|
+
await requestDiagnosticRefresh();
|
|
1511
|
+
respondJson(200, getState());
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (pathname === '/api/runtime' || pathname === '/api/runtime/status' || pathname === '/api/client/status' || pathname === '/api/client/computer' || pathname === '/api/client/system' || pathname === '/api/client/permissions') {
|
|
1515
|
+
respondJson(200, getState());
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
if (pathname === '/api/runtime/events') {
|
|
1519
|
+
respondJson(200, { ok: true, events: runtime.getEvents() });
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
if (pathname === '/api/auth/status') {
|
|
1523
|
+
const snapshot = runtime.getSnapshot();
|
|
1524
|
+
respondJson(200, {
|
|
1525
|
+
ok: true,
|
|
1526
|
+
authenticated: snapshot.authenticated,
|
|
1527
|
+
userId: snapshot.userId || null,
|
|
1528
|
+
userEmail: state.auth.email || null,
|
|
1529
|
+
userName: state.auth.name || null,
|
|
1530
|
+
userAvatarUrl: state.auth.avatarUrl || null,
|
|
1531
|
+
role: 'client'
|
|
1532
|
+
});
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (pathname === '/auth/google' && req.method === 'GET') {
|
|
1536
|
+
try {
|
|
1537
|
+
if (runtime.getSnapshot().authenticated) {
|
|
1538
|
+
res.writeHead(303, { Location: '/', 'Cache-Control': 'no-store' });
|
|
1539
|
+
res.end();
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (typeof options.beginGoogleSignIn !== 'function') {
|
|
1543
|
+
throw new Error('client-google-sign-in-unavailable');
|
|
1544
|
+
}
|
|
1545
|
+
recordAuthAttempt('oauth-starting');
|
|
1546
|
+
const redirectTo = `http://${host}:${port}/callback`;
|
|
1547
|
+
const result = await options.beginGoogleSignIn(redirectTo);
|
|
1548
|
+
const authorizationUrl = normalizeString(result?.url || result, 4000);
|
|
1549
|
+
if (!authorizationUrl) throw new Error(result?.error || 'google-authorization-url-missing');
|
|
1550
|
+
recordAuthAttempt('oauth-waiting');
|
|
1551
|
+
res.writeHead(302, {
|
|
1552
|
+
Location: authorizationUrl,
|
|
1553
|
+
'Cache-Control': 'no-store',
|
|
1554
|
+
'Referrer-Policy': 'no-referrer'
|
|
1555
|
+
});
|
|
1556
|
+
res.end();
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
const message = normalizeString(error?.message || error, 240);
|
|
1559
|
+
recordAuthAttempt('rejected', message);
|
|
1560
|
+
res.writeHead(303, { Location: `/?auth_error=${encodeURIComponent(message)}`, 'Cache-Control': 'no-store' });
|
|
1561
|
+
res.end();
|
|
1562
|
+
}
|
|
1568
1563
|
return;
|
|
1569
1564
|
}
|
|
1570
|
-
if (pathname === '/
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1565
|
+
if (pathname === '/callback' && req.method === 'GET') {
|
|
1566
|
+
const providerError = normalizeString(
|
|
1567
|
+
requestUrl.searchParams.get('error_description') || requestUrl.searchParams.get('error'),
|
|
1568
|
+
240
|
|
1569
|
+
);
|
|
1570
|
+
const code = normalizeString(requestUrl.searchParams.get('code'), 2000);
|
|
1571
|
+
try {
|
|
1572
|
+
if (providerError) throw new Error(providerError);
|
|
1573
|
+
if (!code) throw new Error('google-auth-code-missing');
|
|
1574
|
+
if (typeof options.exchangeGoogleCode !== 'function') {
|
|
1575
|
+
throw new Error('client-google-callback-unavailable');
|
|
1576
|
+
}
|
|
1577
|
+
recordAuthAttempt('oauth-exchanging');
|
|
1578
|
+
const exchanged = await options.exchangeGoogleCode(code);
|
|
1579
|
+
const sessionCandidate = exchanged?.session || exchanged;
|
|
1580
|
+
const normalized = normalizeRuntimeAuthSession(sessionCandidate, { requireRefreshToken: true });
|
|
1581
|
+
if (!normalized.ok) throw new Error(normalized.error);
|
|
1582
|
+
const session = {
|
|
1583
|
+
...sessionCandidate,
|
|
1584
|
+
...mergeClientAccountProfile(normalized.session, sessionCandidate?.user)
|
|
1585
|
+
};
|
|
1586
|
+
if (!writeSavedSession(session)) throw new Error('refresh-token-required');
|
|
1587
|
+
state.auth.persisted = true;
|
|
1588
|
+
complete({ type: 'google', session }, 'Signed in. Finding the LiveDesk Hub.');
|
|
1589
|
+
res.writeHead(303, { Location: '/', 'Cache-Control': 'no-store' });
|
|
1590
|
+
res.end();
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
const message = normalizeString(error?.message || error, 240);
|
|
1593
|
+
recordAuthAttempt('rejected', message);
|
|
1594
|
+
res.writeHead(303, { Location: `/?auth_error=${encodeURIComponent(message)}`, 'Cache-Control': 'no-store' });
|
|
1595
|
+
res.end();
|
|
1596
|
+
}
|
|
1581
1597
|
return;
|
|
1582
1598
|
}
|
|
1583
|
-
if (pathname === '/api/auth/
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
}
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
if (pathname === '/auth/google' && req.method === 'GET') {
|
|
1597
|
-
try {
|
|
1598
|
-
if (runtime.getSnapshot().authenticated) {
|
|
1599
|
-
res.writeHead(303, { Location: '/', 'Cache-Control': 'no-store' });
|
|
1600
|
-
res.end();
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
if (typeof options.beginGoogleSignIn !== 'function') {
|
|
1604
|
-
throw new Error('client-google-sign-in-unavailable');
|
|
1605
|
-
}
|
|
1606
|
-
recordAuthAttempt('oauth-starting');
|
|
1607
|
-
const redirectTo = `http://${host}:${port}/callback`;
|
|
1608
|
-
const result = await options.beginGoogleSignIn(redirectTo);
|
|
1609
|
-
const authorizationUrl = normalizeString(result?.url || result, 4000);
|
|
1610
|
-
if (!authorizationUrl) throw new Error(result?.error || 'google-authorization-url-missing');
|
|
1611
|
-
recordAuthAttempt('oauth-waiting');
|
|
1612
|
-
res.writeHead(302, {
|
|
1613
|
-
Location: authorizationUrl,
|
|
1614
|
-
'Cache-Control': 'no-store',
|
|
1615
|
-
'Referrer-Policy': 'no-referrer'
|
|
1616
|
-
});
|
|
1617
|
-
res.end();
|
|
1618
|
-
} catch (error) {
|
|
1619
|
-
const message = normalizeString(error?.message || error, 240);
|
|
1620
|
-
recordAuthAttempt('rejected', message);
|
|
1621
|
-
res.writeHead(303, { Location: `/?auth_error=${encodeURIComponent(message)}`, 'Cache-Control': 'no-store' });
|
|
1622
|
-
res.end();
|
|
1623
|
-
}
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
if (pathname === '/callback' && req.method === 'GET') {
|
|
1627
|
-
const providerError = normalizeString(
|
|
1628
|
-
requestUrl.searchParams.get('error_description') || requestUrl.searchParams.get('error'),
|
|
1629
|
-
240
|
|
1630
|
-
);
|
|
1631
|
-
const code = normalizeString(requestUrl.searchParams.get('code'), 2000);
|
|
1632
|
-
try {
|
|
1633
|
-
if (providerError) throw new Error(providerError);
|
|
1634
|
-
if (!code) throw new Error('google-auth-code-missing');
|
|
1635
|
-
if (typeof options.exchangeGoogleCode !== 'function') {
|
|
1636
|
-
throw new Error('client-google-callback-unavailable');
|
|
1637
|
-
}
|
|
1638
|
-
recordAuthAttempt('oauth-exchanging');
|
|
1639
|
-
const exchanged = await options.exchangeGoogleCode(code);
|
|
1640
|
-
const sessionCandidate = exchanged?.session || exchanged;
|
|
1641
|
-
const normalized = normalizeRuntimeAuthSession(sessionCandidate, { requireRefreshToken: true });
|
|
1642
|
-
if (!normalized.ok) throw new Error(normalized.error);
|
|
1643
|
-
const session = {
|
|
1644
|
-
...sessionCandidate,
|
|
1645
|
-
...mergeClientAccountProfile(normalized.session, sessionCandidate?.user)
|
|
1646
|
-
};
|
|
1647
|
-
if (!writeSavedSession(session)) throw new Error('refresh-token-required');
|
|
1648
|
-
state.auth.persisted = true;
|
|
1649
|
-
complete({ type: 'google', session }, 'Signed in. Finding the LiveDesk Hub.');
|
|
1650
|
-
res.writeHead(303, { Location: '/', 'Cache-Control': 'no-store' });
|
|
1651
|
-
res.end();
|
|
1652
|
-
} catch (error) {
|
|
1653
|
-
const message = normalizeString(error?.message || error, 240);
|
|
1654
|
-
recordAuthAttempt('rejected', message);
|
|
1655
|
-
res.writeHead(303, { Location: `/?auth_error=${encodeURIComponent(message)}`, 'Cache-Control': 'no-store' });
|
|
1656
|
-
res.end();
|
|
1657
|
-
}
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
if (pathname === '/api/auth/session' && req.method === 'POST') {
|
|
1661
|
-
const body = parseJsonBody(await readBody(req));
|
|
1662
|
-
const normalized = normalizeRuntimeAuthSession(body, { requireRefreshToken: true });
|
|
1663
|
-
if (!normalized.ok) {
|
|
1664
|
-
recordAuthAttempt('rejected', normalized.error);
|
|
1665
|
-
respondJson(400, { ok: false, error: normalized.error });
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
const accessToken = normalized.session.access_token;
|
|
1669
|
-
recordAuthAttempt('verifying');
|
|
1670
|
-
const verifyResponse = await fetch(`${SUPABASE_URL.replace(/\/+$/, '')}/auth/v1/user`, {
|
|
1599
|
+
if (pathname === '/api/auth/session' && req.method === 'POST') {
|
|
1600
|
+
const body = parseJsonBody(await readBody(req));
|
|
1601
|
+
const normalized = normalizeRuntimeAuthSession(body, { requireRefreshToken: true });
|
|
1602
|
+
if (!normalized.ok) {
|
|
1603
|
+
recordAuthAttempt('rejected', normalized.error);
|
|
1604
|
+
respondJson(400, { ok: false, error: normalized.error });
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const accessToken = normalized.session.access_token;
|
|
1608
|
+
recordAuthAttempt('verifying');
|
|
1609
|
+
const verifyResponse = await fetch(`${SUPABASE_URL.replace(/\/+$/, '')}/auth/v1/user`, {
|
|
1671
1610
|
headers: { apikey: SUPABASE_PUBLISHABLE_KEY, Authorization: `Bearer ${accessToken}`, Accept: 'application/json' }
|
|
1672
1611
|
});
|
|
1673
|
-
if (!verifyResponse.ok) {
|
|
1674
|
-
recordAuthAttempt('rejected', `supabase-user-verification-failed:${verifyResponse.status}`);
|
|
1675
|
-
respondJson(401, { ok: false, error: `supabase-user-verification-failed:${verifyResponse.status}` });
|
|
1612
|
+
if (!verifyResponse.ok) {
|
|
1613
|
+
recordAuthAttempt('rejected', `supabase-user-verification-failed:${verifyResponse.status}`);
|
|
1614
|
+
respondJson(401, { ok: false, error: `supabase-user-verification-failed:${verifyResponse.status}` });
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const verifiedUser = await verifyResponse.json().catch(() => ({}));
|
|
1618
|
+
const verifiedSession = {
|
|
1619
|
+
...normalized.session,
|
|
1620
|
+
user: {
|
|
1621
|
+
id: normalizeString(verifiedUser?.id || body.user?.id || body.userId, 160),
|
|
1622
|
+
email: normalizeString(verifiedUser?.email || body.user?.email || body.email, 320)
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
const session = mergeClientAccountProfile(verifiedSession, verifiedUser);
|
|
1626
|
+
try {
|
|
1627
|
+
if (!writeSavedSession(session)) throw new Error('refresh-token-required');
|
|
1628
|
+
state.auth.persisted = true;
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
const message = `session-persistence-failed:${normalizeString(error?.message || error, 160)}`;
|
|
1631
|
+
recordAuthAttempt('rejected', message);
|
|
1632
|
+
respondJson(500, { ok: false, error: message });
|
|
1676
1633
|
return;
|
|
1677
1634
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1635
|
+
complete({ type: 'google', session }, 'Signed in. Finding the LiveDesk Hub.');
|
|
1636
|
+
respondJson(200, { ok: true, authenticated: true, persisted: true, role: 'client' });
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (pathname === '/api/auth/session' && req.method === 'DELETE') {
|
|
1640
|
+
clearSavedSession();
|
|
1641
|
+
loggedOut = true;
|
|
1642
|
+
completed = false;
|
|
1643
|
+
lastChoice = null;
|
|
1644
|
+
state.manager = '';
|
|
1645
|
+
state.message = 'Saved sign-in was cleared. Sign in again to start the Client.';
|
|
1646
|
+
state.lastError = '';
|
|
1647
|
+
state.auth = {
|
|
1648
|
+
lastAttemptAt: new Date().toISOString(),
|
|
1649
|
+
lastResult: 'cleared',
|
|
1650
|
+
lastError: '',
|
|
1651
|
+
persisted: false,
|
|
1652
|
+
email: '',
|
|
1653
|
+
name: '',
|
|
1654
|
+
avatarUrl: ''
|
|
1655
|
+
};
|
|
1656
|
+
runtime.setAuthenticated(false);
|
|
1657
|
+
respondJson(200, { ok: true, authenticated: false, role: 'client' });
|
|
1698
1658
|
return;
|
|
1699
|
-
}
|
|
1700
|
-
if (pathname === '/api/auth/session' && req.method === 'DELETE') {
|
|
1701
|
-
const activeSession = lastChoice?.session || savedSession || readSavedSession();
|
|
1702
|
-
const providerLogout = await revokeClientProviderSession(activeSession, {
|
|
1703
|
-
revokeProviderSession: options.revokeProviderSession,
|
|
1704
|
-
fetchImpl: options.fetchImpl
|
|
1705
|
-
});
|
|
1706
|
-
clearSavedSession();
|
|
1707
|
-
loggedOut = true;
|
|
1708
|
-
completed = false;
|
|
1709
|
-
lastChoice = null;
|
|
1710
|
-
state.manager = '';
|
|
1711
|
-
state.message = 'Saved sign-in was cleared. Sign in again to start the Client.';
|
|
1712
|
-
state.lastError = '';
|
|
1713
|
-
state.auth = {
|
|
1714
|
-
lastAttemptAt: new Date().toISOString(),
|
|
1715
|
-
lastResult: 'cleared',
|
|
1716
|
-
lastError: '',
|
|
1717
|
-
persisted: false,
|
|
1718
|
-
email: '',
|
|
1719
|
-
name: '',
|
|
1720
|
-
avatarUrl: ''
|
|
1721
|
-
};
|
|
1722
|
-
runtime.setAuthenticated(false);
|
|
1723
|
-
respondJson(providerLogout.ok ? 200 : 502, {
|
|
1724
|
-
ok: providerLogout.ok,
|
|
1725
|
-
authenticated: false,
|
|
1726
|
-
localCleared: true,
|
|
1727
|
-
role: 'client',
|
|
1728
|
-
providerLogout
|
|
1729
|
-
});
|
|
1730
|
-
return;
|
|
1731
1659
|
}
|
|
1732
1660
|
if (pathname === '/api/runtime/restart' && req.method === 'POST') {
|
|
1733
1661
|
runtime.emit('runtime.restart.requested', { role: 'client' });
|
|
1734
|
-
respondJson(200, { ok: true, restarting: true, role: 'client' });
|
|
1662
|
+
respondJson(200, { ok: true, restarting: true, role: 'client' });
|
|
1735
1663
|
setTimeout(() => options.onRestart?.(), 150);
|
|
1736
1664
|
return;
|
|
1737
1665
|
}
|
|
1738
|
-
if (pathname === '/api/runtime/shutdown' && req.method === 'POST') {
|
|
1739
|
-
runtime.emit('runtime.shutdown.requested', { role: 'client' });
|
|
1740
|
-
respondJson(200, { ok: true, shuttingDown: true, role: 'client' });
|
|
1741
|
-
setTimeout(() => options.onShutdown?.(), 150);
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
if (pathname === '/api/client/slot' && req.method === 'POST') {
|
|
1745
|
-
const body = parseJsonBody(await readBody(req));
|
|
1746
|
-
const slotNumber = normalizeSlotNumber(body.slotNumber ?? body.slot);
|
|
1747
|
-
if (!slotNumber) {
|
|
1748
|
-
respondJson(400, { ok: false, error: 'invalid-slot-number', slotNumber: state.slotNumber });
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
if (!state.manager || !state.pairToken || state.agent.state !== 'running') {
|
|
1752
|
-
respondJson(409, { ok: false, error: 'hub-not-connected', slotNumber: state.slotNumber });
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
if (typeof options.assignSlot !== 'function') {
|
|
1756
|
-
respondJson(409, { ok: false, error: 'slot-assignment-unavailable', slotNumber: state.slotNumber });
|
|
1757
|
-
return;
|
|
1758
|
-
}
|
|
1759
|
-
runtime.emit('client.slot.assignment-requested', {
|
|
1760
|
-
deviceId,
|
|
1761
|
-
previousSlotNumber: state.slotNumber,
|
|
1762
|
-
slotNumber
|
|
1763
|
-
});
|
|
1764
|
-
try {
|
|
1765
|
-
const result = await options.assignSlot({
|
|
1766
|
-
manager: state.manager,
|
|
1767
|
-
pairToken: state.pairToken,
|
|
1768
|
-
deviceId,
|
|
1769
|
-
slotNumber
|
|
1770
|
-
});
|
|
1771
|
-
if (result?.ok !== true) {
|
|
1772
|
-
const error = normalizeString(result?.error, 120) || 'slot-assignment-failed';
|
|
1773
|
-
runtime.emit('client.slot.assignment-rejected', {
|
|
1774
|
-
deviceId,
|
|
1775
|
-
slotNumber,
|
|
1776
|
-
error,
|
|
1777
|
-
conflictDeviceId: normalizeString(result?.conflict?.deviceId, 160)
|
|
1778
|
-
});
|
|
1779
|
-
respondJson(error === 'slot-taken' ? 409 : 502, {
|
|
1780
|
-
...(result && typeof result === 'object' ? result : {}),
|
|
1781
|
-
ok: false,
|
|
1782
|
-
error,
|
|
1783
|
-
slotNumber: state.slotNumber
|
|
1784
|
-
});
|
|
1785
|
-
return;
|
|
1786
|
-
}
|
|
1787
|
-
state.slotNumber = slotNumber;
|
|
1788
|
-
state.message = `Slot ${String(slotNumber).padStart(3, '0')} saved. Reconnecting this Client.`;
|
|
1789
|
-
runtime.emit('client.slot.assigned', { deviceId, slotNumber });
|
|
1790
|
-
respondJson(200, { ...getState(), ...result, ok: true, slotNumber });
|
|
1791
|
-
} catch (error) {
|
|
1792
|
-
const message = normalizeString(error?.message || error, 240) || 'slot-assignment-failed';
|
|
1793
|
-
runtime.emit('client.slot.assignment-failed', { deviceId, slotNumber, error: message });
|
|
1794
|
-
respondJson(502, { ok: false, error: message, slotNumber: state.slotNumber });
|
|
1795
|
-
}
|
|
1796
|
-
return;
|
|
1797
|
-
}
|
|
1798
|
-
if (pathname === '/api/client/video-acceleration/install' && req.method === 'POST') {
|
|
1799
|
-
if (process.platform !== 'linux') {
|
|
1800
|
-
respondJson(409, { ok: false, error: 'linux-video-acceleration-only' });
|
|
1801
|
-
return;
|
|
1802
|
-
}
|
|
1803
|
-
if (state.videoAcceleration?.busy) {
|
|
1804
|
-
respondJson(409, { ok: false, error: 'video-acceleration-install-in-progress', ...getState() });
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
if (typeof options.installVideoAcceleration !== 'function') {
|
|
1808
|
-
respondJson(409, { ok: false, error: 'video-acceleration-install-unavailable' });
|
|
1809
|
-
return;
|
|
1810
|
-
}
|
|
1811
|
-
state.videoAcceleration = {
|
|
1812
|
-
...(state.videoAcceleration || {}),
|
|
1813
|
-
supported: true,
|
|
1814
|
-
state: 'installing',
|
|
1815
|
-
busy: true,
|
|
1816
|
-
ready: false,
|
|
1817
|
-
error: '',
|
|
1818
|
-
message: 'Installing Linux hardware video support…'
|
|
1819
|
-
};
|
|
1820
|
-
runtime.emit('client.video-acceleration.installing', {
|
|
1821
|
-
packageManager: normalizeString(state.videoAcceleration.packageManager?.id, 40)
|
|
1822
|
-
});
|
|
1823
|
-
try {
|
|
1824
|
-
const result = await options.installVideoAcceleration();
|
|
1825
|
-
state.videoAcceleration = {
|
|
1826
|
-
...(result && typeof result === 'object' ? result : {}),
|
|
1827
|
-
busy: false
|
|
1828
|
-
};
|
|
1829
|
-
const ok = state.videoAcceleration.ready === true;
|
|
1830
|
-
runtime.emit(ok ? 'client.video-acceleration.ready' : 'client.video-acceleration.failed', {
|
|
1831
|
-
encoder: normalizeString(state.videoAcceleration.encoder, 80),
|
|
1832
|
-
driver: normalizeString(state.videoAcceleration.driver, 80),
|
|
1833
|
-
error: normalizeString(state.videoAcceleration.error, 240)
|
|
1834
|
-
});
|
|
1835
|
-
respondJson(ok ? 200 : 409, { ...getState(), ok });
|
|
1836
|
-
if (ok && state.videoAcceleration.restartAgent) {
|
|
1837
|
-
setTimeout(() => options.onVideoAccelerationReady?.(state.videoAcceleration), 150);
|
|
1838
|
-
}
|
|
1839
|
-
} catch (error) {
|
|
1840
|
-
state.videoAcceleration = {
|
|
1841
|
-
...(state.videoAcceleration || {}),
|
|
1842
|
-
state: 'error',
|
|
1843
|
-
busy: false,
|
|
1844
|
-
ready: false,
|
|
1845
|
-
error: normalizeString(error?.message || error, 320)
|
|
1846
|
-
};
|
|
1847
|
-
runtime.emit('client.video-acceleration.failed', { error: state.videoAcceleration.error });
|
|
1848
|
-
respondJson(409, { ok: false, ...getState(), error: state.videoAcceleration.error });
|
|
1849
|
-
}
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
if (pathname === '/api/runtime/switch-role' && req.method === 'POST') {
|
|
1666
|
+
if (pathname === '/api/runtime/shutdown' && req.method === 'POST') {
|
|
1667
|
+
runtime.emit('runtime.shutdown.requested', { role: 'client' });
|
|
1668
|
+
respondJson(200, { ok: true, shuttingDown: true, role: 'client' });
|
|
1669
|
+
setTimeout(() => options.onShutdown?.(), 150);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (pathname === '/api/client/slot' && req.method === 'POST') {
|
|
1673
|
+
const body = parseJsonBody(await readBody(req));
|
|
1674
|
+
const slotNumber = normalizeSlotNumber(body.slotNumber ?? body.slot);
|
|
1675
|
+
if (!slotNumber) {
|
|
1676
|
+
respondJson(400, { ok: false, error: 'invalid-slot-number', slotNumber: state.slotNumber });
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
if (!state.manager || !state.pairToken || state.agent.state !== 'running') {
|
|
1680
|
+
respondJson(409, { ok: false, error: 'hub-not-connected', slotNumber: state.slotNumber });
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (typeof options.assignSlot !== 'function') {
|
|
1684
|
+
respondJson(409, { ok: false, error: 'slot-assignment-unavailable', slotNumber: state.slotNumber });
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
runtime.emit('client.slot.assignment-requested', {
|
|
1688
|
+
deviceId,
|
|
1689
|
+
previousSlotNumber: state.slotNumber,
|
|
1690
|
+
slotNumber
|
|
1691
|
+
});
|
|
1692
|
+
try {
|
|
1693
|
+
const result = await options.assignSlot({
|
|
1694
|
+
manager: state.manager,
|
|
1695
|
+
pairToken: state.pairToken,
|
|
1696
|
+
deviceId,
|
|
1697
|
+
slotNumber
|
|
1698
|
+
});
|
|
1699
|
+
if (result?.ok !== true) {
|
|
1700
|
+
const error = normalizeString(result?.error, 120) || 'slot-assignment-failed';
|
|
1701
|
+
runtime.emit('client.slot.assignment-rejected', {
|
|
1702
|
+
deviceId,
|
|
1703
|
+
slotNumber,
|
|
1704
|
+
error,
|
|
1705
|
+
conflictDeviceId: normalizeString(result?.conflict?.deviceId, 160)
|
|
1706
|
+
});
|
|
1707
|
+
respondJson(error === 'slot-taken' ? 409 : 502, {
|
|
1708
|
+
...(result && typeof result === 'object' ? result : {}),
|
|
1709
|
+
ok: false,
|
|
1710
|
+
error,
|
|
1711
|
+
slotNumber: state.slotNumber
|
|
1712
|
+
});
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
state.slotNumber = slotNumber;
|
|
1716
|
+
state.message = `Slot ${String(slotNumber).padStart(3, '0')} saved. Reconnecting this Client.`;
|
|
1717
|
+
runtime.emit('client.slot.assigned', { deviceId, slotNumber });
|
|
1718
|
+
respondJson(200, { ...getState(), ...result, ok: true, slotNumber });
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
const message = normalizeString(error?.message || error, 240) || 'slot-assignment-failed';
|
|
1721
|
+
runtime.emit('client.slot.assignment-failed', { deviceId, slotNumber, error: message });
|
|
1722
|
+
respondJson(502, { ok: false, error: message, slotNumber: state.slotNumber });
|
|
1723
|
+
}
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
if (pathname === '/api/client/video-acceleration/install' && req.method === 'POST') {
|
|
1727
|
+
if (process.platform !== 'linux') {
|
|
1728
|
+
respondJson(409, { ok: false, error: 'linux-video-acceleration-only' });
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (state.videoAcceleration?.busy) {
|
|
1732
|
+
respondJson(409, { ok: false, error: 'video-acceleration-install-in-progress', ...getState() });
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (typeof options.installVideoAcceleration !== 'function') {
|
|
1736
|
+
respondJson(409, { ok: false, error: 'video-acceleration-install-unavailable' });
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
state.videoAcceleration = {
|
|
1740
|
+
...(state.videoAcceleration || {}),
|
|
1741
|
+
supported: true,
|
|
1742
|
+
state: 'installing',
|
|
1743
|
+
busy: true,
|
|
1744
|
+
ready: false,
|
|
1745
|
+
error: '',
|
|
1746
|
+
message: 'Installing Linux hardware video support…'
|
|
1747
|
+
};
|
|
1748
|
+
runtime.emit('client.video-acceleration.installing', {
|
|
1749
|
+
packageManager: normalizeString(state.videoAcceleration.packageManager?.id, 40)
|
|
1750
|
+
});
|
|
1751
|
+
try {
|
|
1752
|
+
const result = await options.installVideoAcceleration();
|
|
1753
|
+
state.videoAcceleration = {
|
|
1754
|
+
...(result && typeof result === 'object' ? result : {}),
|
|
1755
|
+
busy: false
|
|
1756
|
+
};
|
|
1757
|
+
const ok = state.videoAcceleration.ready === true;
|
|
1758
|
+
runtime.emit(ok ? 'client.video-acceleration.ready' : 'client.video-acceleration.failed', {
|
|
1759
|
+
encoder: normalizeString(state.videoAcceleration.encoder, 80),
|
|
1760
|
+
driver: normalizeString(state.videoAcceleration.driver, 80),
|
|
1761
|
+
error: normalizeString(state.videoAcceleration.error, 240)
|
|
1762
|
+
});
|
|
1763
|
+
respondJson(ok ? 200 : 409, { ...getState(), ok });
|
|
1764
|
+
if (ok && state.videoAcceleration.restartAgent) {
|
|
1765
|
+
setTimeout(() => options.onVideoAccelerationReady?.(state.videoAcceleration), 150);
|
|
1766
|
+
}
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
state.videoAcceleration = {
|
|
1769
|
+
...(state.videoAcceleration || {}),
|
|
1770
|
+
state: 'error',
|
|
1771
|
+
busy: false,
|
|
1772
|
+
ready: false,
|
|
1773
|
+
error: normalizeString(error?.message || error, 320)
|
|
1774
|
+
};
|
|
1775
|
+
runtime.emit('client.video-acceleration.failed', { error: state.videoAcceleration.error });
|
|
1776
|
+
respondJson(409, { ok: false, ...getState(), error: state.videoAcceleration.error });
|
|
1777
|
+
}
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (pathname === '/api/runtime/switch-role' && req.method === 'POST') {
|
|
1853
1781
|
const body = parseJsonBody(await readBody(req));
|
|
1854
1782
|
if (String(body.role || '').trim().toLowerCase() !== 'hub') {
|
|
1855
|
-
respondJson(400, { ok: false, error: 'client-can-only-transition-to-hub' });
|
|
1783
|
+
respondJson(400, { ok: false, error: 'client-can-only-transition-to-hub' });
|
|
1856
1784
|
return;
|
|
1857
1785
|
}
|
|
1858
1786
|
try {
|
|
1859
|
-
const result = await options.changeRole?.(
|
|
1860
|
-
|
|
1861
|
-
runtime.getSnapshot(),
|
|
1862
|
-
lastChoice?.session || savedSession || null
|
|
1863
|
-
);
|
|
1864
|
-
respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
|
|
1787
|
+
const result = await options.changeRole?.('hub', runtime.getSnapshot());
|
|
1788
|
+
respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
|
|
1865
1789
|
} catch (error) {
|
|
1866
|
-
respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
|
|
1790
|
+
respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
|
|
1867
1791
|
}
|
|
1868
1792
|
return;
|
|
1869
1793
|
}
|
|
1870
1794
|
if (pathname === '/api/client/role' && req.method === 'POST') {
|
|
1871
1795
|
const body = parseJsonBody(await readBody(req));
|
|
1872
1796
|
if (String(body.role || '').trim().toLowerCase() !== 'hub') {
|
|
1873
|
-
respondJson(400, { ok: false, error: 'client-can-only-transition-to-hub' });
|
|
1797
|
+
respondJson(400, { ok: false, error: 'client-can-only-transition-to-hub' });
|
|
1874
1798
|
return;
|
|
1875
1799
|
}
|
|
1876
1800
|
try {
|
|
1877
|
-
const result = await options.changeRole?.(
|
|
1878
|
-
|
|
1879
|
-
runtime.getSnapshot(),
|
|
1880
|
-
lastChoice?.session || savedSession || null
|
|
1881
|
-
);
|
|
1882
|
-
respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
|
|
1801
|
+
const result = await options.changeRole?.('hub', runtime.getSnapshot());
|
|
1802
|
+
respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
|
|
1883
1803
|
} catch (error) {
|
|
1884
|
-
respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
|
|
1804
|
+
respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
|
|
1885
1805
|
}
|
|
1886
1806
|
return;
|
|
1887
|
-
}
|
|
1888
|
-
if (pathname === '/api/client/reconnect' && req.method === 'POST') {
|
|
1889
|
-
const body = parseJsonBody(await readBody(req));
|
|
1890
|
-
if (body.onlyIfWaiting === true && state.agent.state === 'running') {
|
|
1891
|
-
respondJson(200, { ok: true, skipped: true, reason: 'already-connected', ...getState() });
|
|
1892
|
-
return;
|
|
1893
|
-
}
|
|
1894
|
-
const previousManager = state.manager;
|
|
1895
|
-
state.manager = '';
|
|
1896
|
-
state.agent.state = 'reconnecting';
|
|
1897
|
-
state.lastError = '';
|
|
1898
|
-
state.message = 'Refreshing the LiveDesk Hub discovery now.';
|
|
1899
|
-
runtime.update({ state: RuntimeState.RESOLVING_ROLE }, 'client-reconnect-requested');
|
|
1900
|
-
runtime.emit('client.reconnect.requested', { deviceId, previousManager: previousManager || '' });
|
|
1901
|
-
await options.onReconnect?.();
|
|
1902
|
-
respondJson(200, { ok: true, ...getState() });
|
|
1807
|
+
}
|
|
1808
|
+
if (pathname === '/api/client/reconnect' && req.method === 'POST') {
|
|
1809
|
+
const body = parseJsonBody(await readBody(req));
|
|
1810
|
+
if (body.onlyIfWaiting === true && state.agent.state === 'running') {
|
|
1811
|
+
respondJson(200, { ok: true, skipped: true, reason: 'already-connected', ...getState() });
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const previousManager = state.manager;
|
|
1815
|
+
state.manager = '';
|
|
1816
|
+
state.agent.state = 'reconnecting';
|
|
1817
|
+
state.lastError = '';
|
|
1818
|
+
state.message = 'Refreshing the LiveDesk Hub discovery now.';
|
|
1819
|
+
runtime.update({ state: RuntimeState.RESOLVING_ROLE }, 'client-reconnect-requested');
|
|
1820
|
+
runtime.emit('client.reconnect.requested', { deviceId, previousManager: previousManager || '' });
|
|
1821
|
+
await options.onReconnect?.();
|
|
1822
|
+
respondJson(200, { ok: true, ...getState() });
|
|
1903
1823
|
return;
|
|
1904
1824
|
}
|
|
1905
1825
|
if (pathname === '/api/client/disconnect' && req.method === 'POST') {
|
|
1906
1826
|
await options.onDisconnect?.();
|
|
1907
1827
|
state.manager = '';
|
|
1908
1828
|
state.agent.state = 'waiting';
|
|
1909
|
-
respondJson(200, { ok: true, ...getState() });
|
|
1829
|
+
respondJson(200, { ok: true, ...getState() });
|
|
1910
1830
|
return;
|
|
1911
1831
|
}
|
|
1912
1832
|
if (pathname === '/api/client/pin' && req.method === 'POST') {
|
|
@@ -1917,181 +1837,181 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1917
1837
|
if (!resolved) throw new Error('pin-not-resolved');
|
|
1918
1838
|
state.message = 'PIN accepted. Starting the Client.';
|
|
1919
1839
|
complete({ type: 'pin', ...resolved }, state.message);
|
|
1920
|
-
respondJson(200, { ok: true, ...getState() });
|
|
1840
|
+
respondJson(200, { ok: true, ...getState() });
|
|
1921
1841
|
} catch (error) {
|
|
1922
|
-
respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
|
|
1842
|
+
respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
|
|
1923
1843
|
}
|
|
1924
1844
|
return;
|
|
1925
1845
|
}
|
|
1926
1846
|
if (pathname.startsWith('/api/')) {
|
|
1927
|
-
respondJson(409, runtimeRoleError('client', 'client'));
|
|
1847
|
+
respondJson(409, runtimeRoleError('client', 'client'));
|
|
1928
1848
|
return;
|
|
1929
1849
|
}
|
|
1930
1850
|
serveApp(req, res, pathname);
|
|
1931
1851
|
};
|
|
1932
1852
|
|
|
1933
|
-
const start = () => new Promise((resolveStart, rejectStart) => {
|
|
1934
|
-
if (process.env.LIVEDESK_DESKTOP_HOST === '1') clearSavedSession();
|
|
1935
|
-
let settled = false;
|
|
1936
|
-
server = createServer((req, res) => {
|
|
1853
|
+
const start = () => new Promise((resolveStart, rejectStart) => {
|
|
1854
|
+
if (process.env.LIVEDESK_DESKTOP_HOST === '1') clearSavedSession();
|
|
1855
|
+
let settled = false;
|
|
1856
|
+
server = createServer((req, res) => {
|
|
1937
1857
|
void handleRequest(req, res).catch(error => {
|
|
1938
|
-
if (!res.headersSent) sendJson(res, 500, { ok: false, error: normalizeString(error?.message || error) }, req.headers.origin, port, trustedWebOrigins);
|
|
1858
|
+
if (!res.headersSent) sendJson(res, 500, { ok: false, error: normalizeString(error?.message || error) }, req.headers.origin, port, trustedWebOrigins);
|
|
1939
1859
|
else res.end();
|
|
1940
|
-
});
|
|
1941
|
-
});
|
|
1942
|
-
const bindTimer = setTimeout(() => {
|
|
1943
|
-
if (settled) return;
|
|
1944
|
-
settled = true;
|
|
1945
|
-
try { server.close(); } catch { /* bind never completed */ }
|
|
1946
|
-
rejectStart(new Error(`client-runtime-bind-timeout:${host}:${port}`));
|
|
1947
|
-
}, CLIENT_RUNTIME_BIND_TIMEOUT_MS);
|
|
1948
|
-
bindTimer.unref?.();
|
|
1949
|
-
server.once('error', error => {
|
|
1950
|
-
if (settled) return;
|
|
1951
|
-
settled = true;
|
|
1952
|
-
clearTimeout(bindTimer);
|
|
1953
|
-
rejectStart(error);
|
|
1954
|
-
});
|
|
1955
|
-
server.listen(port, host, () => {
|
|
1956
|
-
if (settled) {
|
|
1957
|
-
try { server.close(); } catch { /* timed-out bind already owns cleanup */ }
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
settled = true;
|
|
1961
|
-
clearTimeout(bindTimer);
|
|
1962
|
-
runtime.emit('runtime.http.started', { host, port });
|
|
1963
|
-
if (initialChoice) {
|
|
1964
|
-
setImmediate(() => {
|
|
1965
|
-
complete(initialChoice, initialChoiceMessage);
|
|
1966
|
-
});
|
|
1967
|
-
} else {
|
|
1968
|
-
const saved = normalizeRuntimeAuthSession(savedSession, { requireRefreshToken: true });
|
|
1969
|
-
if (saved.ok) {
|
|
1970
|
-
setImmediate(() => {
|
|
1971
|
-
void (async () => {
|
|
1972
|
-
try {
|
|
1973
|
-
// Older persisted sessions kept only id/email on `user`, while
|
|
1974
|
-
// Supabase still carries Google name/avatar claims in the
|
|
1975
|
-
// access-token payload. Recover those display-only claims
|
|
1976
|
-
// locally before any best-effort network hydration.
|
|
1977
|
-
const normalizedSession = mergeClientAccountProfile(saved.session, savedSession);
|
|
1978
|
-
const hydratedSession = await hydrateClientAccountProfile(normalizedSession);
|
|
1979
|
-
if (!writeSavedSession(hydratedSession)) throw new Error('refresh-token-required');
|
|
1980
|
-
state.auth.persisted = true;
|
|
1981
|
-
complete({ type: 'google', session: hydratedSession }, 'Saved sign-in found. Finding the LiveDesk Hub.');
|
|
1982
|
-
} catch (error) {
|
|
1983
|
-
recordAuthAttempt('rejected', `session-persistence-failed:${normalizeString(error?.message || error, 160)}`);
|
|
1984
|
-
}
|
|
1985
|
-
})();
|
|
1986
|
-
});
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
resolveStart();
|
|
1990
|
-
});
|
|
1991
|
-
});
|
|
1992
|
-
|
|
1993
|
-
const closeHttpServer = timeoutMs => new Promise(resolveClose => {
|
|
1994
|
-
if (!server) {
|
|
1995
|
-
resolveClose({ ok: true, listening: false });
|
|
1996
|
-
return;
|
|
1997
|
-
}
|
|
1998
|
-
let settled = false;
|
|
1999
|
-
const finish = ok => {
|
|
2000
|
-
if (settled) return;
|
|
2001
|
-
settled = true;
|
|
2002
|
-
clearTimeout(timer);
|
|
2003
|
-
resolveClose({ ok, listening: Boolean(server?.listening) });
|
|
2004
|
-
};
|
|
2005
|
-
const timer = setTimeout(() => {
|
|
2006
|
-
try { server.closeAllConnections?.(); } catch { /* exact HTTP owner already closed */ }
|
|
2007
|
-
finish(!server.listening);
|
|
2008
|
-
}, Math.max(100, Number(timeoutMs) || CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS));
|
|
2009
|
-
try {
|
|
2010
|
-
server.close(error => finish(!error));
|
|
2011
|
-
server.closeIdleConnections?.();
|
|
2012
|
-
} catch {
|
|
2013
|
-
finish(true);
|
|
2014
|
-
}
|
|
2015
|
-
});
|
|
2016
|
-
|
|
2017
|
-
const closeRuntime = async () => {
|
|
2018
|
-
const closeStartedAt = Date.now();
|
|
2019
|
-
closed = true;
|
|
2020
|
-
runtime.emit('runtime.http.stopping', { port });
|
|
2021
|
-
|
|
2022
|
-
const taskOwners = [...diagnosticTasks];
|
|
2023
|
-
const [diagnosticClose, httpClose] = await Promise.all([
|
|
2024
|
-
diagnosticCommandOwner.close(CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS),
|
|
2025
|
-
closeHttpServer(CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS)
|
|
2026
|
-
]);
|
|
2027
|
-
if (taskOwners.length > 0) {
|
|
2028
|
-
const remainingCloseMs = Math.max(
|
|
2029
|
-
0,
|
|
2030
|
-
CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS - (Date.now() - closeStartedAt)
|
|
2031
|
-
);
|
|
2032
|
-
if (remainingCloseMs > 0) {
|
|
2033
|
-
await Promise.race([
|
|
2034
|
-
Promise.allSettled(taskOwners),
|
|
2035
|
-
new Promise(resolve => setTimeout(resolve, remainingCloseMs))
|
|
2036
|
-
]);
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
const finalDiagnostics = diagnosticCommandOwner.getSnapshot();
|
|
2040
|
-
const result = {
|
|
2041
|
-
ok: diagnosticClose.ok === true
|
|
2042
|
-
&& httpClose.ok === true
|
|
2043
|
-
&& diagnosticTasks.size === 0
|
|
2044
|
-
&& finalDiagnostics.activeCount === 0
|
|
2045
|
-
&& finalDiagnostics.redirectedStreamCount === 0,
|
|
2046
|
-
diagnostics: {
|
|
2047
|
-
...diagnosticClose,
|
|
2048
|
-
taskCount: diagnosticTasks.size
|
|
2049
|
-
},
|
|
2050
|
-
http: httpClose
|
|
2051
|
-
};
|
|
2052
|
-
runtime.emit(
|
|
2053
|
-
result.ok ? 'client.diagnostics.closed' : 'client.diagnostics.close-timeout',
|
|
2054
|
-
{
|
|
2055
|
-
ownerId: finalDiagnostics.ownerId,
|
|
2056
|
-
activeCount: finalDiagnostics.activeCount,
|
|
2057
|
-
redirectedStreamCount: finalDiagnostics.redirectedStreamCount,
|
|
2058
|
-
taskCount: diagnosticTasks.size
|
|
2059
|
-
}
|
|
2060
|
-
);
|
|
2061
|
-
return result;
|
|
2062
|
-
};
|
|
2063
|
-
|
|
2064
|
-
return {
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
const bindTimer = setTimeout(() => {
|
|
1863
|
+
if (settled) return;
|
|
1864
|
+
settled = true;
|
|
1865
|
+
try { server.close(); } catch { /* bind never completed */ }
|
|
1866
|
+
rejectStart(new Error(`client-runtime-bind-timeout:${host}:${port}`));
|
|
1867
|
+
}, CLIENT_RUNTIME_BIND_TIMEOUT_MS);
|
|
1868
|
+
bindTimer.unref?.();
|
|
1869
|
+
server.once('error', error => {
|
|
1870
|
+
if (settled) return;
|
|
1871
|
+
settled = true;
|
|
1872
|
+
clearTimeout(bindTimer);
|
|
1873
|
+
rejectStart(error);
|
|
1874
|
+
});
|
|
1875
|
+
server.listen(port, host, () => {
|
|
1876
|
+
if (settled) {
|
|
1877
|
+
try { server.close(); } catch { /* timed-out bind already owns cleanup */ }
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
settled = true;
|
|
1881
|
+
clearTimeout(bindTimer);
|
|
1882
|
+
runtime.emit('runtime.http.started', { host, port });
|
|
1883
|
+
if (initialChoice) {
|
|
1884
|
+
setImmediate(() => {
|
|
1885
|
+
complete(initialChoice, initialChoiceMessage);
|
|
1886
|
+
});
|
|
1887
|
+
} else {
|
|
1888
|
+
const saved = normalizeRuntimeAuthSession(savedSession, { requireRefreshToken: true });
|
|
1889
|
+
if (saved.ok) {
|
|
1890
|
+
setImmediate(() => {
|
|
1891
|
+
void (async () => {
|
|
1892
|
+
try {
|
|
1893
|
+
// Older persisted sessions kept only id/email on `user`, while
|
|
1894
|
+
// Supabase still carries Google name/avatar claims in the
|
|
1895
|
+
// access-token payload. Recover those display-only claims
|
|
1896
|
+
// locally before any best-effort network hydration.
|
|
1897
|
+
const normalizedSession = mergeClientAccountProfile(saved.session, savedSession);
|
|
1898
|
+
const hydratedSession = await hydrateClientAccountProfile(normalizedSession);
|
|
1899
|
+
if (!writeSavedSession(hydratedSession)) throw new Error('refresh-token-required');
|
|
1900
|
+
state.auth.persisted = true;
|
|
1901
|
+
complete({ type: 'google', session: hydratedSession }, 'Saved sign-in found. Finding the LiveDesk Hub.');
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
recordAuthAttempt('rejected', `session-persistence-failed:${normalizeString(error?.message || error, 160)}`);
|
|
1904
|
+
}
|
|
1905
|
+
})();
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
resolveStart();
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
const closeHttpServer = timeoutMs => new Promise(resolveClose => {
|
|
1914
|
+
if (!server) {
|
|
1915
|
+
resolveClose({ ok: true, listening: false });
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
let settled = false;
|
|
1919
|
+
const finish = ok => {
|
|
1920
|
+
if (settled) return;
|
|
1921
|
+
settled = true;
|
|
1922
|
+
clearTimeout(timer);
|
|
1923
|
+
resolveClose({ ok, listening: Boolean(server?.listening) });
|
|
1924
|
+
};
|
|
1925
|
+
const timer = setTimeout(() => {
|
|
1926
|
+
try { server.closeAllConnections?.(); } catch { /* exact HTTP owner already closed */ }
|
|
1927
|
+
finish(!server.listening);
|
|
1928
|
+
}, Math.max(100, Number(timeoutMs) || CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS));
|
|
1929
|
+
try {
|
|
1930
|
+
server.close(error => finish(!error));
|
|
1931
|
+
server.closeIdleConnections?.();
|
|
1932
|
+
} catch {
|
|
1933
|
+
finish(true);
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
const closeRuntime = async () => {
|
|
1938
|
+
const closeStartedAt = Date.now();
|
|
1939
|
+
closed = true;
|
|
1940
|
+
runtime.emit('runtime.http.stopping', { port });
|
|
1941
|
+
|
|
1942
|
+
const taskOwners = [...diagnosticTasks];
|
|
1943
|
+
const [diagnosticClose, httpClose] = await Promise.all([
|
|
1944
|
+
diagnosticCommandOwner.close(CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS),
|
|
1945
|
+
closeHttpServer(CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS)
|
|
1946
|
+
]);
|
|
1947
|
+
if (taskOwners.length > 0) {
|
|
1948
|
+
const remainingCloseMs = Math.max(
|
|
1949
|
+
0,
|
|
1950
|
+
CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS - (Date.now() - closeStartedAt)
|
|
1951
|
+
);
|
|
1952
|
+
if (remainingCloseMs > 0) {
|
|
1953
|
+
await Promise.race([
|
|
1954
|
+
Promise.allSettled(taskOwners),
|
|
1955
|
+
new Promise(resolve => setTimeout(resolve, remainingCloseMs))
|
|
1956
|
+
]);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
const finalDiagnostics = diagnosticCommandOwner.getSnapshot();
|
|
1960
|
+
const result = {
|
|
1961
|
+
ok: diagnosticClose.ok === true
|
|
1962
|
+
&& httpClose.ok === true
|
|
1963
|
+
&& diagnosticTasks.size === 0
|
|
1964
|
+
&& finalDiagnostics.activeCount === 0
|
|
1965
|
+
&& finalDiagnostics.redirectedStreamCount === 0,
|
|
1966
|
+
diagnostics: {
|
|
1967
|
+
...diagnosticClose,
|
|
1968
|
+
taskCount: diagnosticTasks.size
|
|
1969
|
+
},
|
|
1970
|
+
http: httpClose
|
|
1971
|
+
};
|
|
1972
|
+
runtime.emit(
|
|
1973
|
+
result.ok ? 'client.diagnostics.closed' : 'client.diagnostics.close-timeout',
|
|
1974
|
+
{
|
|
1975
|
+
ownerId: finalDiagnostics.ownerId,
|
|
1976
|
+
activeCount: finalDiagnostics.activeCount,
|
|
1977
|
+
redirectedStreamCount: finalDiagnostics.redirectedStreamCount,
|
|
1978
|
+
taskCount: diagnosticTasks.size
|
|
1979
|
+
}
|
|
1980
|
+
);
|
|
1981
|
+
return result;
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
return {
|
|
2065
1985
|
runtime,
|
|
2066
|
-
waitForChoice,
|
|
2067
|
-
start,
|
|
2068
|
-
get url() { return `http://${host}:${port}/`; },
|
|
2069
|
-
getLastChoice() { return lastChoice; },
|
|
2070
|
-
acceptChoice(choice, message) {
|
|
2071
|
-
complete(choice, message);
|
|
2072
|
-
return lastChoice;
|
|
2073
|
-
},
|
|
2074
|
-
update(patch = {}) {
|
|
1986
|
+
waitForChoice,
|
|
1987
|
+
start,
|
|
1988
|
+
get url() { return `http://${host}:${port}/`; },
|
|
1989
|
+
getLastChoice() { return lastChoice; },
|
|
1990
|
+
acceptChoice(choice, message) {
|
|
1991
|
+
complete(choice, message);
|
|
1992
|
+
return lastChoice;
|
|
1993
|
+
},
|
|
1994
|
+
update(patch = {}) {
|
|
2075
1995
|
Object.assign(state, patch);
|
|
2076
1996
|
if (patch.agent && typeof patch.agent === 'object') {
|
|
2077
1997
|
state.agent = { ...state.agent, ...patch.agent };
|
|
2078
1998
|
runtime.setAgentPid(Number.isInteger(patch.agent.pid) ? patch.agent.pid : runtime.getSnapshot().agentPid);
|
|
2079
1999
|
if (patch.agent.state === 'running') runtime.update({ state: RuntimeState.RUNNING }, 'agent-running');
|
|
2080
2000
|
}
|
|
2081
|
-
if (patch.manager) state.manager = normalizeString(patch.manager, 256);
|
|
2082
|
-
if (patch.pairToken) state.pairToken = normalizeString(patch.pairToken, 256);
|
|
2083
|
-
if (normalizeSlotNumber(patch.slotNumber)) state.slotNumber = normalizeSlotNumber(patch.slotNumber);
|
|
2084
|
-
if (Object.prototype.hasOwnProperty.call(patch, 'assignedHubId')) {
|
|
2085
|
-
state.assignedHubId = normalizeString(patch.assignedHubId, 160);
|
|
2086
|
-
}
|
|
2001
|
+
if (patch.manager) state.manager = normalizeString(patch.manager, 256);
|
|
2002
|
+
if (patch.pairToken) state.pairToken = normalizeString(patch.pairToken, 256);
|
|
2003
|
+
if (normalizeSlotNumber(patch.slotNumber)) state.slotNumber = normalizeSlotNumber(patch.slotNumber);
|
|
2004
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'assignedHubId')) {
|
|
2005
|
+
state.assignedHubId = normalizeString(patch.assignedHubId, 160);
|
|
2006
|
+
}
|
|
2087
2007
|
if (patch.message) state.message = normalizeString(patch.message, 1000);
|
|
2088
2008
|
if (Number.isInteger(patch.roleVersion)) state.roleVersion = patch.roleVersion;
|
|
2089
2009
|
if (Array.isArray(patch.endpointCandidates)) state.endpointCandidates = patch.endpointCandidates;
|
|
2090
2010
|
},
|
|
2091
|
-
close() {
|
|
2092
|
-
if (!runtimeClosePromise) runtimeClosePromise = closeRuntime();
|
|
2093
|
-
return runtimeClosePromise;
|
|
2094
|
-
},
|
|
2011
|
+
close() {
|
|
2012
|
+
if (!runtimeClosePromise) runtimeClosePromise = closeRuntime();
|
|
2013
|
+
return runtimeClosePromise;
|
|
2014
|
+
},
|
|
2095
2015
|
getRoleRestartRequest() { return options.getRoleRestartRequest?.() || null; }
|
|
2096
2016
|
};
|
|
2097
2017
|
}
|