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.
@@ -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.v1',
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 || {}, { hubUrl, deviceId });
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(
package/hub/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",