livedesk 0.1.126 → 0.1.128

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/README.md CHANGED
@@ -10,6 +10,8 @@ npx -y livedesk hub
10
10
  ```
11
11
 
12
12
  This starts the LiveDesk Hub, opens the local screen wall, and accepts clients.
13
+ The Hub UI/API listens on `127.0.0.1` by default while the client endpoint
14
+ continues to listen on the LAN.
13
15
 
14
16
  ## Client
15
17
 
@@ -22,6 +24,14 @@ The client signs in with Google, discovers the active Hub, and connects to the w
22
24
  On Windows, enable **Start with Windows** on the connection page to reconnect
23
25
  automatically after reboot.
24
26
 
27
+ ## Plans
28
+
29
+ - Free: 5 personal devices with a standard wall banner ad.
30
+ - Plus LTD launch: 30 personal devices, no ads, USD 79 one-time.
31
+ - Pro LTD launch: commercial use and larger walls, no ads, USD 199 one-time.
32
+
33
+ Monthly and yearly subscriptions are planned after the LTD launch.
34
+
25
35
  ## Free ad slot
26
36
 
27
37
  Free accounts show a standard bottom ad slot. Set these Vite build variables
package/bin/livedesk.js CHANGED
@@ -34,7 +34,7 @@ Commands:
34
34
  Hub options:
35
35
  --no-open Do not open the browser automatically.
36
36
  --url <url> Browser URL to open. Default: ${DEFAULT_MANAGER_URL}
37
- --host <host> Hub HTTP host. Default: 0.0.0.0
37
+ --host <host> Hub HTTP host. Default: 127.0.0.1
38
38
  --port <port> Hub HTTP port. Default: 5179
39
39
  --remote-port <port>
40
40
  Client connection port. Default: 5197
@@ -304,7 +304,7 @@ async function runManager(args) {
304
304
  const packagedWebDist = resolve(packageRoot, 'web', 'dist');
305
305
  const env = {
306
306
  ...process.env,
307
- LIVEDESK_HUB_HTTP_HOST: options.host || process.env.LIVEDESK_HUB_HTTP_HOST || '0.0.0.0',
307
+ LIVEDESK_HUB_HTTP_HOST: options.host || process.env.LIVEDESK_HUB_HTTP_HOST || '127.0.0.1',
308
308
  LIVEDESK_HUB_HTTP_PORT: String(httpPort),
309
309
  REMOTE_HUB_PORT: String(remotePort),
310
310
  REMOTE_HUB_PAIR_TOKEN: pairToken,
@@ -17,11 +17,11 @@ const MAX_AGENT_TASK_CHARS = 4000;
17
17
  const MAX_AGENT_TASK_RESULT_CHARS = 3000;
18
18
  const RECENT_TASK_LIMIT = 12;
19
19
  const RECENT_TASK_BATCH_LIMIT = 16;
20
- const RECENT_FRAME_CACHE_TTL_MS = 12000;
21
- const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 4;
22
- const RECENT_LIVE_FRAME_CACHE_LIMIT = 80;
23
- const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 3 * 1024 * 1024;
24
- const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 24 * 1024 * 1024;
20
+ const RECENT_FRAME_CACHE_TTL_MS = 4000;
21
+ const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 2;
22
+ const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
23
+ const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
24
+ const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 3 * 1024 * 1024;
25
25
  const LIVE_STREAM_PENDING_REUSE_MS = 5000;
26
26
  const LIVE_STREAM_MIN_FRESH_MS = 3000;
27
27
  const LIVE_STREAM_MAX_FRESH_MS = 12000;
@@ -2840,7 +2840,7 @@ export function createRemoteHub(options = {}) {
2840
2840
 
2841
2841
  function buildFramePayloadBuffer(framePayload, frameData) {
2842
2842
  if (Buffer.isBuffer(framePayload)) {
2843
- return Buffer.from(framePayload);
2843
+ return framePayload;
2844
2844
  }
2845
2845
 
2846
2846
  const raw = String(frameData || '');
@@ -2931,6 +2931,8 @@ export function createRemoteHub(options = {}) {
2931
2931
  durationUs: Number.isFinite(Number(message.durationUs)) ? Number(message.durationUs) : 0,
2932
2932
  sourceGapMs: Number.isFinite(Number(message.sourceGapMs)) ? Number(message.sourceGapMs) : 0,
2933
2933
  agentSendGapMs: Number.isFinite(Number(message.agentSendGapMs)) ? Number(message.agentSendGapMs) : 0,
2934
+ agentSendDurationMs: Number.isFinite(Number(message.agentSendDurationMs)) ? Number(message.agentSendDurationMs) : 0,
2935
+ agentSendDurationFrameSeq: Number.isFinite(Number(message.agentSendDurationFrameSeq)) ? Number(message.agentSendDurationFrameSeq) : 0,
2934
2936
  agentPaceMs: Number.isFinite(Number(message.agentPaceMs)) ? Number(message.agentPaceMs) : 0,
2935
2937
  droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
2936
2938
  hardwareEncoder: safeString(message.hardwareEncoder, 80),
@@ -3113,6 +3115,8 @@ export function createRemoteHub(options = {}) {
3113
3115
  durationUs: Number.isFinite(Number(message.durationUs)) ? Number(message.durationUs) : 0,
3114
3116
  sourceGapMs: Number.isFinite(Number(message.sourceGapMs)) ? Number(message.sourceGapMs) : 0,
3115
3117
  agentSendGapMs: Number.isFinite(Number(message.agentSendGapMs)) ? Number(message.agentSendGapMs) : 0,
3118
+ agentSendDurationMs: Number.isFinite(Number(message.agentSendDurationMs)) ? Number(message.agentSendDurationMs) : 0,
3119
+ agentSendDurationFrameSeq: Number.isFinite(Number(message.agentSendDurationFrameSeq)) ? Number(message.agentSendDurationFrameSeq) : 0,
3116
3120
  agentPaceMs: Number.isFinite(Number(message.agentPaceMs)) ? Number(message.agentPaceMs) : 0,
3117
3121
  droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
3118
3122
  hardwareEncoder: safeString(message.hardwareEncoder, 80),
package/hub/src/server.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import cors from 'cors';
4
3
  import express from 'express';
5
4
  import crypto from 'node:crypto';
6
5
  import { createServer } from 'node:http';
@@ -19,7 +18,7 @@ const webDistCandidates = [
19
18
  const webDistPath = webDistCandidates.find(candidate => existsSync(resolve(candidate, 'index.html'))) || webDistCandidates[webDistCandidates.length - 1];
20
19
  const webIndexPath = resolve(webDistPath, 'index.html');
21
20
  const packageInfo = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
22
- const httpHost = process.env.LIVEDESK_HUB_HTTP_HOST || '0.0.0.0';
21
+ const httpHost = process.env.LIVEDESK_HUB_HTTP_HOST || '127.0.0.1';
23
22
  const httpPort = Number(process.env.LIVEDESK_HUB_HTTP_PORT || process.env.PORT || 5179);
24
23
  const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 16 * 1024 * 1024);
25
24
  const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 12);
@@ -29,6 +28,23 @@ const frameClientsByDeviceId = new Map();
29
28
  const frameWildcardClients = new Set();
30
29
  const inputClients = new Set();
31
30
  const audioClients = new Set();
31
+ const FREE_DEVICE_LIMIT = 5;
32
+ const PLUS_DEVICE_LIMIT = 30;
33
+ const LICENSE_VERIFY_MAX_AGE_MS = 6 * 60 * 60 * 1000;
34
+ const supabaseUrl = String(process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngvjziawki.supabase.co').replace(/\/+$/, '');
35
+ const supabasePublishableKey = String(process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ');
36
+ const testLicensePlan = process.env.LIVEDESK_TEST_MODE === '1'
37
+ && ['ltd', 'pro'].includes(String(process.env.LIVEDESK_TEST_LICENSE_PLAN || '').toLowerCase())
38
+ ? String(process.env.LIVEDESK_TEST_LICENSE_PLAN).toLowerCase()
39
+ : '';
40
+ let connectedDeviceCount = 0;
41
+ let verifiedLicense = {
42
+ userId: '',
43
+ plan: 'free',
44
+ status: 'inactive',
45
+ expiresAt: '',
46
+ verifiedAt: 0
47
+ };
32
48
  let frameClientSeq = 0;
33
49
  let inputClientSeq = 0;
34
50
  let audioClientSeq = 0;
@@ -39,6 +55,9 @@ function readPositiveIntegerEnv(name, fallback) {
39
55
  }
40
56
 
41
57
  function handleRemoteHubEvent(type, event) {
58
+ if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
59
+ connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
60
+ }
42
61
  if (type !== 'RemoteDeviceConnected') {
43
62
  return;
44
63
  }
@@ -59,6 +78,96 @@ function handleRemoteHubEvent(type, event) {
59
78
  }
60
79
  }
61
80
 
81
+ function activeLicensePlan() {
82
+ if (testLicensePlan) {
83
+ return testLicensePlan;
84
+ }
85
+ const now = Date.now();
86
+ if (verifiedLicense.status !== 'active'
87
+ || now - verifiedLicense.verifiedAt > LICENSE_VERIFY_MAX_AGE_MS) {
88
+ return 'free';
89
+ }
90
+ const expiresAt = verifiedLicense.expiresAt ? Date.parse(verifiedLicense.expiresAt) : Number.POSITIVE_INFINITY;
91
+ if (Number.isFinite(expiresAt) && expiresAt <= now) {
92
+ return 'free';
93
+ }
94
+ return verifiedLicense.plan === 'pro' ? 'pro' : verifiedLicense.plan === 'ltd' ? 'ltd' : 'free';
95
+ }
96
+
97
+ function activeDeviceLimit() {
98
+ const plan = activeLicensePlan();
99
+ return plan === 'pro' ? Number.POSITIVE_INFINITY : plan === 'ltd' ? PLUS_DEVICE_LIMIT : FREE_DEVICE_LIMIT;
100
+ }
101
+
102
+ function hasHubFeatureAccess() {
103
+ return connectedDeviceCount <= activeDeviceLimit();
104
+ }
105
+
106
+ function licenseSnapshot() {
107
+ const plan = activeLicensePlan();
108
+ const limit = activeDeviceLimit();
109
+ return {
110
+ plan,
111
+ status: plan === 'free' ? 'free' : 'active',
112
+ deviceLimit: Number.isFinite(limit) ? limit : null,
113
+ connectedDeviceCount,
114
+ featureAccess: connectedDeviceCount <= limit,
115
+ verifiedAt: verifiedLicense.verifiedAt ? new Date(verifiedLicense.verifiedAt).toISOString() : '',
116
+ expiresAt: verifiedLicense.expiresAt || ''
117
+ };
118
+ }
119
+
120
+ async function syncVerifiedLicense(accessToken) {
121
+ const token = String(accessToken || '').trim();
122
+ if (!token) {
123
+ throw new Error('supabase-access-token-required');
124
+ }
125
+ const headers = {
126
+ apikey: supabasePublishableKey,
127
+ Authorization: `Bearer ${token}`,
128
+ Accept: 'application/json'
129
+ };
130
+ const userResponse = await fetch(`${supabaseUrl}/auth/v1/user`, { headers });
131
+ if (!userResponse.ok) {
132
+ throw new Error(`supabase-user-verification-failed:${userResponse.status}`);
133
+ }
134
+ const user = await userResponse.json();
135
+ const userId = String(user?.id || '').trim();
136
+ if (!userId) {
137
+ throw new Error('supabase-user-missing');
138
+ }
139
+ const query = new URLSearchParams({
140
+ select: 'user_id,product_key,plan,status,expires_at,updated_at',
141
+ user_id: `eq.${userId}`,
142
+ product_key: 'eq.livedesk',
143
+ limit: '1'
144
+ });
145
+ const entitlementResponse = await fetch(`${supabaseUrl}/rest/v1/livedesk_entitlements?${query}`, { headers });
146
+ if (!entitlementResponse.ok) {
147
+ throw new Error(`supabase-entitlement-query-failed:${entitlementResponse.status}`);
148
+ }
149
+ const rows = await entitlementResponse.json();
150
+ const entitlement = Array.isArray(rows) ? rows[0] : null;
151
+ const plan = entitlement?.plan === 'pro' ? 'pro' : entitlement?.plan === 'ltd' ? 'ltd' : 'free';
152
+ const status = entitlement?.status === 'active' ? 'active' : 'inactive';
153
+ verifiedLicense = {
154
+ userId,
155
+ plan,
156
+ status,
157
+ expiresAt: String(entitlement?.expires_at || ''),
158
+ verifiedAt: Date.now()
159
+ };
160
+ return licenseSnapshot();
161
+ }
162
+
163
+ function requireHubFeatureAccess(_req, res, next) {
164
+ if (hasHubFeatureAccess()) {
165
+ next();
166
+ return;
167
+ }
168
+ res.status(402).json({ ok: false, error: 'livedesk-plan-device-limit', license: licenseSnapshot() });
169
+ }
170
+
62
171
  const remoteHub = createRemoteHub({
63
172
  managerPackage: '@livedesk/hub',
64
173
  managerVersion: packageInfo.version,
@@ -80,20 +189,85 @@ const frameWss = new WebSocketServer({ noServer: true, perMessageDeflate: false
80
189
  const inputWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
81
190
  const audioWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
82
191
 
192
+ function normalizeHostname(value) {
193
+ return String(value || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
194
+ }
195
+
196
+ function isLoopbackHostname(value) {
197
+ const hostname = normalizeHostname(value);
198
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
199
+ }
200
+
201
+ function isPrivateHubHostname(value) {
202
+ const hostname = normalizeHostname(value);
203
+ if (isLoopbackHostname(hostname)
204
+ || hostname === '0.0.0.0'
205
+ || hostname.startsWith('10.')
206
+ || hostname.startsWith('192.168.')
207
+ || hostname.startsWith('169.254.')
208
+ || hostname.startsWith('fc')
209
+ || hostname.startsWith('fd')
210
+ || hostname.startsWith('fe80:')) {
211
+ return true;
212
+ }
213
+ const match = /^(172)\.(\d{1,3})\./.exec(hostname);
214
+ return !!match && Number(match[2]) >= 16 && Number(match[2]) <= 31;
215
+ }
216
+
217
+ function isLoopbackAddress(value) {
218
+ const address = String(value || '').trim().toLowerCase();
219
+ return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
220
+ }
221
+
222
+ function requestHostname(req) {
223
+ try {
224
+ return normalizeHostname(new URL(`http://${req.headers.host || '127.0.0.1'}`).hostname);
225
+ } catch {
226
+ return '';
227
+ }
228
+ }
229
+
230
+ function isTrustedBrowserRequest(req) {
231
+ const origin = String(req.headers.origin || '').trim();
232
+ if (!origin) {
233
+ const fetchSite = String(req.headers['sec-fetch-site'] || '').toLowerCase();
234
+ return isLoopbackAddress(req.socket?.remoteAddress)
235
+ || fetchSite === 'same-origin'
236
+ || fetchSite === 'none';
237
+ }
238
+ try {
239
+ const parsed = new URL(origin);
240
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
241
+ return false;
242
+ }
243
+ const originHost = normalizeHostname(parsed.hostname);
244
+ const targetHost = requestHostname(req);
245
+ return (originHost === targetHost && isPrivateHubHostname(targetHost))
246
+ || (isLoopbackHostname(originHost) && isLoopbackHostname(targetHost));
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+
83
252
  app.use((req, res, next) => {
84
- if (req.headers.origin) {
253
+ if (!isTrustedBrowserRequest(req)) {
254
+ res.status(403).json({ ok: false, error: 'untrusted-hub-origin' });
255
+ return;
256
+ }
257
+ const origin = String(req.headers.origin || '').trim();
258
+ if (origin) {
259
+ res.setHeader('Access-Control-Allow-Origin', origin);
85
260
  res.setHeader('Access-Control-Allow-Private-Network', 'true');
261
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
262
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
86
263
  res.setHeader('Vary', 'Origin, Access-Control-Request-Headers, Access-Control-Request-Private-Network');
87
264
  }
265
+ if (req.method === 'OPTIONS') {
266
+ res.status(204).end();
267
+ return;
268
+ }
88
269
  next();
89
270
  });
90
-
91
- app.use(cors({
92
- origin: true,
93
- credentials: false,
94
- methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
95
- allowedHeaders: ['Content-Type']
96
- }));
97
271
  app.use(express.json({ limit: '32mb' }));
98
272
 
99
273
  const MAX_FILE_TRANSFER_FILES = 24;
@@ -280,7 +454,8 @@ function ensureFrameClientSendLane(ws) {
280
454
  ws.liveDeskFrameSendLane = {
281
455
  queue: [],
282
456
  draining: false,
283
- dropped: 0
457
+ dropped: 0,
458
+ awaitingKeyFrames: new Set()
284
459
  };
285
460
  }
286
461
  return ws.liveDeskFrameSendLane;
@@ -291,6 +466,26 @@ function dropQueuedFrameForLane(lane) {
291
466
  const dropIndex = deltaIndex >= 0 ? deltaIndex : 0;
292
467
  const dropped = lane.queue.splice(dropIndex, 1);
293
468
  lane.dropped += dropped.length;
469
+ const droppedItem = dropped[0];
470
+ if (droppedItem?.isH264 && droppedItem.deviceId) {
471
+ lane.awaitingKeyFrames.add(droppedItem.deviceId);
472
+ let recoveryKeySeen = false;
473
+ lane.queue = lane.queue.filter(item => {
474
+ if (!item?.isH264 || item.deviceId !== droppedItem.deviceId) {
475
+ return true;
476
+ }
477
+ if (item.isKeyFrame) {
478
+ recoveryKeySeen = true;
479
+ lane.awaitingKeyFrames.delete(item.deviceId);
480
+ return true;
481
+ }
482
+ if (recoveryKeySeen) {
483
+ return true;
484
+ }
485
+ lane.dropped += 1;
486
+ return false;
487
+ });
488
+ }
294
489
  }
295
490
 
296
491
  function drainFrameClientSendLane(ws) {
@@ -332,6 +527,13 @@ function enqueueFramePacketForClient(ws, packet, meta) {
332
527
  return false;
333
528
  }
334
529
  const lane = ensureFrameClientSendLane(ws);
530
+ if (meta.isH264 && meta.isKeyFrame) {
531
+ lane.awaitingKeyFrames.delete(meta.deviceId);
532
+ } else if (meta.isH264 && lane.awaitingKeyFrames.has(meta.deviceId)) {
533
+ lane.dropped += 1;
534
+ ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
535
+ return false;
536
+ }
335
537
  if (ws.bufferedAmount > frameBackpressureBytes) {
336
538
  ws.liveDeskFrameBackpressured = true;
337
539
  ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
@@ -377,6 +579,10 @@ function updateFrameSubscription(ws, payload = {}) {
377
579
  }
378
580
 
379
581
  function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '', overrideLiveOptions = null) {
582
+ if (!hasHubFeatureAccess()) {
583
+ sendJson(ws, { type: 'RemoteFrameSubscriptionError', error: 'livedesk-plan-device-limit', license: licenseSnapshot() });
584
+ return;
585
+ }
380
586
  const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
381
587
  if (!ws.liveDeskAutoStart || subscribedIds.length === 0) {
382
588
  return;
@@ -503,6 +709,8 @@ function buildRemoteFrameBinaryPacket(frameEvent) {
503
709
  durationUs: Number(frame.durationUs || 0) || 0,
504
710
  sourceGapMs: Number(frame.sourceGapMs || 0) || 0,
505
711
  agentSendGapMs: Number(frame.agentSendGapMs || 0) || 0,
712
+ agentSendDurationMs: Number(frame.agentSendDurationMs || 0) || 0,
713
+ agentSendDurationFrameSeq: Number(frame.agentSendDurationFrameSeq || 0) || 0,
506
714
  agentPaceMs: Number(frame.agentPaceMs || 0) || 0,
507
715
  droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
508
716
  hardwareEncoder: frame.hardwareEncoder || '',
@@ -548,6 +756,9 @@ function buildRemoteAudioBinaryPacket(audioEvent) {
548
756
  }
549
757
 
550
758
  function broadcastRemoteBinaryFrame(frameEvent) {
759
+ if (!hasHubFeatureAccess()) {
760
+ return;
761
+ }
551
762
  const deviceId = String(frameEvent?.deviceId || frameEvent?.frame?.deviceId || '').trim();
552
763
  if (!deviceId || frameClients.size === 0) {
553
764
  return;
@@ -581,6 +792,9 @@ function broadcastRemoteBinaryFrame(frameEvent) {
581
792
  }
582
793
 
583
794
  function broadcastRemoteBinaryAudio(audioEvent) {
795
+ if (!hasHubFeatureAccess()) {
796
+ return;
797
+ }
584
798
  const deviceId = String(audioEvent?.deviceId || audioEvent?.frame?.deviceId || '').trim();
585
799
  if (!deviceId || audioClients.size === 0) {
586
800
  return;
@@ -639,13 +853,60 @@ app.get('/api/health', (_req, res) => {
639
853
 
640
854
  app.get('/api/remote/status', (_req, res) => {
641
855
  noStore(res);
856
+ const secretStatus = remoteHub.getStatus({ includeSecrets: true });
642
857
  res.json({
643
- ...remoteHub.getStatus({ includeSecrets: true }),
858
+ ...remoteHub.getStatus({ includeSecrets: false }),
859
+ pairingPin: secretStatus.pairingPin,
644
860
  product: 'LiveDesk',
645
861
  agentPackage: '@livedesk/client'
646
862
  });
647
863
  });
648
864
 
865
+ app.get('/api/remote/registry-credentials', (_req, res) => {
866
+ noStore(res);
867
+ const status = remoteHub.getStatus({ includeSecrets: true });
868
+ res.json({
869
+ pairToken: status.pairToken,
870
+ pairingPin: status.pairingPin,
871
+ pairingPinUpdatedAt: status.pairingPinUpdatedAt
872
+ });
873
+ });
874
+
875
+ app.get('/api/remote/license', (_req, res) => {
876
+ noStore(res);
877
+ res.json(licenseSnapshot());
878
+ });
879
+
880
+ app.post('/api/remote/license/sync', async (req, res) => {
881
+ noStore(res);
882
+ try {
883
+ const authorization = String(req.headers.authorization || '');
884
+ const accessToken = authorization.replace(/^Bearer\s+/i, '').trim();
885
+ res.json(await syncVerifiedLicense(accessToken));
886
+ } catch (err) {
887
+ verifiedLicense = {
888
+ userId: '',
889
+ plan: 'free',
890
+ status: 'inactive',
891
+ expiresAt: '',
892
+ verifiedAt: Date.now()
893
+ };
894
+ res.status(401).json({ ok: false, error: err instanceof Error ? err.message : String(err), license: licenseSnapshot() });
895
+ }
896
+ });
897
+
898
+ app.delete('/api/remote/license', (_req, res) => {
899
+ noStore(res);
900
+ verifiedLicense = {
901
+ userId: '',
902
+ plan: 'free',
903
+ status: 'inactive',
904
+ expiresAt: '',
905
+ verifiedAt: Date.now()
906
+ };
907
+ res.json(licenseSnapshot());
908
+ });
909
+
649
910
  app.post('/api/remote/pairing-pin', (_req, res) => {
650
911
  noStore(res);
651
912
  try {
@@ -736,12 +997,12 @@ app.post('/api/remote/devices/:deviceId/ping', (req, res) => {
736
997
  }));
737
998
  });
738
999
 
739
- app.post('/api/remote/devices/:deviceId/input', (req, res) => {
1000
+ app.post('/api/remote/devices/:deviceId/input', requireHubFeatureAccess, (req, res) => {
740
1001
  noStore(res);
741
1002
  res.json(remoteHub.sendInputControl(req.params.deviceId, req.body || {}));
742
1003
  });
743
1004
 
744
- app.post('/api/remote/files/transfer', (req, res) => {
1005
+ app.post('/api/remote/files/transfer', requireHubFeatureAccess, (req, res) => {
745
1006
  noStore(res);
746
1007
  const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
747
1008
  if (deviceIds.length === 0) {
@@ -782,7 +1043,7 @@ app.post('/api/remote/files/transfer', (req, res) => {
782
1043
  });
783
1044
  });
784
1045
 
785
- app.post('/api/remote/devices/:deviceId/tasks', (req, res) => {
1046
+ app.post('/api/remote/devices/:deviceId/tasks', requireHubFeatureAccess, (req, res) => {
786
1047
  noStore(res);
787
1048
  res.json(remoteHub.requestAgentTask(req.params.deviceId, {
788
1049
  instruction: req.body?.instruction,
@@ -794,7 +1055,7 @@ app.post('/api/remote/devices/:deviceId/tasks', (req, res) => {
794
1055
  }));
795
1056
  });
796
1057
 
797
- app.post('/api/remote/tasks', (req, res) => {
1058
+ app.post('/api/remote/tasks', requireHubFeatureAccess, (req, res) => {
798
1059
  noStore(res);
799
1060
  const requestedIds = normalizeDeviceIds(req.body?.deviceIds);
800
1061
  const approvalLevel = req.body?.approvalLevel === 'ai-assist' ? 'ai-assist' : 'task-only';
@@ -843,7 +1104,7 @@ app.post('/api/remote/devices/:deviceId/thumbnail/request', (req, res) => {
843
1104
  res.json(remoteHub.requestThumbnail(req.params.deviceId, req.body || {}));
844
1105
  });
845
1106
 
846
- app.get('/api/remote/devices/:deviceId/live/frame', (req, res) => {
1107
+ app.get('/api/remote/devices/:deviceId/live/frame', requireHubFeatureAccess, (req, res) => {
847
1108
  noStore(res);
848
1109
  if (wantsBinaryFrame(req)) {
849
1110
  sendFrameBinary(req, res, 'live');
@@ -859,7 +1120,7 @@ app.get('/api/remote/devices/:deviceId/live/frame', (req, res) => {
859
1120
  res.json({ ok: true, frame });
860
1121
  });
861
1122
 
862
- app.post('/api/remote/devices/:deviceId/live/start', (req, res) => {
1123
+ app.post('/api/remote/devices/:deviceId/live/start', requireHubFeatureAccess, (req, res) => {
863
1124
  noStore(res);
864
1125
  res.json(remoteHub.startLiveStream(req.params.deviceId, req.body || {}));
865
1126
  });
@@ -869,7 +1130,7 @@ app.post('/api/remote/devices/:deviceId/live/stop', (req, res) => {
869
1130
  res.json(remoteHub.stopLiveStream(req.params.deviceId, req.body || {}));
870
1131
  });
871
1132
 
872
- app.post('/api/remote/devices/:deviceId/audio/start', (req, res) => {
1133
+ app.post('/api/remote/devices/:deviceId/audio/start', requireHubFeatureAccess, (req, res) => {
873
1134
  noStore(res);
874
1135
  res.json(remoteHub.startAudioStream(req.params.deviceId, req.body || {}));
875
1136
  });
@@ -895,6 +1156,11 @@ if (existsSync(webIndexPath)) {
895
1156
  }
896
1157
 
897
1158
  httpServer.on('upgrade', (req, socket, head) => {
1159
+ if (!isTrustedBrowserRequest(req)) {
1160
+ socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
1161
+ socket.destroy();
1162
+ return;
1163
+ }
898
1164
  try {
899
1165
  const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
900
1166
  if (parsed.pathname === '/api/remote/frames/ws') {
@@ -990,6 +1256,10 @@ inputWss.on('connection', ws => {
990
1256
  inputClients.add(ws);
991
1257
  ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
992
1258
  ws.on('message', data => {
1259
+ if (!hasHubFeatureAccess()) {
1260
+ sendJson(ws, { type: 'RemoteInputError', error: 'livedesk-plan-device-limit', license: licenseSnapshot() });
1261
+ return;
1262
+ }
993
1263
  let payload;
994
1264
  try {
995
1265
  payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
@@ -1024,6 +1294,7 @@ inputWss.on('connection', ws => {
1024
1294
  });
1025
1295
 
1026
1296
  await remoteHub.start();
1297
+ connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
1027
1298
  httpServer.listen(httpPort, httpHost, () => {
1028
1299
  const status = remoteHub.getStatus({ includeSecrets: true });
1029
1300
  console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.126",
3
+ "version": "0.1.128",
4
4
  "description": "LiveDesk Hub and client launcher",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,7 @@
30
30
  "node": ">=20"
31
31
  },
32
32
  "dependencies": {
33
- "@livedesk/client": "0.1.86",
33
+ "@livedesk/client": "0.1.87",
34
34
  "cors": "^2.8.5",
35
35
  "express": "^4.21.2",
36
36
  "ws": "^8.18.3"
@@ -0,0 +1 @@
1
+ var b={exports:{}},n={};var V;function et(){if(V)return n;V=1;var f=Symbol.for("react.transitional.element"),p=Symbol.for("react.portal"),l=Symbol.for("react.fragment"),d=Symbol.for("react.strict_mode"),m=Symbol.for("react.profiler"),_=Symbol.for("react.consumer"),R=Symbol.for("react.context"),C=Symbol.for("react.forward_ref"),x=Symbol.for("react.suspense"),T=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),K=Symbol.for("react.activity"),H=Symbol.iterator;function G(t){return t===null||typeof t!="object"?null:(t=H&&t[H]||t["@@iterator"],typeof t=="function"?t:null)}var P={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},L=Object.assign,O={};function k(t,e,r){this.props=t,this.context=e,this.refs=O,this.updater=r||P}k.prototype.isReactComponent={},k.prototype.setState=function(t,e){if(typeof t!="object"&&typeof t!="function"&&t!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,t,e,"setState")},k.prototype.forceUpdate=function(t){this.updater.enqueueForceUpdate(this,t,"forceUpdate")};function z(){}z.prototype=k.prototype;function M(t,e,r){this.props=t,this.context=e,this.refs=O,this.updater=r||P}var $=M.prototype=new z;$.constructor=M,L($,k.prototype),$.isPureReactComponent=!0;var q=Array.isArray;function A(){}var a={H:null,A:null,T:null,S:null},I=Object.prototype.hasOwnProperty;function N(t,e,r){var o=r.ref;return{$$typeof:f,type:t,key:e,ref:o!==void 0?o:null,props:r}}function Z(t,e){return N(t.type,e,t.props)}function S(t){return typeof t=="object"&&t!==null&&t.$$typeof===f}function X(t){var e={"=":"=0",":":"=2"};return"$"+t.replace(/[=:]/g,function(r){return e[r]})}var Y=/\/+/g;function j(t,e){return typeof t=="object"&&t!==null&&t.key!=null?X(""+t.key):e.toString(36)}function Q(t){switch(t.status){case"fulfilled":return t.value;case"rejected":throw t.reason;default:switch(typeof t.status=="string"?t.then(A,A):(t.status="pending",t.then(function(e){t.status==="pending"&&(t.status="fulfilled",t.value=e)},function(e){t.status==="pending"&&(t.status="rejected",t.reason=e)})),t.status){case"fulfilled":return t.value;case"rejected":throw t.reason}}throw t}function v(t,e,r,o,s){var c=typeof t;(c==="undefined"||c==="boolean")&&(t=null);var i=!1;if(t===null)i=!0;else switch(c){case"bigint":case"string":case"number":i=!0;break;case"object":switch(t.$$typeof){case f:case p:i=!0;break;case g:return i=t._init,v(i(t._payload),e,r,o,s)}}if(i)return s=s(t),i=o===""?"."+j(t,0):o,q(s)?(r="",i!=null&&(r=i.replace(Y,"$&/")+"/"),v(s,e,r,"",function(tt){return tt})):s!=null&&(S(s)&&(s=Z(s,r+(s.key==null||t&&t.key===s.key?"":(""+s.key).replace(Y,"$&/")+"/")+i)),e.push(s)),1;i=0;var h=o===""?".":o+":";if(q(t))for(var y=0;y<t.length;y++)o=t[y],c=h+j(o,y),i+=v(o,e,r,c,s);else if(y=G(t),typeof y=="function")for(t=y.call(t),y=0;!(o=t.next()).done;)o=o.value,c=h+j(o,y++),i+=v(o,e,r,c,s);else if(c==="object"){if(typeof t.then=="function")return v(Q(t),e,r,o,s);throw e=String(t),Error("Objects are not valid as a React child (found: "+(e==="[object Object]"?"object with keys {"+Object.keys(t).join(", ")+"}":e)+"). If you meant to render a collection of children, use an array instead.")}return i}function w(t,e,r){if(t==null)return t;var o=[],s=0;return v(t,o,"","",function(c){return e.call(r,c,s++)}),o}function J(t){if(t._status===-1){var e=t._result;e=e(),e.then(function(r){(t._status===0||t._status===-1)&&(t._status=1,t._result=r)},function(r){(t._status===0||t._status===-1)&&(t._status=2,t._result=r)}),t._status===-1&&(t._status=0,t._result=e)}if(t._status===1)return t._result.default;throw t._result}var U=typeof reportError=="function"?reportError:function(t){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var e=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof t=="object"&&t!==null&&typeof t.message=="string"?String(t.message):String(t),error:t});if(!window.dispatchEvent(e))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",t);return}console.error(t)},F={map:w,forEach:function(t,e,r){w(t,function(){e.apply(this,arguments)},r)},count:function(t){var e=0;return w(t,function(){e++}),e},toArray:function(t){return w(t,function(e){return e})||[]},only:function(t){if(!S(t))throw Error("React.Children.only expected to receive a single React element child.");return t}};return n.Activity=K,n.Children=F,n.Component=k,n.Fragment=l,n.Profiler=m,n.PureComponent=M,n.StrictMode=d,n.Suspense=x,n.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=a,n.__COMPILER_RUNTIME={__proto__:null,c:function(t){return a.H.useMemoCache(t)}},n.cache=function(t){return function(){return t.apply(null,arguments)}},n.cacheSignal=function(){return null},n.cloneElement=function(t,e,r){if(t==null)throw Error("The argument must be a React element, but you passed "+t+".");var o=L({},t.props),s=t.key;if(e!=null)for(c in e.key!==void 0&&(s=""+e.key),e)!I.call(e,c)||c==="key"||c==="__self"||c==="__source"||c==="ref"&&e.ref===void 0||(o[c]=e[c]);var c=arguments.length-2;if(c===1)o.children=r;else if(1<c){for(var i=Array(c),h=0;h<c;h++)i[h]=arguments[h+2];o.children=i}return N(t.type,s,o)},n.createContext=function(t){return t={$$typeof:R,_currentValue:t,_currentValue2:t,_threadCount:0,Provider:null,Consumer:null},t.Provider=t,t.Consumer={$$typeof:_,_context:t},t},n.createElement=function(t,e,r){var o,s={},c=null;if(e!=null)for(o in e.key!==void 0&&(c=""+e.key),e)I.call(e,o)&&o!=="key"&&o!=="__self"&&o!=="__source"&&(s[o]=e[o]);var i=arguments.length-2;if(i===1)s.children=r;else if(1<i){for(var h=Array(i),y=0;y<i;y++)h[y]=arguments[y+2];s.children=h}if(t&&t.defaultProps)for(o in i=t.defaultProps,i)s[o]===void 0&&(s[o]=i[o]);return N(t,c,s)},n.createRef=function(){return{current:null}},n.forwardRef=function(t){return{$$typeof:C,render:t}},n.isValidElement=S,n.lazy=function(t){return{$$typeof:g,_payload:{_status:-1,_result:t},_init:J}},n.memo=function(t,e){return{$$typeof:T,type:t,compare:e===void 0?null:e}},n.startTransition=function(t){var e=a.T,r={};a.T=r;try{var o=t(),s=a.S;s!==null&&s(r,o),typeof o=="object"&&o!==null&&typeof o.then=="function"&&o.then(A,U)}catch(c){U(c)}finally{e!==null&&r.types!==null&&(e.types=r.types),a.T=e}},n.unstable_useCacheRefresh=function(){return a.H.useCacheRefresh()},n.use=function(t){return a.H.use(t)},n.useActionState=function(t,e,r){return a.H.useActionState(t,e,r)},n.useCallback=function(t,e){return a.H.useCallback(t,e)},n.useContext=function(t){return a.H.useContext(t)},n.useDebugValue=function(){},n.useDeferredValue=function(t,e){return a.H.useDeferredValue(t,e)},n.useEffect=function(t,e){return a.H.useEffect(t,e)},n.useEffectEvent=function(t){return a.H.useEffectEvent(t)},n.useId=function(){return a.H.useId()},n.useImperativeHandle=function(t,e,r){return a.H.useImperativeHandle(t,e,r)},n.useInsertionEffect=function(t,e){return a.H.useInsertionEffect(t,e)},n.useLayoutEffect=function(t,e){return a.H.useLayoutEffect(t,e)},n.useMemo=function(t,e){return a.H.useMemo(t,e)},n.useOptimistic=function(t,e){return a.H.useOptimistic(t,e)},n.useReducer=function(t,e,r){return a.H.useReducer(t,e,r)},n.useRef=function(t){return a.H.useRef(t)},n.useState=function(t){return a.H.useState(t)},n.useSyncExternalStore=function(t,e,r){return a.H.useSyncExternalStore(t,e,r)},n.useTransition=function(){return a.H.useTransition()},n.version="19.2.7",n}var D;function nt(){return D||(D=1,b.exports=et()),b.exports}var E=nt();const ot=f=>f.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),rt=f=>f.replace(/^([A-Z])|[\s-_]+(\w)/g,(p,l,d)=>d?d.toUpperCase():l.toLowerCase()),B=f=>{const p=rt(f);return p.charAt(0).toUpperCase()+p.slice(1)},W=(...f)=>f.filter((p,l,d)=>!!p&&p.trim()!==""&&d.indexOf(p)===l).join(" ").trim(),st=f=>{for(const p in f)if(p.startsWith("aria-")||p==="role"||p==="title")return!0};var ut={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};const ct=E.forwardRef(({color:f="currentColor",size:p=24,strokeWidth:l=2,absoluteStrokeWidth:d,className:m="",children:_,iconNode:R,...C},x)=>E.createElement("svg",{ref:x,...ut,width:p,height:p,stroke:f,strokeWidth:d?Number(l)*24/Number(p):l,className:W("lucide",m),...!_&&!st(C)&&{"aria-hidden":"true"},...C},[...R.map(([T,g])=>E.createElement(T,g)),...Array.isArray(_)?_:[_]]));const u=(f,p)=>{const l=E.forwardRef(({className:d,...m},_)=>E.createElement(ct,{ref:_,iconNode:p,className:W(`lucide-${ot(B(f))}`,`lucide-${f}`,d),...m}));return l.displayName=B(f),l};const at=[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]],Ot=u("activity",at);const it=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z",key:"3c2336"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],zt=u("badge-check",it);const ft=[["path",{d:"M12 7v14",key:"1akyts"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",key:"ruj8y"}]],qt=u("book-open",ft);const pt=[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]],It=u("chevron-left",pt);const yt=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],Yt=u("chevron-right",yt);const lt=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],Ut=u("circle-check",lt);const dt=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1",key:"tgr4d6"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2",key:"116196"}]],Vt=u("clipboard",dt);const ht=[["rect",{width:"20",height:"14",x:"2",y:"5",rx:"2",key:"ynyp8z"}],["line",{x1:"2",x2:"22",y1:"10",y2:"10",key:"1b3vmo"}]],Dt=u("credit-card",ht);const _t=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]],Bt=u("database",_t);const kt=[["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54",key:"1djwo0"}],["path",{d:"M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17",key:"1tzkfa"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05",key:"14pb5j"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],Wt=u("earth",kt);const vt=[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]],Kt=u("folder-open",vt);const mt=[["rect",{width:"7",height:"7",x:"3",y:"3",rx:"1",key:"1g98yp"}],["rect",{width:"7",height:"7",x:"14",y:"3",rx:"1",key:"6d4xhi"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1",key:"nxv5o0"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1",key:"1bb6yr"}]],Gt=u("layout-grid",mt);const Et=[["circle",{cx:"12",cy:"16",r:"1",key:"1au0dj"}],["rect",{x:"3",y:"10",width:"18",height:"12",rx:"2",key:"6s8ecr"}],["path",{d:"M7 10V7a5 5 0 0 1 10 0v3",key:"1pqi11"}]],Zt=u("lock-keyhole",Et);const Ct=[["path",{d:"m10 17 5-5-5-5",key:"1bsop3"}],["path",{d:"M15 12H3",key:"6jk70r"}],["path",{d:"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4",key:"u53s6r"}]],Xt=u("log-in",Ct);const gt=[["path",{d:"m16 17 5-5-5-5",key:"1bji2h"}],["path",{d:"M21 12H9",key:"dn1m92"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4",key:"1uf3rs"}]],Qt=u("log-out",gt);const wt=[["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2",key:"48i651"}],["line",{x1:"8",x2:"16",y1:"21",y2:"21",key:"1svkeh"}],["line",{x1:"12",x2:"12",y1:"17",y2:"21",key:"vw1qmm"}]],Jt=u("monitor",wt);const Rt=[["path",{d:"M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z",key:"edeuup"}]],Ft=u("mouse-pointer-2",Rt);const xt=[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]],te=u("play",xt);const Tt=[["path",{d:"M12 2v10",key:"mnfbl"}],["path",{d:"M18.4 6.6a9 9 0 1 1-12.77.04",key:"obofu9"}]],ee=u("power",Tt);const Mt=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]],ne=u("rotate-ccw",Mt);const $t=[["path",{d:"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z",key:"1ffxy3"}],["path",{d:"m21.854 2.147-10.94 10.939",key:"12cjpa"}]],oe=u("send",$t);const At=[["rect",{width:"20",height:"8",x:"2",y:"2",rx:"2",ry:"2",key:"ngkwjq"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2",ry:"2",key:"iecqi9"}],["line",{x1:"6",x2:"6.01",y1:"6",y2:"6",key:"16zg32"}],["line",{x1:"6",x2:"6.01",y1:"18",y2:"18",key:"nzw8ys"}]],re=u("server",At);const Nt=[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",key:"1qme2f"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],se=u("settings",Nt);const St=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],ue=u("shield-check",St);const jt=[["path",{d:"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z",key:"r04s7s"}]],ce=u("star",jt);const bt=[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]],ae=u("terminal",bt);const Ht=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z",key:"uqj9uw"}],["path",{d:"M16 9a5 5 0 0 1 0 6",key:"1q6k2b"}],["path",{d:"M19.364 18.364a9 9 0 0 0 0-12.728",key:"ijwkga"}]],ie=u("volume-2",Ht);const Pt=[["path",{d:"M12 20h.01",key:"zekei9"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0",key:"dnpr2z"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0",key:"1x1e6c"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0",key:"1bycff"}]],fe=u("wifi",Pt);const Lt=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],pe=u("x",Lt);export{Ot as A,qt as B,Dt as C,Bt as D,Wt as E,Kt as F,Gt as L,Jt as M,te as P,ne as R,se as S,ae as T,ie as V,fe as W,pe as X,E as a,Ft as b,Xt as c,Qt as d,re as e,ue as f,Vt as g,ce as h,zt as i,Ut as j,It as k,Yt as l,Zt as m,ee as n,oe as o,nt as r};