livedesk 0.1.459 → 0.1.461
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 +28 -5
- package/bin/livedesk.js +223 -24
- package/bootstrap/runtime-lock.js +6 -0
- package/client/package.json +9 -8
- package/client/src/runtime/windows-owned-process-manifest.js +181 -14
- package/diagnostics/livedesk-mac-physical-gate-core.mjs +214 -26
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +406 -4
- package/diagnostics/livedesk-transport-physical-core.mjs +200 -1
- package/diagnostics/livedesk-transport-physical.mjs +60 -1
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +364 -49
- package/hub/src/server.js +4 -0
- package/hub/src/transport/udp-hub-transport.js +29 -6
- package/package.json +10 -9
- package/scripts/sync-web-dist.js +127 -0
- package/web/dist/assets/index-l4YzRK9d.js +25 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-JqqQ5DKN.js +0 -25
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { isIP } from 'node:net';
|
|
2
3
|
|
|
3
4
|
const REQUIRED_PHASES = Object.freeze([
|
|
@@ -33,6 +34,7 @@ const SAFE_NETWORK_KEYS = new Set([
|
|
|
33
34
|
'differentpublicegress',
|
|
34
35
|
'evidence',
|
|
35
36
|
'observedat',
|
|
37
|
+
'rendezvousport',
|
|
36
38
|
'udpready'
|
|
37
39
|
]);
|
|
38
40
|
const IDENTIFIER_KEYS = new Set([
|
|
@@ -50,12 +52,65 @@ const IDENTIFIER_KEYS = new Set([
|
|
|
50
52
|
'clientid',
|
|
51
53
|
'ownerid'
|
|
52
54
|
]);
|
|
55
|
+
const FILESYSTEM_KEYS = new Set([
|
|
56
|
+
'cwd',
|
|
57
|
+
'directory',
|
|
58
|
+
'dirname',
|
|
59
|
+
'executablepath',
|
|
60
|
+
'filepath',
|
|
61
|
+
'filename',
|
|
62
|
+
'homedir',
|
|
63
|
+
'packagepath',
|
|
64
|
+
'path',
|
|
65
|
+
'reportpath'
|
|
66
|
+
]);
|
|
53
67
|
const SECRET_TEXT = /\b(?:authorization|bearer|token|secret|password|api[-_ ]?key|pair[-_ ]?token)(?:\s+|\s*[:=]\s*)\S+/i;
|
|
68
|
+
const FILESYSTEM_TEXT = /(?:^|\s)(?:[a-z]:[\\/]|\\\\[^\\\s]+\\|\/(?:applications|etc|home|opt|private|run|srv|tmp|users|usr|var|volumes)\/)\S*/i;
|
|
54
69
|
const IPV4_TEXT = /(^|[^\d])(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?($|[^\d])/;
|
|
55
70
|
const IPV6_CANDIDATE_TEXT = /\[[0-9a-f:.%]+\](?::\d{1,5})?|[0-9a-f:%]*:[0-9a-f:.%:]*/gi;
|
|
56
71
|
const URL_TEXT = /\b[a-z][a-z0-9+.-]*:\/\/[^\s]+/i;
|
|
57
72
|
const HOST_TEXT = /\b(?:localhost|(?:[a-z0-9-]+\.)+[a-z]{2,63})(?::\d{1,5})?\b/i;
|
|
58
73
|
const UUID_TEXT = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
|
74
|
+
const SAFE_PLATFORMS = new Set([
|
|
75
|
+
'aix',
|
|
76
|
+
'android',
|
|
77
|
+
'darwin',
|
|
78
|
+
'freebsd',
|
|
79
|
+
'linux',
|
|
80
|
+
'macos',
|
|
81
|
+
'openbsd',
|
|
82
|
+
'sunos',
|
|
83
|
+
'win32',
|
|
84
|
+
'windows'
|
|
85
|
+
]);
|
|
86
|
+
const SAFE_ARCHITECTURES = new Set([
|
|
87
|
+
'arm',
|
|
88
|
+
'arm64',
|
|
89
|
+
'ia32',
|
|
90
|
+
'loong64',
|
|
91
|
+
'mips',
|
|
92
|
+
'mipsel',
|
|
93
|
+
'ppc',
|
|
94
|
+
'ppc64',
|
|
95
|
+
'riscv64',
|
|
96
|
+
's390',
|
|
97
|
+
's390x',
|
|
98
|
+
'x64'
|
|
99
|
+
]);
|
|
100
|
+
const SAFE_RUNTIME_STATES = new Set([
|
|
101
|
+
'booting',
|
|
102
|
+
'connecting',
|
|
103
|
+
'error',
|
|
104
|
+
'resolving-role',
|
|
105
|
+
'running',
|
|
106
|
+
'starting',
|
|
107
|
+
'stopping',
|
|
108
|
+
'switching',
|
|
109
|
+
'updating',
|
|
110
|
+
'waiting-auth'
|
|
111
|
+
]);
|
|
112
|
+
const SAFE_RUNTIME_ROLES = new Set(['client', 'hub']);
|
|
113
|
+
const SAFE_VERSION = /^(?:v)?\d+(?:\.\d+){1,3}(?:[-+][0-9a-z][0-9a-z.-]{0,47})?$/i;
|
|
59
114
|
|
|
60
115
|
function normalizedKey(key) {
|
|
61
116
|
return String(key || '').replace(/[-_]/g, '').toLowerCase();
|
|
@@ -74,6 +129,10 @@ function isSecretKey(key) {
|
|
|
74
129
|
|| normalized.endsWith('apikey');
|
|
75
130
|
}
|
|
76
131
|
|
|
132
|
+
function isRawFilesystemKey(key) {
|
|
133
|
+
return FILESYSTEM_KEYS.has(normalizedKey(key));
|
|
134
|
+
}
|
|
135
|
+
|
|
77
136
|
function isRawNetworkKey(key) {
|
|
78
137
|
const rawKey = String(key || '');
|
|
79
138
|
const normalized = normalizedKey(key);
|
|
@@ -119,6 +178,139 @@ function identifierMarker(value) {
|
|
|
119
178
|
return '[redacted-id]';
|
|
120
179
|
}
|
|
121
180
|
|
|
181
|
+
function normalizedEnum(value, allowed) {
|
|
182
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
183
|
+
return allowed.has(normalized) ? normalized : 'unknown';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizedVersion(value) {
|
|
187
|
+
const normalized = String(value || '').trim();
|
|
188
|
+
if (normalized === 'dev') return 'dev';
|
|
189
|
+
return SAFE_VERSION.test(normalized) && isIP(normalized.replace(/^v/i, '')) === 0
|
|
190
|
+
? normalized
|
|
191
|
+
: 'unknown';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizedPackageName(value, fallback = '') {
|
|
195
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
196
|
+
if (normalized === 'livedesk' || /^@livedesk\/[a-z0-9-]{1,48}$/.test(normalized)) {
|
|
197
|
+
return normalized;
|
|
198
|
+
}
|
|
199
|
+
return fallback;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizedBoolean(value) {
|
|
203
|
+
return typeof value === 'boolean' ? value : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isAllowedStructuredScalar(key, value) {
|
|
207
|
+
const normalized = normalizedKey(key);
|
|
208
|
+
const text = String(value || '');
|
|
209
|
+
if (normalized === 'schema') {
|
|
210
|
+
return /^livedesk\.transport-physical\.v\d+$/.test(text);
|
|
211
|
+
}
|
|
212
|
+
if (normalized.endsWith('hash')) {
|
|
213
|
+
return /^sha256:[0-9a-f]{20,64}$/.test(text);
|
|
214
|
+
}
|
|
215
|
+
if ([
|
|
216
|
+
'agentversion',
|
|
217
|
+
'appversion',
|
|
218
|
+
'managerversion',
|
|
219
|
+
'nodeversion',
|
|
220
|
+
'packageversion',
|
|
221
|
+
'productversion'
|
|
222
|
+
].includes(normalized)) {
|
|
223
|
+
return text === 'unknown'
|
|
224
|
+
|| text === 'dev'
|
|
225
|
+
|| normalizedVersion(text) === text;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function stableFingerprint(value) {
|
|
231
|
+
const normalized = String(value || '').trim();
|
|
232
|
+
if (!normalized) return '';
|
|
233
|
+
return `sha256:${createHash('sha256').update(normalized).digest('hex').slice(0, 20)}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function runtimeFingerprint(value = {}) {
|
|
237
|
+
const explicit = String(value.runtimeId || value.sessionId || '').trim();
|
|
238
|
+
if (explicit) return stableFingerprint(explicit);
|
|
239
|
+
const pid = Number(value.runtimePid ?? value.pid);
|
|
240
|
+
const startedAt = String(value.startedAt || value.connectedAt || '').trim();
|
|
241
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || !startedAt) return '';
|
|
242
|
+
return stableFingerprint(`${pid}:${startedAt}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function normalizedRendezvousEndpoint(value = {}) {
|
|
246
|
+
let rawHost = String(value.host || value.rendezvousHost || '').trim();
|
|
247
|
+
let rawPort = Number(value.port ?? value.rendezvousPort);
|
|
248
|
+
if (rawHost) {
|
|
249
|
+
try {
|
|
250
|
+
const parsed = new URL(rawHost.includes('://') ? rawHost : `udp://${rawHost}`);
|
|
251
|
+
rawHost = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase().replace(/\.$/, '');
|
|
252
|
+
if ((!Number.isSafeInteger(rawPort) || rawPort < 1 || rawPort > 65_535)
|
|
253
|
+
&& parsed.port) {
|
|
254
|
+
rawPort = Number(parsed.port);
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
rawHost = '';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const rendezvousPort = Number.isSafeInteger(rawPort) && rawPort >= 1 && rawPort <= 65_535
|
|
261
|
+
? rawPort
|
|
262
|
+
: null;
|
|
263
|
+
return {
|
|
264
|
+
endpointKind: 'udp-rendezvous',
|
|
265
|
+
configured: !!rawHost && rendezvousPort !== null,
|
|
266
|
+
hostHash: stableFingerprint(rawHost),
|
|
267
|
+
rendezvousPort
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function buildTransportPhysicalProvenance(input = {}) {
|
|
272
|
+
const liveDeskPackage = input.liveDeskPackage || {};
|
|
273
|
+
const runner = input.runner || {};
|
|
274
|
+
const hubRuntime = input.hubRuntime || {};
|
|
275
|
+
const hubRemote = input.hubRemote || {};
|
|
276
|
+
const client = input.client || {};
|
|
277
|
+
const udp = input.rendezvous || hubRemote.udp || {};
|
|
278
|
+
const hubVersion = normalizedVersion(hubRuntime.appVersion);
|
|
279
|
+
const managerVersion = normalizedVersion(hubRemote.managerVersion);
|
|
280
|
+
return {
|
|
281
|
+
livedesk: {
|
|
282
|
+
packageName: 'livedesk',
|
|
283
|
+
packageVersion: normalizedVersion(liveDeskPackage.version)
|
|
284
|
+
},
|
|
285
|
+
runner: {
|
|
286
|
+
nodeVersion: normalizedVersion(runner.nodeVersion),
|
|
287
|
+
platform: normalizedEnum(runner.platform, SAFE_PLATFORMS),
|
|
288
|
+
arch: normalizedEnum(runner.arch, SAFE_ARCHITECTURES)
|
|
289
|
+
},
|
|
290
|
+
hub: {
|
|
291
|
+
packageName: normalizedPackageName(hubRemote.managerPackage, '@livedesk/hub'),
|
|
292
|
+
appVersion: hubVersion !== 'unknown' ? hubVersion : managerVersion,
|
|
293
|
+
managerVersion,
|
|
294
|
+
role: normalizedEnum(hubRuntime.role || hubRemote.runtimeRole, SAFE_RUNTIME_ROLES),
|
|
295
|
+
state: normalizedEnum(hubRuntime.state, SAFE_RUNTIME_STATES),
|
|
296
|
+
runtimeStarted: normalizedBoolean(hubRuntime.runtimeStarted),
|
|
297
|
+
runtimeIdentityHash: runtimeFingerprint(hubRuntime)
|
|
298
|
+
},
|
|
299
|
+
client: {
|
|
300
|
+
productVersion: normalizedVersion(client.productVersion),
|
|
301
|
+
agentVersion: normalizedVersion(client.agentVersion),
|
|
302
|
+
platform: normalizedEnum(client.platform, SAFE_PLATFORMS),
|
|
303
|
+
arch: normalizedEnum(client.arch, SAFE_ARCHITECTURES),
|
|
304
|
+
runtimeIdentityHash: runtimeFingerprint(client)
|
|
305
|
+
},
|
|
306
|
+
rendezvous: {
|
|
307
|
+
...normalizedRendezvousEndpoint(udp),
|
|
308
|
+
enabled: normalizedBoolean(udp.enabled),
|
|
309
|
+
started: normalizedBoolean(udp.started)
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
122
314
|
export function genericTransportPhysicalError(value, fallback = 'transport-diagnostic-error') {
|
|
123
315
|
const text = String(value instanceof Error ? value.message : value || '').trim();
|
|
124
316
|
if (!text
|
|
@@ -132,6 +324,7 @@ export function genericTransportPhysicalError(value, fallback = 'transport-diagn
|
|
|
132
324
|
export function redactTransportPhysicalReport(value, key = '') {
|
|
133
325
|
if (isSecretKey(key)) return '[redacted]';
|
|
134
326
|
if (isIdentifierKey(key)) return identifierMarker(value);
|
|
327
|
+
if (isRawFilesystemKey(key)) return '[redacted-path]';
|
|
135
328
|
if (isRawNetworkKey(key)) return '[redacted-network]';
|
|
136
329
|
if (Array.isArray(value)) {
|
|
137
330
|
return value.map(item => redactTransportPhysicalReport(item));
|
|
@@ -145,7 +338,9 @@ export function redactTransportPhysicalReport(value, key = '') {
|
|
|
145
338
|
]));
|
|
146
339
|
}
|
|
147
340
|
if (typeof value === 'string'
|
|
341
|
+
&& !isAllowedStructuredScalar(key, value)
|
|
148
342
|
&& (containsRawNetworkText(value)
|
|
343
|
+
|| FILESYSTEM_TEXT.test(value)
|
|
149
344
|
|| SECRET_TEXT.test(value)
|
|
150
345
|
|| UUID_TEXT.test(value))) {
|
|
151
346
|
return /error|reason|detail|summary/i.test(key)
|
|
@@ -163,6 +358,7 @@ export function findTransportPhysicalReportLeaks(value, path = '$', leaks = [])
|
|
|
163
358
|
if (value && typeof value === 'object') {
|
|
164
359
|
for (const [key, child] of Object.entries(value)) {
|
|
165
360
|
if (isSecretKey(key)
|
|
361
|
+
|| (isRawFilesystemKey(key) && child !== '[redacted-path]')
|
|
166
362
|
|| (isRawNetworkKey(key) && child !== '[redacted-network]')) {
|
|
167
363
|
leaks.push(`${path}.${key}`);
|
|
168
364
|
}
|
|
@@ -176,7 +372,9 @@ export function findTransportPhysicalReportLeaks(value, path = '$', leaks = [])
|
|
|
176
372
|
return leaks;
|
|
177
373
|
}
|
|
178
374
|
if (typeof value === 'string'
|
|
375
|
+
&& !isAllowedStructuredScalar(path.split('.').at(-1)?.replace(/\[\d+\]$/, ''), value)
|
|
179
376
|
&& (containsRawNetworkText(value)
|
|
377
|
+
|| FILESYSTEM_TEXT.test(value)
|
|
180
378
|
|| SECRET_TEXT.test(value)
|
|
181
379
|
|| UUID_TEXT.test(value))) {
|
|
182
380
|
leaks.push(path);
|
|
@@ -482,10 +680,11 @@ export function classifyTransportPhysicalSnapshot(snapshot) {
|
|
|
482
680
|
export function buildTransportPhysicalReport(snapshot, context = {}) {
|
|
483
681
|
const classified = classifyTransportPhysicalSnapshot(snapshot);
|
|
484
682
|
const report = redactTransportPhysicalReport({
|
|
485
|
-
schema: 'livedesk.transport-physical.
|
|
683
|
+
schema: 'livedesk.transport-physical.v2',
|
|
486
684
|
generatedAt: new Date().toISOString(),
|
|
487
685
|
hubUrl: context.hubUrl,
|
|
488
686
|
selectedDeviceId: context.deviceId,
|
|
687
|
+
provenance: buildTransportPhysicalProvenance(context.provenance),
|
|
489
688
|
result: classified,
|
|
490
689
|
transport: snapshot
|
|
491
690
|
});
|
|
@@ -22,6 +22,54 @@ const CLEANUP_RETRY_DELAY_MS = 250;
|
|
|
22
22
|
const DEFAULT_TTL_MS = 150_000;
|
|
23
23
|
const MIN_TTL_MARGIN_MS = 20_000;
|
|
24
24
|
|
|
25
|
+
async function readLiveDeskPackageIdentity() {
|
|
26
|
+
try {
|
|
27
|
+
const packageInfo = JSON.parse(
|
|
28
|
+
await fs.readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
29
|
+
return { version: String(packageInfo?.version || '') };
|
|
30
|
+
} catch {
|
|
31
|
+
return { version: '' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function collectTransportPhysicalProvenance({
|
|
36
|
+
hubUrl = '',
|
|
37
|
+
deviceId = '',
|
|
38
|
+
signal,
|
|
39
|
+
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS
|
|
40
|
+
} = {}) {
|
|
41
|
+
const provenance = {
|
|
42
|
+
liveDeskPackage: await readLiveDeskPackageIdentity(),
|
|
43
|
+
runner: {
|
|
44
|
+
nodeVersion: process.versions.node,
|
|
45
|
+
platform: process.platform,
|
|
46
|
+
arch: process.arch
|
|
47
|
+
},
|
|
48
|
+
hubRuntime: null,
|
|
49
|
+
hubRemote: null,
|
|
50
|
+
client: null
|
|
51
|
+
};
|
|
52
|
+
if (!hubUrl || !deviceId) return provenance;
|
|
53
|
+
|
|
54
|
+
const requests = await Promise.allSettled([
|
|
55
|
+
requestJson(`${hubUrl}/api/runtime/status`, { signal, requestTimeoutMs }),
|
|
56
|
+
requestJson(`${hubUrl}/api/remote/status`, { signal, requestTimeoutMs }),
|
|
57
|
+
requestJson(`${hubUrl}/api/remote/devices`, { signal, requestTimeoutMs })
|
|
58
|
+
]);
|
|
59
|
+
if (signal?.aborted) {
|
|
60
|
+
throw signal.reason instanceof Error
|
|
61
|
+
? signal.reason
|
|
62
|
+
: new Error('transport diagnostic aborted');
|
|
63
|
+
}
|
|
64
|
+
provenance.hubRuntime = requests[0].status === 'fulfilled' ? requests[0].value : null;
|
|
65
|
+
provenance.hubRemote = requests[1].status === 'fulfilled' ? requests[1].value : null;
|
|
66
|
+
const devices = requests[2].status === 'fulfilled' && Array.isArray(requests[2].value?.devices)
|
|
67
|
+
? requests[2].value.devices
|
|
68
|
+
: [];
|
|
69
|
+
provenance.client = devices.find(device => device?.deviceId === deviceId) || null;
|
|
70
|
+
return provenance;
|
|
71
|
+
}
|
|
72
|
+
|
|
25
73
|
export function normalizeTransportPhysicalTiming(options = {}) {
|
|
26
74
|
const ttlMs = Math.max(15_000, Math.min(180_000, Number(options.ttlMs) || DEFAULT_TTL_MS));
|
|
27
75
|
const phaseTimeoutMs = Math.max(
|
|
@@ -355,6 +403,7 @@ export async function runTransportPhysicalDiagnostic(options = {}) {
|
|
|
355
403
|
let latest = null;
|
|
356
404
|
let completed = false;
|
|
357
405
|
let runError = null;
|
|
406
|
+
let provenance = await collectTransportPhysicalProvenance();
|
|
358
407
|
|
|
359
408
|
const stopForSignal = () => {
|
|
360
409
|
if (!lifecycleController.signal.aborted) {
|
|
@@ -369,6 +418,12 @@ export async function runTransportPhysicalDiagnostic(options = {}) {
|
|
|
369
418
|
options.deviceId || '',
|
|
370
419
|
signal,
|
|
371
420
|
timing.requestTimeoutMs);
|
|
421
|
+
provenance = await collectTransportPhysicalProvenance({
|
|
422
|
+
hubUrl,
|
|
423
|
+
deviceId,
|
|
424
|
+
signal,
|
|
425
|
+
requestTimeoutMs: timing.requestTimeoutMs
|
|
426
|
+
});
|
|
372
427
|
const started = await requestJson(`${hubUrl}/api/remote/diagnostics/transport/start`, {
|
|
373
428
|
method: 'POST',
|
|
374
429
|
body: { deviceId, ttlMs: timing.ttlMs },
|
|
@@ -445,7 +500,11 @@ export async function runTransportPhysicalDiagnostic(options = {}) {
|
|
|
445
500
|
error: genericTransportPhysicalError(runError)
|
|
446
501
|
};
|
|
447
502
|
}
|
|
448
|
-
const report = buildTransportPhysicalReport(latest || {}, {
|
|
503
|
+
const report = buildTransportPhysicalReport(latest || {}, {
|
|
504
|
+
hubUrl,
|
|
505
|
+
deviceId,
|
|
506
|
+
provenance
|
|
507
|
+
});
|
|
449
508
|
await writeStableJson(reportPath, report);
|
|
450
509
|
if (runError) {
|
|
451
510
|
const failure = new Error(
|