livedesk 0.1.593 → 0.1.595
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.js +8 -8
- package/bootstrap/role-bootstrap-server.js +1 -1
- package/client/bin/livedesk-client-fast.js +1 -1
- package/client/bin/livedesk-client-node.js +1 -1
- package/client/bin/livedesk-client.js +30 -30
- package/client/package.json +6 -6
- package/client/src/runtime/client-runtime-server.js +4 -4
- package/diagnostics/livedesk-mac-physical-isolation.mjs +354 -354
- package/diagnostics/livedesk-transport-physical-core.mjs +702 -702
- package/electron/auth-config.mjs +10 -10
- package/electron/bounded-desktop-log.mjs +609 -609
- package/electron/electron-builder.win-dev.yml +1 -1
- package/electron/icon.svg +11 -11
- package/electron/main.mjs +5 -5
- package/electron/runtime-process-owner.mjs +287 -287
- package/hub/package.json +2 -2
- package/hub/src/server.js +2 -2
- package/package.json +6 -6
- package/runtime-core/package.json +1 -1
- package/runtime-core/src/auth-config.js +1 -1
- package/web/dist/app.webmanifest +1 -1
- package/web/dist/assets/LiveDeskApp-CvQUgQ08.js +178 -0
- package/web/dist/assets/{icons-DwJmccVC.js → icons-tFaWqIIe.js} +1 -1
- package/web/dist/assets/{index-DjIbOQxl.js → index-BDVKTkV3.js} +3 -3
- package/web/dist/assets/{index-DRI7OJI6.css → index-CpHNEjHy.css} +1 -1
- package/web/dist/assets/{react-DOZUPHF8.js → react-BQos8uGS.js} +1 -1
- package/web/dist/favicon.svg +11 -11
- package/web/dist/index.html +6 -6
- package/web/dist/livedesk-build-evidence.json +25 -25
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/LiveDeskApp-Djx1CsIp.js +0 -178
|
@@ -1,702 +1,702 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { isIP } from 'node:net';
|
|
3
|
-
|
|
4
|
-
const REQUIRED_PHASES = Object.freeze([
|
|
5
|
-
'direct',
|
|
6
|
-
'p2p',
|
|
7
|
-
'relay',
|
|
8
|
-
'recover-direct'
|
|
9
|
-
]);
|
|
10
|
-
|
|
11
|
-
const MAX_PHASE_LATENCY_MS = 15_000;
|
|
12
|
-
const MAX_AGENT_QUEUE_DEPTH = 8;
|
|
13
|
-
const MAX_AGENT_QUEUE_DROPPED_DELTA = 8;
|
|
14
|
-
const MAX_HUB_INGRESS_DROPPED_DELTA = 8;
|
|
15
|
-
const MAX_UDP_INCOMPLETE_DELTA = 2;
|
|
16
|
-
const MAX_HUB_ADDRESS_OBSERVATION_SKEW_MS = 60_000;
|
|
17
|
-
|
|
18
|
-
const SECRET_KEYS = new Set([
|
|
19
|
-
'authorization',
|
|
20
|
-
'bearer',
|
|
21
|
-
'token',
|
|
22
|
-
'secret',
|
|
23
|
-
'password',
|
|
24
|
-
'apikey',
|
|
25
|
-
'pairtoken'
|
|
26
|
-
]);
|
|
27
|
-
const SAFE_NETWORK_KEYS = new Set([
|
|
28
|
-
'clientaddresshash',
|
|
29
|
-
'hubaddresshash',
|
|
30
|
-
'clientaddresspublic',
|
|
31
|
-
'hubaddresspublic',
|
|
32
|
-
'hubaddressobservedat',
|
|
33
|
-
'endpointauthenticated',
|
|
34
|
-
'differentpublicegress',
|
|
35
|
-
'evidence',
|
|
36
|
-
'observedat',
|
|
37
|
-
'rendezvousport',
|
|
38
|
-
'udpready'
|
|
39
|
-
]);
|
|
40
|
-
const IDENTIFIER_KEYS = new Set([
|
|
41
|
-
'leaseid',
|
|
42
|
-
'deviceid',
|
|
43
|
-
'selecteddeviceid',
|
|
44
|
-
'streamid',
|
|
45
|
-
'streamcommandid',
|
|
46
|
-
'commandid',
|
|
47
|
-
'sessionid',
|
|
48
|
-
'requestid',
|
|
49
|
-
'connectionid',
|
|
50
|
-
'hubconnectionid',
|
|
51
|
-
'inputeventid',
|
|
52
|
-
'clientid',
|
|
53
|
-
'ownerid'
|
|
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
|
-
]);
|
|
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;
|
|
69
|
-
const IPV4_TEXT = /(^|[^\d])(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?($|[^\d])/;
|
|
70
|
-
const IPV6_CANDIDATE_TEXT = /\[[0-9a-f:.%]+\](?::\d{1,5})?|[0-9a-f:%]*:[0-9a-f:.%:]*/gi;
|
|
71
|
-
const URL_TEXT = /\b[a-z][a-z0-9+.-]*:\/\/[^\s]+/i;
|
|
72
|
-
const HOST_TEXT = /\b(?:localhost|(?:[a-z0-9-]+\.)+[a-z]{2,63})(?::\d{1,5})?\b/i;
|
|
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;
|
|
114
|
-
|
|
115
|
-
function normalizedKey(key) {
|
|
116
|
-
return String(key || '').replace(/[-_]/g, '').toLowerCase();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function isIdentifierKey(key) {
|
|
120
|
-
return IDENTIFIER_KEYS.has(normalizedKey(key));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function isSecretKey(key) {
|
|
124
|
-
const normalized = normalizedKey(key);
|
|
125
|
-
return SECRET_KEYS.has(normalized)
|
|
126
|
-
|| normalized.endsWith('token')
|
|
127
|
-
|| normalized.endsWith('secret')
|
|
128
|
-
|| normalized.endsWith('password')
|
|
129
|
-
|| normalized.endsWith('apikey');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function isRawFilesystemKey(key) {
|
|
133
|
-
return FILESYSTEM_KEYS.has(normalizedKey(key));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isRawNetworkKey(key) {
|
|
137
|
-
const rawKey = String(key || '');
|
|
138
|
-
const normalized = normalizedKey(key);
|
|
139
|
-
if (SAFE_NETWORK_KEYS.has(normalized) || normalized.endsWith('hash')) return false;
|
|
140
|
-
return normalized === 'address'
|
|
141
|
-
|| normalized.endsWith('address')
|
|
142
|
-
|| normalized === 'endpoint'
|
|
143
|
-
|| normalized.endsWith('endpoint')
|
|
144
|
-
|| normalized === 'host'
|
|
145
|
-
|| normalized.endsWith('host')
|
|
146
|
-
|| normalized === 'port'
|
|
147
|
-
|| /(?:^|[-_])port$/i.test(rawKey)
|
|
148
|
-
|| /[A-Z]Port$/.test(rawKey)
|
|
149
|
-
|| normalized === 'url'
|
|
150
|
-
|| normalized.endsWith('url')
|
|
151
|
-
|| normalized === 'uri'
|
|
152
|
-
|| normalized.endsWith('uri')
|
|
153
|
-
|| normalized === 'origin'
|
|
154
|
-
|| normalized.endsWith('origin');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function containsIpv6Text(value) {
|
|
158
|
-
const candidates = String(value || '').match(IPV6_CANDIDATE_TEXT) || [];
|
|
159
|
-
return candidates.some(candidate => {
|
|
160
|
-
const bracketed = candidate.match(/^\[([^\]]+)\](?::\d{1,5})?$/);
|
|
161
|
-
const address = String(bracketed?.[1] || candidate)
|
|
162
|
-
.replace(/%[a-z0-9_.-]+$/i, '')
|
|
163
|
-
.replace(/[.,;]+$/g, '');
|
|
164
|
-
return isIP(address) === 6;
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function containsRawNetworkText(value) {
|
|
169
|
-
const text = String(value || '');
|
|
170
|
-
return IPV4_TEXT.test(text)
|
|
171
|
-
|| containsIpv6Text(text)
|
|
172
|
-
|| URL_TEXT.test(text)
|
|
173
|
-
|| HOST_TEXT.test(text);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function identifierMarker(value) {
|
|
177
|
-
if (value === null || value === undefined || String(value) === '') return '';
|
|
178
|
-
return '[redacted-id]';
|
|
179
|
-
}
|
|
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
|
-
|
|
314
|
-
export function genericTransportPhysicalError(value, fallback = 'transport-diagnostic-error') {
|
|
315
|
-
const text = String(value instanceof Error ? value.message : value || '').trim();
|
|
316
|
-
if (!text
|
|
317
|
-
|| containsRawNetworkText(text)
|
|
318
|
-
|| SECRET_TEXT.test(text)
|
|
319
|
-
|| UUID_TEXT.test(text)) return fallback;
|
|
320
|
-
if (!/^[a-z0-9][a-z0-9 _.,:;()[\]/'"+=-]{0,239}$/i.test(text)) return fallback;
|
|
321
|
-
return text;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export function redactTransportPhysicalReport(value, key = '') {
|
|
325
|
-
if (isSecretKey(key)) return '[redacted]';
|
|
326
|
-
if (isIdentifierKey(key)) return identifierMarker(value);
|
|
327
|
-
if (isRawFilesystemKey(key)) return '[redacted-path]';
|
|
328
|
-
if (isRawNetworkKey(key)) return '[redacted-network]';
|
|
329
|
-
if (Array.isArray(value)) {
|
|
330
|
-
return value.map(item => redactTransportPhysicalReport(item));
|
|
331
|
-
}
|
|
332
|
-
if (value && typeof value === 'object') {
|
|
333
|
-
return Object.fromEntries(Object.entries(value)
|
|
334
|
-
.filter(([childKey]) => !isSecretKey(childKey))
|
|
335
|
-
.map(([childKey, childValue]) => [
|
|
336
|
-
childKey,
|
|
337
|
-
redactTransportPhysicalReport(childValue, childKey)
|
|
338
|
-
]));
|
|
339
|
-
}
|
|
340
|
-
if (typeof value === 'string'
|
|
341
|
-
&& !isAllowedStructuredScalar(key, value)
|
|
342
|
-
&& (containsRawNetworkText(value)
|
|
343
|
-
|| FILESYSTEM_TEXT.test(value)
|
|
344
|
-
|| SECRET_TEXT.test(value)
|
|
345
|
-
|| UUID_TEXT.test(value))) {
|
|
346
|
-
return /error|reason|detail|summary/i.test(key)
|
|
347
|
-
? '[redacted-sensitive-error]'
|
|
348
|
-
: '[redacted-sensitive]';
|
|
349
|
-
}
|
|
350
|
-
return value;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export function findTransportPhysicalReportLeaks(value, path = '$', leaks = []) {
|
|
354
|
-
if (Array.isArray(value)) {
|
|
355
|
-
value.forEach((item, index) => findTransportPhysicalReportLeaks(item, `${path}[${index}]`, leaks));
|
|
356
|
-
return leaks;
|
|
357
|
-
}
|
|
358
|
-
if (value && typeof value === 'object') {
|
|
359
|
-
for (const [key, child] of Object.entries(value)) {
|
|
360
|
-
if (isSecretKey(key)
|
|
361
|
-
|| (isRawFilesystemKey(key) && child !== '[redacted-path]')
|
|
362
|
-
|| (isRawNetworkKey(key) && child !== '[redacted-network]')) {
|
|
363
|
-
leaks.push(`${path}.${key}`);
|
|
364
|
-
}
|
|
365
|
-
if (isIdentifierKey(key)
|
|
366
|
-
&& child !== ''
|
|
367
|
-
&& child !== '[redacted-id]') {
|
|
368
|
-
leaks.push(`${path}.${key}`);
|
|
369
|
-
}
|
|
370
|
-
findTransportPhysicalReportLeaks(child, `${path}.${key}`, leaks);
|
|
371
|
-
}
|
|
372
|
-
return leaks;
|
|
373
|
-
}
|
|
374
|
-
if (typeof value === 'string'
|
|
375
|
-
&& !isAllowedStructuredScalar(path.split('.').at(-1)?.replace(/\[\d+\]$/, ''), value)
|
|
376
|
-
&& (containsRawNetworkText(value)
|
|
377
|
-
|| FILESYSTEM_TEXT.test(value)
|
|
378
|
-
|| SECRET_TEXT.test(value)
|
|
379
|
-
|| UUID_TEXT.test(value))) {
|
|
380
|
-
leaks.push(path);
|
|
381
|
-
}
|
|
382
|
-
return leaks;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function optionalFinite(value) {
|
|
386
|
-
if (value === null || value === undefined || value === '') return null;
|
|
387
|
-
const parsed = Number(value);
|
|
388
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function phaseFrame(snapshot, phase) {
|
|
392
|
-
return Array.isArray(snapshot?.frames)
|
|
393
|
-
? snapshot.frames.find(frame => frame?.phase === phase && frame?.accepted === true)
|
|
394
|
-
: null;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function expectedPathForPhase(phase) {
|
|
398
|
-
if (phase === 'p2p') return 'p2p';
|
|
399
|
-
if (phase === 'relay') return 'relay';
|
|
400
|
-
return 'direct';
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
export function hubObservedTransport(frame) {
|
|
404
|
-
const transport = String(frame?.transport || '').trim().toLowerCase();
|
|
405
|
-
if (transport === 'udp-p2p') return { path: 'p2p', label: 'udp-p2p' };
|
|
406
|
-
if (transport === 'relay-binary') return { path: 'relay', label: 'relay-binary' };
|
|
407
|
-
if (transport === 'binary' || transport === 'tcp-binary') {
|
|
408
|
-
return { path: 'direct', label: 'direct-tcp' };
|
|
409
|
-
}
|
|
410
|
-
if (transport === 'ws-binary') return { path: 'direct', label: 'direct-ws' };
|
|
411
|
-
return { path: '', label: 'unknown' };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
export function isTransportPhysicalPhaseEvidence(frame, phase) {
|
|
415
|
-
const expectedPath = expectedPathForPhase(phase);
|
|
416
|
-
const observed = hubObservedTransport(frame);
|
|
417
|
-
const phaseLatencyMs = optionalFinite(frame?.phaseLatencyMs);
|
|
418
|
-
return frame?.accepted === true
|
|
419
|
-
&& frame?.phase === phase
|
|
420
|
-
&& frame?.actualPath === expectedPath
|
|
421
|
-
&& observed.path === expectedPath
|
|
422
|
-
&& frame?.isKeyFrame === true
|
|
423
|
-
&& frame?.annexB === true
|
|
424
|
-
&& frame?.hasIdr === true
|
|
425
|
-
&& frame?.hasSps === true
|
|
426
|
-
&& frame?.hasPps === true
|
|
427
|
-
&& frame?.nalOrderValid === true
|
|
428
|
-
&& optionalFinite(frame?.byteLength) > 0
|
|
429
|
-
&& phaseLatencyMs !== null
|
|
430
|
-
&& phaseLatencyMs >= 0
|
|
431
|
-
&& phaseLatencyMs <= MAX_PHASE_LATENCY_MS;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function cumulativeDelta(values) {
|
|
435
|
-
if (!Array.isArray(values) || values.length < 2) {
|
|
436
|
-
return { valid: false, delta: null };
|
|
437
|
-
}
|
|
438
|
-
const normalized = values.map(optionalFinite);
|
|
439
|
-
const valid = normalized.every(value => value !== null && value >= 0)
|
|
440
|
-
&& normalized.every((value, index) => index === 0 || value >= normalized[index - 1]);
|
|
441
|
-
return {
|
|
442
|
-
valid,
|
|
443
|
-
delta: valid ? normalized.at(-1) - normalized[0] : null
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function scopedCounterDelta(baselines, samples) {
|
|
448
|
-
const scopes = new Map();
|
|
449
|
-
for (const baseline of baselines || []) {
|
|
450
|
-
const scope = String(baseline?.scope || '');
|
|
451
|
-
const value = optionalFinite(baseline?.value);
|
|
452
|
-
if (!scope || value === null || value < 0 || scopes.has(scope)) {
|
|
453
|
-
return { valid: false, delta: null };
|
|
454
|
-
}
|
|
455
|
-
scopes.set(scope, { initial: value, last: value });
|
|
456
|
-
}
|
|
457
|
-
let total = 0;
|
|
458
|
-
for (const sample of samples || []) {
|
|
459
|
-
const scope = String(sample?.scope || '');
|
|
460
|
-
const value = optionalFinite(sample?.value);
|
|
461
|
-
if (!scope || value === null || value < 0) {
|
|
462
|
-
return { valid: false, delta: null };
|
|
463
|
-
}
|
|
464
|
-
const state = scopes.get(scope) || { initial: 0, last: 0 };
|
|
465
|
-
if (value < state.last) {
|
|
466
|
-
return { valid: false, delta: null };
|
|
467
|
-
}
|
|
468
|
-
state.last = value;
|
|
469
|
-
scopes.set(scope, state);
|
|
470
|
-
}
|
|
471
|
-
for (const state of scopes.values()) {
|
|
472
|
-
total += state.last - state.initial;
|
|
473
|
-
}
|
|
474
|
-
return { valid: true, delta: total };
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function timestampWithin(value, reference, maximumSkewMs) {
|
|
478
|
-
const observedAt = Date.parse(String(value || ''));
|
|
479
|
-
const referenceAt = Date.parse(String(reference || ''));
|
|
480
|
-
return Number.isFinite(observedAt)
|
|
481
|
-
&& Number.isFinite(referenceAt)
|
|
482
|
-
&& Math.abs(observedAt - referenceAt) <= maximumSkewMs;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
export function classifyTransportPhysicalSnapshot(snapshot) {
|
|
486
|
-
const checks = [];
|
|
487
|
-
const add = (id, ok, detail) => checks.push({ id, ok: ok === true, detail });
|
|
488
|
-
const owner = snapshot?.owner || {};
|
|
489
|
-
|
|
490
|
-
add(
|
|
491
|
-
'same-current-owner',
|
|
492
|
-
!!owner.streamId
|
|
493
|
-
&& !!owner.streamCommandId
|
|
494
|
-
&& Number(owner.captureGeneration || 0) > 0
|
|
495
|
-
&& Number(owner.monitorIndex) >= 0,
|
|
496
|
-
'One immutable stream, command, generation, and monitor must own the complete run.');
|
|
497
|
-
|
|
498
|
-
const observedFrames = REQUIRED_PHASES.map(phase => phaseFrame(snapshot, phase));
|
|
499
|
-
for (let index = 0; index < REQUIRED_PHASES.length; index += 1) {
|
|
500
|
-
const phase = REQUIRED_PHASES[index];
|
|
501
|
-
const frame = observedFrames[index];
|
|
502
|
-
const expectedPath = expectedPathForPhase(phase);
|
|
503
|
-
const observed = hubObservedTransport(frame);
|
|
504
|
-
add(
|
|
505
|
-
`received-h264-${phase}`,
|
|
506
|
-
!!frame
|
|
507
|
-
&& frame.actualPath === expectedPath
|
|
508
|
-
&& frame.isKeyFrame === true
|
|
509
|
-
&& frame.annexB === true
|
|
510
|
-
&& frame.hasIdr === true
|
|
511
|
-
&& frame.hasSps === true
|
|
512
|
-
&& frame.hasPps === true
|
|
513
|
-
&& frame.nalOrderValid === true
|
|
514
|
-
&& optionalFinite(frame.byteLength) > 0,
|
|
515
|
-
`A received Annex-B H.264 SPS -> PPS -> IDR access unit must prove ${phase}.`);
|
|
516
|
-
add(
|
|
517
|
-
`hub-observed-transport-${phase}`,
|
|
518
|
-
!!frame && observed.path === expectedPath,
|
|
519
|
-
`Hub ingress must observe ${phase} as ${observed.label}.`);
|
|
520
|
-
const phaseLatencyMs = optionalFinite(frame?.phaseLatencyMs);
|
|
521
|
-
add(
|
|
522
|
-
`phase-latency-${phase}`,
|
|
523
|
-
phaseLatencyMs !== null
|
|
524
|
-
&& phaseLatencyMs >= 0
|
|
525
|
-
&& phaseLatencyMs <= MAX_PHASE_LATENCY_MS,
|
|
526
|
-
`${phaseLatencyMs === null ? 'unknown' : Math.round(phaseLatencyMs)}ms/15000ms`);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const receivedTimes = observedFrames.map(frame => Date.parse(frame?.receivedAt || ''));
|
|
530
|
-
add(
|
|
531
|
-
'ordered-path-transition',
|
|
532
|
-
observedFrames.every(Boolean)
|
|
533
|
-
&& receivedTimes.every(Number.isFinite)
|
|
534
|
-
&& receivedTimes.every((time, index) => index === 0 || time > receivedTimes[index - 1]),
|
|
535
|
-
'Frames must arrive in strict Direct, UDP P2P, encrypted relay, Direct order.');
|
|
536
|
-
|
|
537
|
-
const frameSequences = observedFrames.map(frame => optionalFinite(frame?.frameSeq));
|
|
538
|
-
add(
|
|
539
|
-
'strict-frame-sequence',
|
|
540
|
-
frameSequences.every(value => Number.isSafeInteger(value) && value > 0)
|
|
541
|
-
&& frameSequences.every((value, index) => index === 0 || value > frameSequences[index - 1]),
|
|
542
|
-
frameSequences.map(value => value ?? '?').join(' -> '));
|
|
543
|
-
|
|
544
|
-
const switchCounts = observedFrames.map(frame => optionalFinite(frame?.switchCount));
|
|
545
|
-
add(
|
|
546
|
-
'transport-switch-count',
|
|
547
|
-
switchCounts.every(value => Number.isSafeInteger(value) && value >= 0)
|
|
548
|
-
&& switchCounts.every((value, index) => index === 0 || value > switchCounts[index - 1]),
|
|
549
|
-
switchCounts.map(value => value ?? '?').join(' -> '));
|
|
550
|
-
|
|
551
|
-
const network = snapshot?.network || {};
|
|
552
|
-
add(
|
|
553
|
-
'distinct-public-ipv4-egress',
|
|
554
|
-
network.evidence === 'authenticated-udp-endpoint-rendezvous-observed'
|
|
555
|
-
&& network.endpointAuthenticated === true
|
|
556
|
-
&& network.clientAddressPublic === true
|
|
557
|
-
&& network.hubAddressPublic === true
|
|
558
|
-
&& network.differentPublicEgress === true
|
|
559
|
-
&& network.hubEndpointSource === 'trusted-rendezvous-observation',
|
|
560
|
-
'Authenticated UDP and trusted-rendezvous-observed distinct public IPv4 egress values are required; this does not prove distinct NAT devices.');
|
|
561
|
-
add(
|
|
562
|
-
'fresh-hub-public-ipv4-observation',
|
|
563
|
-
timestampWithin(
|
|
564
|
-
network.hubAddressObservedAt,
|
|
565
|
-
snapshot?.startedAt,
|
|
566
|
-
MAX_HUB_ADDRESS_OBSERVATION_SKEW_MS),
|
|
567
|
-
'The Hub public IPv4 value must be freshly observed within 60 seconds of diagnostic start.');
|
|
568
|
-
add(
|
|
569
|
-
'fresh-client-udp-observation',
|
|
570
|
-
timestampWithin(
|
|
571
|
-
network.observedAt,
|
|
572
|
-
snapshot?.startedAt,
|
|
573
|
-
MAX_HUB_ADDRESS_OBSERVATION_SKEW_MS),
|
|
574
|
-
'The authenticated Client UDP endpoint must be observed within 60 seconds of diagnostic start.');
|
|
575
|
-
|
|
576
|
-
const cleanup = Array.isArray(snapshot?.cleanup) ? snapshot.cleanup : [];
|
|
577
|
-
add(
|
|
578
|
-
'lease-cleaned',
|
|
579
|
-
snapshot?.state === 'completed'
|
|
580
|
-
&& cleanup.some(entry => entry?.state === 'agent-released'),
|
|
581
|
-
'The exact-owner diagnostic lease must be released by the Agent.');
|
|
582
|
-
|
|
583
|
-
const baseline = snapshot?.baseline || {};
|
|
584
|
-
const agentTelemetryAvailable = baseline.agentQueueTelemetryAvailable === true
|
|
585
|
-
&& optionalFinite(baseline.agentQueueDropped) !== null
|
|
586
|
-
&& observedFrames.every(frame =>
|
|
587
|
-
frame?.agentQueueTelemetryAvailable === true
|
|
588
|
-
&& optionalFinite(frame.agentQueueDepth) !== null
|
|
589
|
-
&& optionalFinite(frame.agentQueueDropped) !== null);
|
|
590
|
-
const hubTelemetryAvailable = baseline.hubIngressTelemetryAvailable === true
|
|
591
|
-
&& optionalFinite(baseline.hubIngressDropped) !== null
|
|
592
|
-
&& !!baseline.hubIngressCounterEpochHash
|
|
593
|
-
&& !!baseline.udpCounterEpochHash
|
|
594
|
-
&& observedFrames.every(frame =>
|
|
595
|
-
frame?.hubIngressTelemetryAvailable === true
|
|
596
|
-
&& optionalFinite(frame.hubIngressDropped) !== null
|
|
597
|
-
&& !!frame.hubIngressCounterEpochHash);
|
|
598
|
-
const p2pFrame = observedFrames[REQUIRED_PHASES.indexOf('p2p')];
|
|
599
|
-
const udpTelemetryAvailable = baseline.udpTelemetryAvailable === true
|
|
600
|
-
&& optionalFinite(baseline.udpIncompleteFrames) !== null
|
|
601
|
-
&& p2pFrame?.udpTelemetryAvailable === true
|
|
602
|
-
&& optionalFinite(p2pFrame?.udpIncompleteFrames) !== null
|
|
603
|
-
&& p2pFrame?.hubIngressCounterEpochHash === baseline.udpCounterEpochHash;
|
|
604
|
-
add(
|
|
605
|
-
'agent-queue-telemetry',
|
|
606
|
-
agentTelemetryAvailable,
|
|
607
|
-
agentTelemetryAvailable ? 'baseline and all four phases' : 'unknown or incomplete');
|
|
608
|
-
add(
|
|
609
|
-
'hub-ingress-telemetry',
|
|
610
|
-
hubTelemetryAvailable,
|
|
611
|
-
hubTelemetryAvailable ? 'baseline and all four phases' : 'unknown or incomplete');
|
|
612
|
-
add(
|
|
613
|
-
'udp-reassembly-telemetry',
|
|
614
|
-
udpTelemetryAvailable,
|
|
615
|
-
udpTelemetryAvailable ? 'baseline and P2P phase' : 'unknown or incomplete');
|
|
616
|
-
|
|
617
|
-
const agentDropped = cumulativeDelta([
|
|
618
|
-
baseline.agentQueueDropped,
|
|
619
|
-
...observedFrames.map(frame => frame?.agentQueueDropped)
|
|
620
|
-
]);
|
|
621
|
-
const hubDropped = scopedCounterDelta([
|
|
622
|
-
{
|
|
623
|
-
scope: baseline.hubIngressCounterEpochHash,
|
|
624
|
-
value: baseline.hubIngressDropped
|
|
625
|
-
},
|
|
626
|
-
{
|
|
627
|
-
scope: baseline.udpCounterEpochHash,
|
|
628
|
-
value: baseline.udpIncompleteFrames
|
|
629
|
-
}
|
|
630
|
-
], observedFrames.map(frame => ({
|
|
631
|
-
scope: frame?.hubIngressCounterEpochHash,
|
|
632
|
-
value: frame?.hubIngressDropped
|
|
633
|
-
})));
|
|
634
|
-
const udpIncomplete = cumulativeDelta([
|
|
635
|
-
baseline.udpIncompleteFrames,
|
|
636
|
-
p2pFrame?.udpIncompleteFrames
|
|
637
|
-
]);
|
|
638
|
-
const maxAgentQueueDepth = agentTelemetryAvailable
|
|
639
|
-
? Math.max(...observedFrames.map(frame => Number(frame.agentQueueDepth)))
|
|
640
|
-
: null;
|
|
641
|
-
const boundedQueues = agentTelemetryAvailable
|
|
642
|
-
&& hubTelemetryAvailable
|
|
643
|
-
&& udpTelemetryAvailable
|
|
644
|
-
&& agentDropped.valid
|
|
645
|
-
&& hubDropped.valid
|
|
646
|
-
&& udpIncomplete.valid
|
|
647
|
-
&& maxAgentQueueDepth <= MAX_AGENT_QUEUE_DEPTH
|
|
648
|
-
&& agentDropped.delta <= MAX_AGENT_QUEUE_DROPPED_DELTA
|
|
649
|
-
&& hubDropped.delta <= MAX_HUB_INGRESS_DROPPED_DELTA
|
|
650
|
-
&& udpIncomplete.delta <= MAX_UDP_INCOMPLETE_DELTA;
|
|
651
|
-
add(
|
|
652
|
-
'bounded-frame-queues',
|
|
653
|
-
boundedQueues,
|
|
654
|
-
`queueDepth=${maxAgentQueueDepth ?? 'unknown'}, `
|
|
655
|
-
+ `agentDroppedDelta=${agentDropped.delta ?? 'unknown'}, `
|
|
656
|
-
+ `hubDroppedDelta=${hubDropped.delta ?? 'unknown'}, `
|
|
657
|
-
+ `udpIncompleteDelta=${udpIncomplete.delta ?? 'unknown'}`);
|
|
658
|
-
|
|
659
|
-
const failed = checks.filter(check => !check.ok);
|
|
660
|
-
const phaseLatencies = observedFrames.map(frame => optionalFinite(frame?.phaseLatencyMs));
|
|
661
|
-
return {
|
|
662
|
-
status: failed.length === 0 ? 'passed' : 'blocked',
|
|
663
|
-
summary: failed.length === 0
|
|
664
|
-
? 'Direct, UDP P2P, encrypted relay, and Direct recovery were proven by independent received H.264 access units.'
|
|
665
|
-
: `Physical transport proof is incomplete (${failed.map(check => check.id).join(', ')}).`,
|
|
666
|
-
checks,
|
|
667
|
-
metrics: {
|
|
668
|
-
maxPhaseLatencyMs: phaseLatencies.every(value => value !== null)
|
|
669
|
-
? Math.max(...phaseLatencies)
|
|
670
|
-
: null,
|
|
671
|
-
maxAgentQueueDepth,
|
|
672
|
-
agentQueueDroppedDelta: agentDropped.delta,
|
|
673
|
-
hubIngressDroppedDelta: hubDropped.delta,
|
|
674
|
-
udpIncompleteFramesDelta: udpIncomplete.delta,
|
|
675
|
-
observedTransports: observedFrames.map(frame => hubObservedTransport(frame).label)
|
|
676
|
-
}
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
export function buildTransportPhysicalReport(snapshot, context = {}) {
|
|
681
|
-
const classified = classifyTransportPhysicalSnapshot(snapshot);
|
|
682
|
-
const report = redactTransportPhysicalReport({
|
|
683
|
-
schema: 'livedesk.transport-physical.v2',
|
|
684
|
-
generatedAt: new Date().toISOString(),
|
|
685
|
-
hubUrl: context.hubUrl,
|
|
686
|
-
selectedDeviceId: context.deviceId,
|
|
687
|
-
provenance: buildTransportPhysicalProvenance(context.provenance),
|
|
688
|
-
result: classified,
|
|
689
|
-
transport: snapshot
|
|
690
|
-
});
|
|
691
|
-
const leaks = findTransportPhysicalReportLeaks(report);
|
|
692
|
-
if (leaks.length > 0) {
|
|
693
|
-
throw new Error(`transport-physical-report-redaction-failed:${leaks.join(',')}`);
|
|
694
|
-
}
|
|
695
|
-
return report;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
export {
|
|
699
|
-
MAX_HUB_ADDRESS_OBSERVATION_SKEW_MS,
|
|
700
|
-
MAX_PHASE_LATENCY_MS,
|
|
701
|
-
REQUIRED_PHASES
|
|
702
|
-
};
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
|
|
4
|
+
const REQUIRED_PHASES = Object.freeze([
|
|
5
|
+
'direct',
|
|
6
|
+
'p2p',
|
|
7
|
+
'relay',
|
|
8
|
+
'recover-direct'
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const MAX_PHASE_LATENCY_MS = 15_000;
|
|
12
|
+
const MAX_AGENT_QUEUE_DEPTH = 8;
|
|
13
|
+
const MAX_AGENT_QUEUE_DROPPED_DELTA = 8;
|
|
14
|
+
const MAX_HUB_INGRESS_DROPPED_DELTA = 8;
|
|
15
|
+
const MAX_UDP_INCOMPLETE_DELTA = 2;
|
|
16
|
+
const MAX_HUB_ADDRESS_OBSERVATION_SKEW_MS = 60_000;
|
|
17
|
+
|
|
18
|
+
const SECRET_KEYS = new Set([
|
|
19
|
+
'authorization',
|
|
20
|
+
'bearer',
|
|
21
|
+
'token',
|
|
22
|
+
'secret',
|
|
23
|
+
'password',
|
|
24
|
+
'apikey',
|
|
25
|
+
'pairtoken'
|
|
26
|
+
]);
|
|
27
|
+
const SAFE_NETWORK_KEYS = new Set([
|
|
28
|
+
'clientaddresshash',
|
|
29
|
+
'hubaddresshash',
|
|
30
|
+
'clientaddresspublic',
|
|
31
|
+
'hubaddresspublic',
|
|
32
|
+
'hubaddressobservedat',
|
|
33
|
+
'endpointauthenticated',
|
|
34
|
+
'differentpublicegress',
|
|
35
|
+
'evidence',
|
|
36
|
+
'observedat',
|
|
37
|
+
'rendezvousport',
|
|
38
|
+
'udpready'
|
|
39
|
+
]);
|
|
40
|
+
const IDENTIFIER_KEYS = new Set([
|
|
41
|
+
'leaseid',
|
|
42
|
+
'deviceid',
|
|
43
|
+
'selecteddeviceid',
|
|
44
|
+
'streamid',
|
|
45
|
+
'streamcommandid',
|
|
46
|
+
'commandid',
|
|
47
|
+
'sessionid',
|
|
48
|
+
'requestid',
|
|
49
|
+
'connectionid',
|
|
50
|
+
'hubconnectionid',
|
|
51
|
+
'inputeventid',
|
|
52
|
+
'clientid',
|
|
53
|
+
'ownerid'
|
|
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
|
+
]);
|
|
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;
|
|
69
|
+
const IPV4_TEXT = /(^|[^\d])(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?($|[^\d])/;
|
|
70
|
+
const IPV6_CANDIDATE_TEXT = /\[[0-9a-f:.%]+\](?::\d{1,5})?|[0-9a-f:%]*:[0-9a-f:.%:]*/gi;
|
|
71
|
+
const URL_TEXT = /\b[a-z][a-z0-9+.-]*:\/\/[^\s]+/i;
|
|
72
|
+
const HOST_TEXT = /\b(?:localhost|(?:[a-z0-9-]+\.)+[a-z]{2,63})(?::\d{1,5})?\b/i;
|
|
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;
|
|
114
|
+
|
|
115
|
+
function normalizedKey(key) {
|
|
116
|
+
return String(key || '').replace(/[-_]/g, '').toLowerCase();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isIdentifierKey(key) {
|
|
120
|
+
return IDENTIFIER_KEYS.has(normalizedKey(key));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isSecretKey(key) {
|
|
124
|
+
const normalized = normalizedKey(key);
|
|
125
|
+
return SECRET_KEYS.has(normalized)
|
|
126
|
+
|| normalized.endsWith('token')
|
|
127
|
+
|| normalized.endsWith('secret')
|
|
128
|
+
|| normalized.endsWith('password')
|
|
129
|
+
|| normalized.endsWith('apikey');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isRawFilesystemKey(key) {
|
|
133
|
+
return FILESYSTEM_KEYS.has(normalizedKey(key));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isRawNetworkKey(key) {
|
|
137
|
+
const rawKey = String(key || '');
|
|
138
|
+
const normalized = normalizedKey(key);
|
|
139
|
+
if (SAFE_NETWORK_KEYS.has(normalized) || normalized.endsWith('hash')) return false;
|
|
140
|
+
return normalized === 'address'
|
|
141
|
+
|| normalized.endsWith('address')
|
|
142
|
+
|| normalized === 'endpoint'
|
|
143
|
+
|| normalized.endsWith('endpoint')
|
|
144
|
+
|| normalized === 'host'
|
|
145
|
+
|| normalized.endsWith('host')
|
|
146
|
+
|| normalized === 'port'
|
|
147
|
+
|| /(?:^|[-_])port$/i.test(rawKey)
|
|
148
|
+
|| /[A-Z]Port$/.test(rawKey)
|
|
149
|
+
|| normalized === 'url'
|
|
150
|
+
|| normalized.endsWith('url')
|
|
151
|
+
|| normalized === 'uri'
|
|
152
|
+
|| normalized.endsWith('uri')
|
|
153
|
+
|| normalized === 'origin'
|
|
154
|
+
|| normalized.endsWith('origin');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function containsIpv6Text(value) {
|
|
158
|
+
const candidates = String(value || '').match(IPV6_CANDIDATE_TEXT) || [];
|
|
159
|
+
return candidates.some(candidate => {
|
|
160
|
+
const bracketed = candidate.match(/^\[([^\]]+)\](?::\d{1,5})?$/);
|
|
161
|
+
const address = String(bracketed?.[1] || candidate)
|
|
162
|
+
.replace(/%[a-z0-9_.-]+$/i, '')
|
|
163
|
+
.replace(/[.,;]+$/g, '');
|
|
164
|
+
return isIP(address) === 6;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function containsRawNetworkText(value) {
|
|
169
|
+
const text = String(value || '');
|
|
170
|
+
return IPV4_TEXT.test(text)
|
|
171
|
+
|| containsIpv6Text(text)
|
|
172
|
+
|| URL_TEXT.test(text)
|
|
173
|
+
|| HOST_TEXT.test(text);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function identifierMarker(value) {
|
|
177
|
+
if (value === null || value === undefined || String(value) === '') return '';
|
|
178
|
+
return '[redacted-id]';
|
|
179
|
+
}
|
|
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
|
+
|
|
314
|
+
export function genericTransportPhysicalError(value, fallback = 'transport-diagnostic-error') {
|
|
315
|
+
const text = String(value instanceof Error ? value.message : value || '').trim();
|
|
316
|
+
if (!text
|
|
317
|
+
|| containsRawNetworkText(text)
|
|
318
|
+
|| SECRET_TEXT.test(text)
|
|
319
|
+
|| UUID_TEXT.test(text)) return fallback;
|
|
320
|
+
if (!/^[a-z0-9][a-z0-9 _.,:;()[\]/'"+=-]{0,239}$/i.test(text)) return fallback;
|
|
321
|
+
return text;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function redactTransportPhysicalReport(value, key = '') {
|
|
325
|
+
if (isSecretKey(key)) return '[redacted]';
|
|
326
|
+
if (isIdentifierKey(key)) return identifierMarker(value);
|
|
327
|
+
if (isRawFilesystemKey(key)) return '[redacted-path]';
|
|
328
|
+
if (isRawNetworkKey(key)) return '[redacted-network]';
|
|
329
|
+
if (Array.isArray(value)) {
|
|
330
|
+
return value.map(item => redactTransportPhysicalReport(item));
|
|
331
|
+
}
|
|
332
|
+
if (value && typeof value === 'object') {
|
|
333
|
+
return Object.fromEntries(Object.entries(value)
|
|
334
|
+
.filter(([childKey]) => !isSecretKey(childKey))
|
|
335
|
+
.map(([childKey, childValue]) => [
|
|
336
|
+
childKey,
|
|
337
|
+
redactTransportPhysicalReport(childValue, childKey)
|
|
338
|
+
]));
|
|
339
|
+
}
|
|
340
|
+
if (typeof value === 'string'
|
|
341
|
+
&& !isAllowedStructuredScalar(key, value)
|
|
342
|
+
&& (containsRawNetworkText(value)
|
|
343
|
+
|| FILESYSTEM_TEXT.test(value)
|
|
344
|
+
|| SECRET_TEXT.test(value)
|
|
345
|
+
|| UUID_TEXT.test(value))) {
|
|
346
|
+
return /error|reason|detail|summary/i.test(key)
|
|
347
|
+
? '[redacted-sensitive-error]'
|
|
348
|
+
: '[redacted-sensitive]';
|
|
349
|
+
}
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function findTransportPhysicalReportLeaks(value, path = '$', leaks = []) {
|
|
354
|
+
if (Array.isArray(value)) {
|
|
355
|
+
value.forEach((item, index) => findTransportPhysicalReportLeaks(item, `${path}[${index}]`, leaks));
|
|
356
|
+
return leaks;
|
|
357
|
+
}
|
|
358
|
+
if (value && typeof value === 'object') {
|
|
359
|
+
for (const [key, child] of Object.entries(value)) {
|
|
360
|
+
if (isSecretKey(key)
|
|
361
|
+
|| (isRawFilesystemKey(key) && child !== '[redacted-path]')
|
|
362
|
+
|| (isRawNetworkKey(key) && child !== '[redacted-network]')) {
|
|
363
|
+
leaks.push(`${path}.${key}`);
|
|
364
|
+
}
|
|
365
|
+
if (isIdentifierKey(key)
|
|
366
|
+
&& child !== ''
|
|
367
|
+
&& child !== '[redacted-id]') {
|
|
368
|
+
leaks.push(`${path}.${key}`);
|
|
369
|
+
}
|
|
370
|
+
findTransportPhysicalReportLeaks(child, `${path}.${key}`, leaks);
|
|
371
|
+
}
|
|
372
|
+
return leaks;
|
|
373
|
+
}
|
|
374
|
+
if (typeof value === 'string'
|
|
375
|
+
&& !isAllowedStructuredScalar(path.split('.').at(-1)?.replace(/\[\d+\]$/, ''), value)
|
|
376
|
+
&& (containsRawNetworkText(value)
|
|
377
|
+
|| FILESYSTEM_TEXT.test(value)
|
|
378
|
+
|| SECRET_TEXT.test(value)
|
|
379
|
+
|| UUID_TEXT.test(value))) {
|
|
380
|
+
leaks.push(path);
|
|
381
|
+
}
|
|
382
|
+
return leaks;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function optionalFinite(value) {
|
|
386
|
+
if (value === null || value === undefined || value === '') return null;
|
|
387
|
+
const parsed = Number(value);
|
|
388
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function phaseFrame(snapshot, phase) {
|
|
392
|
+
return Array.isArray(snapshot?.frames)
|
|
393
|
+
? snapshot.frames.find(frame => frame?.phase === phase && frame?.accepted === true)
|
|
394
|
+
: null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function expectedPathForPhase(phase) {
|
|
398
|
+
if (phase === 'p2p') return 'p2p';
|
|
399
|
+
if (phase === 'relay') return 'relay';
|
|
400
|
+
return 'direct';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function hubObservedTransport(frame) {
|
|
404
|
+
const transport = String(frame?.transport || '').trim().toLowerCase();
|
|
405
|
+
if (transport === 'udp-p2p') return { path: 'p2p', label: 'udp-p2p' };
|
|
406
|
+
if (transport === 'relay-binary') return { path: 'relay', label: 'relay-binary' };
|
|
407
|
+
if (transport === 'binary' || transport === 'tcp-binary') {
|
|
408
|
+
return { path: 'direct', label: 'direct-tcp' };
|
|
409
|
+
}
|
|
410
|
+
if (transport === 'ws-binary') return { path: 'direct', label: 'direct-ws' };
|
|
411
|
+
return { path: '', label: 'unknown' };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function isTransportPhysicalPhaseEvidence(frame, phase) {
|
|
415
|
+
const expectedPath = expectedPathForPhase(phase);
|
|
416
|
+
const observed = hubObservedTransport(frame);
|
|
417
|
+
const phaseLatencyMs = optionalFinite(frame?.phaseLatencyMs);
|
|
418
|
+
return frame?.accepted === true
|
|
419
|
+
&& frame?.phase === phase
|
|
420
|
+
&& frame?.actualPath === expectedPath
|
|
421
|
+
&& observed.path === expectedPath
|
|
422
|
+
&& frame?.isKeyFrame === true
|
|
423
|
+
&& frame?.annexB === true
|
|
424
|
+
&& frame?.hasIdr === true
|
|
425
|
+
&& frame?.hasSps === true
|
|
426
|
+
&& frame?.hasPps === true
|
|
427
|
+
&& frame?.nalOrderValid === true
|
|
428
|
+
&& optionalFinite(frame?.byteLength) > 0
|
|
429
|
+
&& phaseLatencyMs !== null
|
|
430
|
+
&& phaseLatencyMs >= 0
|
|
431
|
+
&& phaseLatencyMs <= MAX_PHASE_LATENCY_MS;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function cumulativeDelta(values) {
|
|
435
|
+
if (!Array.isArray(values) || values.length < 2) {
|
|
436
|
+
return { valid: false, delta: null };
|
|
437
|
+
}
|
|
438
|
+
const normalized = values.map(optionalFinite);
|
|
439
|
+
const valid = normalized.every(value => value !== null && value >= 0)
|
|
440
|
+
&& normalized.every((value, index) => index === 0 || value >= normalized[index - 1]);
|
|
441
|
+
return {
|
|
442
|
+
valid,
|
|
443
|
+
delta: valid ? normalized.at(-1) - normalized[0] : null
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function scopedCounterDelta(baselines, samples) {
|
|
448
|
+
const scopes = new Map();
|
|
449
|
+
for (const baseline of baselines || []) {
|
|
450
|
+
const scope = String(baseline?.scope || '');
|
|
451
|
+
const value = optionalFinite(baseline?.value);
|
|
452
|
+
if (!scope || value === null || value < 0 || scopes.has(scope)) {
|
|
453
|
+
return { valid: false, delta: null };
|
|
454
|
+
}
|
|
455
|
+
scopes.set(scope, { initial: value, last: value });
|
|
456
|
+
}
|
|
457
|
+
let total = 0;
|
|
458
|
+
for (const sample of samples || []) {
|
|
459
|
+
const scope = String(sample?.scope || '');
|
|
460
|
+
const value = optionalFinite(sample?.value);
|
|
461
|
+
if (!scope || value === null || value < 0) {
|
|
462
|
+
return { valid: false, delta: null };
|
|
463
|
+
}
|
|
464
|
+
const state = scopes.get(scope) || { initial: 0, last: 0 };
|
|
465
|
+
if (value < state.last) {
|
|
466
|
+
return { valid: false, delta: null };
|
|
467
|
+
}
|
|
468
|
+
state.last = value;
|
|
469
|
+
scopes.set(scope, state);
|
|
470
|
+
}
|
|
471
|
+
for (const state of scopes.values()) {
|
|
472
|
+
total += state.last - state.initial;
|
|
473
|
+
}
|
|
474
|
+
return { valid: true, delta: total };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function timestampWithin(value, reference, maximumSkewMs) {
|
|
478
|
+
const observedAt = Date.parse(String(value || ''));
|
|
479
|
+
const referenceAt = Date.parse(String(reference || ''));
|
|
480
|
+
return Number.isFinite(observedAt)
|
|
481
|
+
&& Number.isFinite(referenceAt)
|
|
482
|
+
&& Math.abs(observedAt - referenceAt) <= maximumSkewMs;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function classifyTransportPhysicalSnapshot(snapshot) {
|
|
486
|
+
const checks = [];
|
|
487
|
+
const add = (id, ok, detail) => checks.push({ id, ok: ok === true, detail });
|
|
488
|
+
const owner = snapshot?.owner || {};
|
|
489
|
+
|
|
490
|
+
add(
|
|
491
|
+
'same-current-owner',
|
|
492
|
+
!!owner.streamId
|
|
493
|
+
&& !!owner.streamCommandId
|
|
494
|
+
&& Number(owner.captureGeneration || 0) > 0
|
|
495
|
+
&& Number(owner.monitorIndex) >= 0,
|
|
496
|
+
'One immutable stream, command, generation, and monitor must own the complete run.');
|
|
497
|
+
|
|
498
|
+
const observedFrames = REQUIRED_PHASES.map(phase => phaseFrame(snapshot, phase));
|
|
499
|
+
for (let index = 0; index < REQUIRED_PHASES.length; index += 1) {
|
|
500
|
+
const phase = REQUIRED_PHASES[index];
|
|
501
|
+
const frame = observedFrames[index];
|
|
502
|
+
const expectedPath = expectedPathForPhase(phase);
|
|
503
|
+
const observed = hubObservedTransport(frame);
|
|
504
|
+
add(
|
|
505
|
+
`received-h264-${phase}`,
|
|
506
|
+
!!frame
|
|
507
|
+
&& frame.actualPath === expectedPath
|
|
508
|
+
&& frame.isKeyFrame === true
|
|
509
|
+
&& frame.annexB === true
|
|
510
|
+
&& frame.hasIdr === true
|
|
511
|
+
&& frame.hasSps === true
|
|
512
|
+
&& frame.hasPps === true
|
|
513
|
+
&& frame.nalOrderValid === true
|
|
514
|
+
&& optionalFinite(frame.byteLength) > 0,
|
|
515
|
+
`A received Annex-B H.264 SPS -> PPS -> IDR access unit must prove ${phase}.`);
|
|
516
|
+
add(
|
|
517
|
+
`hub-observed-transport-${phase}`,
|
|
518
|
+
!!frame && observed.path === expectedPath,
|
|
519
|
+
`Hub ingress must observe ${phase} as ${observed.label}.`);
|
|
520
|
+
const phaseLatencyMs = optionalFinite(frame?.phaseLatencyMs);
|
|
521
|
+
add(
|
|
522
|
+
`phase-latency-${phase}`,
|
|
523
|
+
phaseLatencyMs !== null
|
|
524
|
+
&& phaseLatencyMs >= 0
|
|
525
|
+
&& phaseLatencyMs <= MAX_PHASE_LATENCY_MS,
|
|
526
|
+
`${phaseLatencyMs === null ? 'unknown' : Math.round(phaseLatencyMs)}ms/15000ms`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const receivedTimes = observedFrames.map(frame => Date.parse(frame?.receivedAt || ''));
|
|
530
|
+
add(
|
|
531
|
+
'ordered-path-transition',
|
|
532
|
+
observedFrames.every(Boolean)
|
|
533
|
+
&& receivedTimes.every(Number.isFinite)
|
|
534
|
+
&& receivedTimes.every((time, index) => index === 0 || time > receivedTimes[index - 1]),
|
|
535
|
+
'Frames must arrive in strict Direct, UDP P2P, encrypted relay, Direct order.');
|
|
536
|
+
|
|
537
|
+
const frameSequences = observedFrames.map(frame => optionalFinite(frame?.frameSeq));
|
|
538
|
+
add(
|
|
539
|
+
'strict-frame-sequence',
|
|
540
|
+
frameSequences.every(value => Number.isSafeInteger(value) && value > 0)
|
|
541
|
+
&& frameSequences.every((value, index) => index === 0 || value > frameSequences[index - 1]),
|
|
542
|
+
frameSequences.map(value => value ?? '?').join(' -> '));
|
|
543
|
+
|
|
544
|
+
const switchCounts = observedFrames.map(frame => optionalFinite(frame?.switchCount));
|
|
545
|
+
add(
|
|
546
|
+
'transport-switch-count',
|
|
547
|
+
switchCounts.every(value => Number.isSafeInteger(value) && value >= 0)
|
|
548
|
+
&& switchCounts.every((value, index) => index === 0 || value > switchCounts[index - 1]),
|
|
549
|
+
switchCounts.map(value => value ?? '?').join(' -> '));
|
|
550
|
+
|
|
551
|
+
const network = snapshot?.network || {};
|
|
552
|
+
add(
|
|
553
|
+
'distinct-public-ipv4-egress',
|
|
554
|
+
network.evidence === 'authenticated-udp-endpoint-rendezvous-observed'
|
|
555
|
+
&& network.endpointAuthenticated === true
|
|
556
|
+
&& network.clientAddressPublic === true
|
|
557
|
+
&& network.hubAddressPublic === true
|
|
558
|
+
&& network.differentPublicEgress === true
|
|
559
|
+
&& network.hubEndpointSource === 'trusted-rendezvous-observation',
|
|
560
|
+
'Authenticated UDP and trusted-rendezvous-observed distinct public IPv4 egress values are required; this does not prove distinct NAT devices.');
|
|
561
|
+
add(
|
|
562
|
+
'fresh-hub-public-ipv4-observation',
|
|
563
|
+
timestampWithin(
|
|
564
|
+
network.hubAddressObservedAt,
|
|
565
|
+
snapshot?.startedAt,
|
|
566
|
+
MAX_HUB_ADDRESS_OBSERVATION_SKEW_MS),
|
|
567
|
+
'The Hub public IPv4 value must be freshly observed within 60 seconds of diagnostic start.');
|
|
568
|
+
add(
|
|
569
|
+
'fresh-client-udp-observation',
|
|
570
|
+
timestampWithin(
|
|
571
|
+
network.observedAt,
|
|
572
|
+
snapshot?.startedAt,
|
|
573
|
+
MAX_HUB_ADDRESS_OBSERVATION_SKEW_MS),
|
|
574
|
+
'The authenticated Client UDP endpoint must be observed within 60 seconds of diagnostic start.');
|
|
575
|
+
|
|
576
|
+
const cleanup = Array.isArray(snapshot?.cleanup) ? snapshot.cleanup : [];
|
|
577
|
+
add(
|
|
578
|
+
'lease-cleaned',
|
|
579
|
+
snapshot?.state === 'completed'
|
|
580
|
+
&& cleanup.some(entry => entry?.state === 'agent-released'),
|
|
581
|
+
'The exact-owner diagnostic lease must be released by the Agent.');
|
|
582
|
+
|
|
583
|
+
const baseline = snapshot?.baseline || {};
|
|
584
|
+
const agentTelemetryAvailable = baseline.agentQueueTelemetryAvailable === true
|
|
585
|
+
&& optionalFinite(baseline.agentQueueDropped) !== null
|
|
586
|
+
&& observedFrames.every(frame =>
|
|
587
|
+
frame?.agentQueueTelemetryAvailable === true
|
|
588
|
+
&& optionalFinite(frame.agentQueueDepth) !== null
|
|
589
|
+
&& optionalFinite(frame.agentQueueDropped) !== null);
|
|
590
|
+
const hubTelemetryAvailable = baseline.hubIngressTelemetryAvailable === true
|
|
591
|
+
&& optionalFinite(baseline.hubIngressDropped) !== null
|
|
592
|
+
&& !!baseline.hubIngressCounterEpochHash
|
|
593
|
+
&& !!baseline.udpCounterEpochHash
|
|
594
|
+
&& observedFrames.every(frame =>
|
|
595
|
+
frame?.hubIngressTelemetryAvailable === true
|
|
596
|
+
&& optionalFinite(frame.hubIngressDropped) !== null
|
|
597
|
+
&& !!frame.hubIngressCounterEpochHash);
|
|
598
|
+
const p2pFrame = observedFrames[REQUIRED_PHASES.indexOf('p2p')];
|
|
599
|
+
const udpTelemetryAvailable = baseline.udpTelemetryAvailable === true
|
|
600
|
+
&& optionalFinite(baseline.udpIncompleteFrames) !== null
|
|
601
|
+
&& p2pFrame?.udpTelemetryAvailable === true
|
|
602
|
+
&& optionalFinite(p2pFrame?.udpIncompleteFrames) !== null
|
|
603
|
+
&& p2pFrame?.hubIngressCounterEpochHash === baseline.udpCounterEpochHash;
|
|
604
|
+
add(
|
|
605
|
+
'agent-queue-telemetry',
|
|
606
|
+
agentTelemetryAvailable,
|
|
607
|
+
agentTelemetryAvailable ? 'baseline and all four phases' : 'unknown or incomplete');
|
|
608
|
+
add(
|
|
609
|
+
'hub-ingress-telemetry',
|
|
610
|
+
hubTelemetryAvailable,
|
|
611
|
+
hubTelemetryAvailable ? 'baseline and all four phases' : 'unknown or incomplete');
|
|
612
|
+
add(
|
|
613
|
+
'udp-reassembly-telemetry',
|
|
614
|
+
udpTelemetryAvailable,
|
|
615
|
+
udpTelemetryAvailable ? 'baseline and P2P phase' : 'unknown or incomplete');
|
|
616
|
+
|
|
617
|
+
const agentDropped = cumulativeDelta([
|
|
618
|
+
baseline.agentQueueDropped,
|
|
619
|
+
...observedFrames.map(frame => frame?.agentQueueDropped)
|
|
620
|
+
]);
|
|
621
|
+
const hubDropped = scopedCounterDelta([
|
|
622
|
+
{
|
|
623
|
+
scope: baseline.hubIngressCounterEpochHash,
|
|
624
|
+
value: baseline.hubIngressDropped
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
scope: baseline.udpCounterEpochHash,
|
|
628
|
+
value: baseline.udpIncompleteFrames
|
|
629
|
+
}
|
|
630
|
+
], observedFrames.map(frame => ({
|
|
631
|
+
scope: frame?.hubIngressCounterEpochHash,
|
|
632
|
+
value: frame?.hubIngressDropped
|
|
633
|
+
})));
|
|
634
|
+
const udpIncomplete = cumulativeDelta([
|
|
635
|
+
baseline.udpIncompleteFrames,
|
|
636
|
+
p2pFrame?.udpIncompleteFrames
|
|
637
|
+
]);
|
|
638
|
+
const maxAgentQueueDepth = agentTelemetryAvailable
|
|
639
|
+
? Math.max(...observedFrames.map(frame => Number(frame.agentQueueDepth)))
|
|
640
|
+
: null;
|
|
641
|
+
const boundedQueues = agentTelemetryAvailable
|
|
642
|
+
&& hubTelemetryAvailable
|
|
643
|
+
&& udpTelemetryAvailable
|
|
644
|
+
&& agentDropped.valid
|
|
645
|
+
&& hubDropped.valid
|
|
646
|
+
&& udpIncomplete.valid
|
|
647
|
+
&& maxAgentQueueDepth <= MAX_AGENT_QUEUE_DEPTH
|
|
648
|
+
&& agentDropped.delta <= MAX_AGENT_QUEUE_DROPPED_DELTA
|
|
649
|
+
&& hubDropped.delta <= MAX_HUB_INGRESS_DROPPED_DELTA
|
|
650
|
+
&& udpIncomplete.delta <= MAX_UDP_INCOMPLETE_DELTA;
|
|
651
|
+
add(
|
|
652
|
+
'bounded-frame-queues',
|
|
653
|
+
boundedQueues,
|
|
654
|
+
`queueDepth=${maxAgentQueueDepth ?? 'unknown'}, `
|
|
655
|
+
+ `agentDroppedDelta=${agentDropped.delta ?? 'unknown'}, `
|
|
656
|
+
+ `hubDroppedDelta=${hubDropped.delta ?? 'unknown'}, `
|
|
657
|
+
+ `udpIncompleteDelta=${udpIncomplete.delta ?? 'unknown'}`);
|
|
658
|
+
|
|
659
|
+
const failed = checks.filter(check => !check.ok);
|
|
660
|
+
const phaseLatencies = observedFrames.map(frame => optionalFinite(frame?.phaseLatencyMs));
|
|
661
|
+
return {
|
|
662
|
+
status: failed.length === 0 ? 'passed' : 'blocked',
|
|
663
|
+
summary: failed.length === 0
|
|
664
|
+
? 'Direct, UDP P2P, encrypted relay, and Direct recovery were proven by independent received H.264 access units.'
|
|
665
|
+
: `Physical transport proof is incomplete (${failed.map(check => check.id).join(', ')}).`,
|
|
666
|
+
checks,
|
|
667
|
+
metrics: {
|
|
668
|
+
maxPhaseLatencyMs: phaseLatencies.every(value => value !== null)
|
|
669
|
+
? Math.max(...phaseLatencies)
|
|
670
|
+
: null,
|
|
671
|
+
maxAgentQueueDepth,
|
|
672
|
+
agentQueueDroppedDelta: agentDropped.delta,
|
|
673
|
+
hubIngressDroppedDelta: hubDropped.delta,
|
|
674
|
+
udpIncompleteFramesDelta: udpIncomplete.delta,
|
|
675
|
+
observedTransports: observedFrames.map(frame => hubObservedTransport(frame).label)
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function buildTransportPhysicalReport(snapshot, context = {}) {
|
|
681
|
+
const classified = classifyTransportPhysicalSnapshot(snapshot);
|
|
682
|
+
const report = redactTransportPhysicalReport({
|
|
683
|
+
schema: 'livedesk.transport-physical.v2',
|
|
684
|
+
generatedAt: new Date().toISOString(),
|
|
685
|
+
hubUrl: context.hubUrl,
|
|
686
|
+
selectedDeviceId: context.deviceId,
|
|
687
|
+
provenance: buildTransportPhysicalProvenance(context.provenance),
|
|
688
|
+
result: classified,
|
|
689
|
+
transport: snapshot
|
|
690
|
+
});
|
|
691
|
+
const leaks = findTransportPhysicalReportLeaks(report);
|
|
692
|
+
if (leaks.length > 0) {
|
|
693
|
+
throw new Error(`transport-physical-report-redaction-failed:${leaks.join(',')}`);
|
|
694
|
+
}
|
|
695
|
+
return report;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export {
|
|
699
|
+
MAX_HUB_ADDRESS_OBSERVATION_SKEW_MS,
|
|
700
|
+
MAX_PHASE_LATENCY_MS,
|
|
701
|
+
REQUIRED_PHASES
|
|
702
|
+
};
|