@livedesk/hub 0.1.16 → 0.1.18
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/package.json +1 -1
- package/src/agents/agent-manager.js +0 -9
- package/src/agents/agent-settings.js +4 -34
- package/src/agents/codex-agent-runtime.js +12 -14
- package/src/remote-hub.js +19 -6
- package/src/server.js +13 -11
- package/src/transport/udp-hub-transport.js +22 -22
- package/src/transport/udp-protocol.js +51 -26
package/package.json
CHANGED
|
@@ -94,15 +94,6 @@ export function createAgentManager({
|
|
|
94
94
|
async deleteApiKey() {
|
|
95
95
|
return publicSettings();
|
|
96
96
|
},
|
|
97
|
-
async getModels() {
|
|
98
|
-
return [{
|
|
99
|
-
id: '',
|
|
100
|
-
name: 'Codex default',
|
|
101
|
-
protocol: 'codex-sdk',
|
|
102
|
-
available: true,
|
|
103
|
-
capabilities: { supportsReasoningLevel: true, supportsStreaming: true, supportsTools: true }
|
|
104
|
-
}];
|
|
105
|
-
},
|
|
106
97
|
async testConnection() {
|
|
107
98
|
if (!runtime) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
108
99
|
statusCache = { expiresAt: 0, value: null };
|
|
@@ -4,14 +4,11 @@ import path from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
export const AGENT_PROVIDER_CODEX = 'codex';
|
|
6
6
|
export const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_CODEX;
|
|
7
|
-
export const DEFAULT_CODEX_MODEL_ID = '';
|
|
8
7
|
export const DEFAULT_AGENT_WORKSPACE = path.join(os.homedir(), '.livedesk', 'agent-workspace');
|
|
9
8
|
export const DEFAULT_AGENT_SETTINGS = Object.freeze({
|
|
10
|
-
settingsVersion:
|
|
9
|
+
settingsVersion: 4,
|
|
11
10
|
enabled: true,
|
|
12
11
|
provider: DEFAULT_AGENT_PROVIDER,
|
|
13
|
-
codexModelId: DEFAULT_CODEX_MODEL_ID,
|
|
14
|
-
codexReasoningEffort: 'medium',
|
|
15
12
|
codexSandboxMode: 'read-only',
|
|
16
13
|
codexApprovalPolicy: 'never',
|
|
17
14
|
codexWorkingDirectory: DEFAULT_AGENT_WORKSPACE,
|
|
@@ -25,20 +22,6 @@ export const DEFAULT_AGENT_SETTINGS = Object.freeze({
|
|
|
25
22
|
maxConcurrentRequests: 2
|
|
26
23
|
});
|
|
27
24
|
|
|
28
|
-
const REASONING_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
|
|
29
|
-
const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
|
|
30
|
-
const APPROVAL_POLICIES = new Set(['never', 'on-request', 'on-failure', 'untrusted']);
|
|
31
|
-
const WEB_SEARCH_MODES = new Set(['disabled', 'cached', 'live']);
|
|
32
|
-
|
|
33
|
-
export class AgentSettingsError extends Error {
|
|
34
|
-
constructor(code, message) {
|
|
35
|
-
super(message);
|
|
36
|
-
this.name = 'AgentSettingsError';
|
|
37
|
-
this.code = code;
|
|
38
|
-
this.status = 400;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
25
|
function booleanValue(value, fallback) {
|
|
43
26
|
return typeof value === 'boolean' ? value : fallback;
|
|
44
27
|
}
|
|
@@ -50,18 +33,7 @@ function numberValue(value, min, max, fallback, integer = false) {
|
|
|
50
33
|
return integer ? Math.round(clamped) : clamped;
|
|
51
34
|
}
|
|
52
35
|
|
|
53
|
-
function
|
|
54
|
-
if (typeof value !== 'string') return fallback;
|
|
55
|
-
return value.trim().slice(0, maxLength);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function enumValue(value, allowed, fallback, code) {
|
|
59
|
-
const normalized = stringValue(value, fallback, 80).toLowerCase();
|
|
60
|
-
if (!allowed.has(normalized)) throw new AgentSettingsError(code, `Invalid agent setting: ${normalized}.`);
|
|
61
|
-
return normalized;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizeWorkingDirectory(value) {
|
|
36
|
+
function normalizeWorkingDirectory() {
|
|
65
37
|
// The agent runtime owns this directory. Do not allow a browser/API caller
|
|
66
38
|
// to move Codex into a user project or an arbitrary filesystem root.
|
|
67
39
|
return DEFAULT_AGENT_WORKSPACE;
|
|
@@ -70,18 +42,16 @@ function normalizeWorkingDirectory(value) {
|
|
|
70
42
|
export function normalizeAgentSettings(value = {}) {
|
|
71
43
|
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
72
44
|
return {
|
|
73
|
-
settingsVersion:
|
|
45
|
+
settingsVersion: 4,
|
|
74
46
|
enabled: booleanValue(source.enabled, DEFAULT_AGENT_SETTINGS.enabled),
|
|
75
47
|
// LiveDesk has one Agent runtime. Old provider values are deliberately
|
|
76
48
|
// normalized to Codex so an upgrade cannot reactivate a retired provider.
|
|
77
49
|
provider: AGENT_PROVIDER_CODEX,
|
|
78
|
-
codexModelId: stringValue(source.codexModelId, DEFAULT_AGENT_SETTINGS.codexModelId, 160),
|
|
79
|
-
codexReasoningEffort: enumValue(source.codexReasoningEffort, REASONING_EFFORTS, DEFAULT_AGENT_SETTINGS.codexReasoningEffort, 'agent-invalid-reasoning-effort'),
|
|
80
50
|
// These are safety settings, not user-controlled execution toggles. They
|
|
81
51
|
// are accepted for compatibility but always normalized to the safe floor.
|
|
82
52
|
codexSandboxMode: 'read-only',
|
|
83
53
|
codexApprovalPolicy: 'never',
|
|
84
|
-
codexWorkingDirectory: normalizeWorkingDirectory(
|
|
54
|
+
codexWorkingDirectory: normalizeWorkingDirectory(),
|
|
85
55
|
codexTaskTimeoutMs: numberValue(source.codexTaskTimeoutMs, 30000, 1800000, DEFAULT_AGENT_SETTINGS.codexTaskTimeoutMs, true),
|
|
86
56
|
codexMaxTurns: numberValue(source.codexMaxTurns, 1, 40, DEFAULT_AGENT_SETTINGS.codexMaxTurns, true),
|
|
87
57
|
codexMaxToolCalls: numberValue(source.codexMaxToolCalls, 1, 100, DEFAULT_AGENT_SETTINGS.codexMaxToolCalls, true),
|
|
@@ -66,15 +66,13 @@ function normalizeCodexError(error) {
|
|
|
66
66
|
return new AgentProviderError('codex-run-failed', message, { status: 502, retryable: true });
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function codexThreadOptions(
|
|
70
|
-
return {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
modelReasoningEffort: settings.codexReasoningEffort,
|
|
77
|
-
networkAccessEnabled: false,
|
|
69
|
+
function codexThreadOptions(workspace) {
|
|
70
|
+
return {
|
|
71
|
+
sandboxMode: 'read-only',
|
|
72
|
+
approvalPolicy: 'never',
|
|
73
|
+
workingDirectory: workspace,
|
|
74
|
+
skipGitRepoCheck: true,
|
|
75
|
+
networkAccessEnabled: false,
|
|
78
76
|
webSearchMode: 'disabled',
|
|
79
77
|
webSearchEnabled: false,
|
|
80
78
|
additionalDirectories: []
|
|
@@ -417,10 +415,10 @@ export function createCodexAgentRuntime({
|
|
|
417
415
|
const controller = new AbortController();
|
|
418
416
|
const timer = setTimeout(() => controller.abort(), Math.min(30000, settings.codexTaskTimeoutMs));
|
|
419
417
|
try {
|
|
420
|
-
const thread = codex.startThread(codexThreadOptions(
|
|
421
|
-
const turn = await thread.run('Reply with the single word READY. Do not use tools.', { signal: controller.signal });
|
|
422
|
-
if (!String(turn.finalResponse || '').trim()) throw new AgentProviderError('codex-empty-response', 'Codex returned an empty response.', { status: 502 });
|
|
423
|
-
return { ok: true,
|
|
418
|
+
const thread = codex.startThread(codexThreadOptions(workspace));
|
|
419
|
+
const turn = await thread.run('Reply with the single word READY. Do not use tools.', { signal: controller.signal });
|
|
420
|
+
if (!String(turn.finalResponse || '').trim()) throw new AgentProviderError('codex-empty-response', 'Codex returned an empty response.', { status: 502 });
|
|
421
|
+
return { ok: true, latencyMs: Date.now() - startedAt, threadId: thread.id || '' };
|
|
424
422
|
} catch (error) {
|
|
425
423
|
throw normalizeCodexError(error);
|
|
426
424
|
} finally {
|
|
@@ -531,7 +529,7 @@ export function createCodexAgentRuntime({
|
|
|
531
529
|
if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
|
|
532
530
|
throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
|
|
533
531
|
}
|
|
534
|
-
const threadOptions = codexThreadOptions(
|
|
532
|
+
const threadOptions = codexThreadOptions(workspace);
|
|
535
533
|
run.status = 'running';
|
|
536
534
|
run.updatedAt = new Date().toISOString();
|
|
537
535
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
package/src/remote-hub.js
CHANGED
|
@@ -4232,9 +4232,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4232
4232
|
const captureTimingAvailable = hasFrameCaptureTiming(message);
|
|
4233
4233
|
const videoTransport = transport === 'udp-p2p'
|
|
4234
4234
|
? 'p2p-udp'
|
|
4235
|
-
: transport === '
|
|
4236
|
-
? '
|
|
4237
|
-
: '
|
|
4235
|
+
: transport === 'relay-binary'
|
|
4236
|
+
? 'encrypted-relay'
|
|
4237
|
+
: transport === 'ws-binary'
|
|
4238
|
+
? 'direct-ws'
|
|
4239
|
+
: 'direct-tcp';
|
|
4238
4240
|
device.latestLiveFrame = {
|
|
4239
4241
|
streamId,
|
|
4240
4242
|
frameSeq,
|
|
@@ -4322,9 +4324,16 @@ export function createRemoteHub(options = {}) {
|
|
|
4322
4324
|
deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
|
|
4323
4325
|
deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
|
|
4324
4326
|
videoTransport,
|
|
4325
|
-
frameTransportActive: safeString(message.frameTransportActive, 20)
|
|
4327
|
+
frameTransportActive: safeString(message.frameTransportActive, 20)
|
|
4328
|
+
|| (videoTransport === 'p2p-udp' ? 'p2p' : videoTransport === 'encrypted-relay' ? 'relay' : 'direct'),
|
|
4326
4329
|
frameTransportProtocol: safeString(message.frameTransportProtocol, 20)
|
|
4327
|
-
|| (videoTransport === 'p2p-udp'
|
|
4330
|
+
|| (videoTransport === 'p2p-udp'
|
|
4331
|
+
? 'udp'
|
|
4332
|
+
: videoTransport === 'encrypted-relay'
|
|
4333
|
+
? 'relay'
|
|
4334
|
+
: videoTransport === 'direct-ws'
|
|
4335
|
+
? 'ws'
|
|
4336
|
+
: 'tcp'),
|
|
4328
4337
|
frameTransportState: safeString(message.frameTransportState, 20) || 'active',
|
|
4329
4338
|
frameTransportP2pReady: message.frameTransportP2pReady === true,
|
|
4330
4339
|
frameTransportReason: safeString(message.frameTransportReason, 240),
|
|
@@ -4518,7 +4527,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4518
4527
|
}
|
|
4519
4528
|
|
|
4520
4529
|
const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
|
|
4521
|
-
const binaryTransport =
|
|
4530
|
+
const binaryTransport = socket.__liveDeskRelayControl === true
|
|
4531
|
+
? 'relay-binary'
|
|
4532
|
+
: state.binaryTransport === 'ws-binary'
|
|
4533
|
+
? 'ws-binary'
|
|
4534
|
+
: 'binary';
|
|
4522
4535
|
if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
|
|
4523
4536
|
applyThumbnailFrame(device, header, framePayload, binaryTransport);
|
|
4524
4537
|
return;
|
package/src/server.js
CHANGED
|
@@ -2127,9 +2127,11 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
2127
2127
|
transport: frame.transport || '',
|
|
2128
2128
|
videoTransport: frame.videoTransport || (frame.transport === 'udp-p2p'
|
|
2129
2129
|
? 'p2p-udp'
|
|
2130
|
-
: frame.transport === '
|
|
2131
|
-
? '
|
|
2132
|
-
: '
|
|
2130
|
+
: frame.transport === 'relay-binary'
|
|
2131
|
+
? 'encrypted-relay'
|
|
2132
|
+
: frame.transport === 'ws-binary'
|
|
2133
|
+
? 'direct-ws'
|
|
2134
|
+
: 'direct-tcp'),
|
|
2133
2135
|
frameTransportActive: frame.frameTransportActive || '',
|
|
2134
2136
|
frameTransportProtocol: frame.frameTransportProtocol || '',
|
|
2135
2137
|
frameTransportState: frame.frameTransportState || '',
|
|
@@ -2955,14 +2957,14 @@ app.delete('/api/settings/agent/api-key', async (_req, res) => {
|
|
|
2955
2957
|
}
|
|
2956
2958
|
});
|
|
2957
2959
|
|
|
2958
|
-
app.get('/api/settings/agent/models', async (_req, res) => {
|
|
2959
|
-
noStore(res);
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
}
|
|
2965
|
-
});
|
|
2960
|
+
app.get('/api/settings/agent/models', async (_req, res) => {
|
|
2961
|
+
noStore(res);
|
|
2962
|
+
res.status(410).json({
|
|
2963
|
+
ok: false,
|
|
2964
|
+
error: 'agent-model-catalog-retired',
|
|
2965
|
+
message: 'LiveDesk uses the Codex SDK defaults and does not manage models.'
|
|
2966
|
+
});
|
|
2967
|
+
});
|
|
2966
2968
|
|
|
2967
2969
|
app.post('/api/settings/agent/test', async (_req, res) => {
|
|
2968
2970
|
noStore(res);
|
|
@@ -2,10 +2,10 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { lookup } from 'node:dns/promises';
|
|
3
3
|
import dgram from 'node:dgram';
|
|
4
4
|
import {
|
|
5
|
-
UDP_MAX_DATAGRAM_BYTES,
|
|
6
|
-
UDP_P2P_PROTOCOL,
|
|
7
|
-
UdpFrameReassembler,
|
|
8
|
-
UdpReplayWindow,
|
|
5
|
+
UDP_MAX_DATAGRAM_BYTES,
|
|
6
|
+
UDP_P2P_PROTOCOL,
|
|
7
|
+
UdpFrameReassembler,
|
|
8
|
+
UdpReplayWindow,
|
|
9
9
|
decodeRendezvousMessage,
|
|
10
10
|
decodeUdpPacket,
|
|
11
11
|
encodeRendezvousMessage,
|
|
@@ -52,7 +52,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
52
52
|
const socket = dgram.createSocket('udp4');
|
|
53
53
|
const sessions = new Map();
|
|
54
54
|
let frameHandler = () => {};
|
|
55
|
-
let eventHandler = () => {};
|
|
55
|
+
let eventHandler = () => {};
|
|
56
56
|
let started = false;
|
|
57
57
|
let boundPort = requestedPort;
|
|
58
58
|
let sequence = crypto.randomInt(1, 0x7fffffff);
|
|
@@ -65,20 +65,20 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
65
65
|
if (trace) logEvent('udp', `${type} ${JSON.stringify({ deviceId: event.deviceId || '', state: event.state || '', reason: event.reason || '' })}`);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function nextSequence() {
|
|
69
|
-
sequence = (sequence + 1) >>> 0;
|
|
70
|
-
return sequence;
|
|
71
|
-
}
|
|
72
|
-
|
|
68
|
+
function nextSequence() {
|
|
69
|
+
sequence = (sequence + 1) >>> 0;
|
|
70
|
+
return sequence;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
73
|
function sendPacket(session, type, header = {}, payload = Buffer.alloc(0), address = session.clientEndpoint || session.peerEndpoint) {
|
|
74
74
|
if (!address || !session) return false;
|
|
75
|
-
let packet;
|
|
76
|
-
try {
|
|
77
|
-
packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
|
|
78
|
-
} catch (error) {
|
|
79
|
-
logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
75
|
+
let packet;
|
|
76
|
+
try {
|
|
77
|
+
packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
82
|
socket.send(packet, address.port, address.address);
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
@@ -288,9 +288,9 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
288
288
|
keyBase64: key.toString('base64'),
|
|
289
289
|
rendezvousToken: crypto.randomBytes(16).toString('hex'),
|
|
290
290
|
sendControl,
|
|
291
|
-
clientEndpoint: null,
|
|
292
|
-
peerEndpoint: null,
|
|
293
|
-
replay: new UdpReplayWindow(),
|
|
291
|
+
clientEndpoint: null,
|
|
292
|
+
peerEndpoint: null,
|
|
293
|
+
replay: new UdpReplayWindow(),
|
|
294
294
|
reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
|
|
295
295
|
ready: false,
|
|
296
296
|
createdAt: Date.now(),
|
|
@@ -333,8 +333,8 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
333
333
|
return {
|
|
334
334
|
enabled,
|
|
335
335
|
preferred: preferP2p,
|
|
336
|
-
state: session.ready ? 'udp-ready' : 'udp-session-created',
|
|
337
|
-
ready: session.ready,
|
|
336
|
+
state: session.ready ? 'udp-ready' : 'udp-session-created',
|
|
337
|
+
ready: session.ready,
|
|
338
338
|
sessionId: session.sessionId,
|
|
339
339
|
lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
|
|
340
340
|
framesReceived: session.framesReceived,
|
|
@@ -2,9 +2,11 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { inflateRawSync } from 'node:zlib';
|
|
3
3
|
|
|
4
4
|
export const UDP_P2P_PROTOCOL = 'livedesk.udp.p2p.v1';
|
|
5
|
-
export const UDP_P2P_VERSION = 1;
|
|
6
|
-
export const UDP_MAX_DATAGRAM_BYTES = 1200;
|
|
7
|
-
|
|
5
|
+
export const UDP_P2P_VERSION = 1;
|
|
6
|
+
export const UDP_MAX_DATAGRAM_BYTES = 1200;
|
|
7
|
+
const UDP_SERIAL_MODULUS = 0x1_0000_0000;
|
|
8
|
+
const UDP_SERIAL_HALF_RANGE = 0x8000_0000;
|
|
9
|
+
export const UDP_PACKET_TYPES = Object.freeze({
|
|
8
10
|
probe: 1,
|
|
9
11
|
'probe-ack': 2,
|
|
10
12
|
ready: 3,
|
|
@@ -83,13 +85,21 @@ function decodeFramedPayload(payload, legacyHeader = null) {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
function packetTypeCode(type) {
|
|
88
|
+
function packetTypeCode(type) {
|
|
87
89
|
const code = typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()];
|
|
88
90
|
if (!code || !UDP_PACKET_TYPE_NAMES.has(code)) throw new Error('invalid-udp-packet-type');
|
|
89
91
|
return code;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeSequence(value) {
|
|
95
|
+
const sequence = Number(value);
|
|
96
|
+
if (!Number.isInteger(sequence) || sequence < 0 || sequence >= UDP_SERIAL_MODULUS) {
|
|
97
|
+
throw new Error('invalid-udp-packet-sequence');
|
|
98
|
+
}
|
|
99
|
+
return sequence;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function encodeHeader(header) {
|
|
93
103
|
const bytes = Buffer.from(JSON.stringify(header && typeof header === 'object' ? header : {}), 'utf8');
|
|
94
104
|
if (bytes.length > UDP_MAX_HEADER_BYTES) throw new Error('udp-packet-header-too-large');
|
|
95
105
|
return bytes;
|
|
@@ -103,9 +113,9 @@ export function createUdpSessionKey() {
|
|
|
103
113
|
return crypto.randomBytes(32);
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
|
|
107
|
-
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
108
|
-
const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
|
|
116
|
+
export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
|
|
117
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
118
|
+
const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
|
|
109
119
|
const headerBytes = encodeHeader(header);
|
|
110
120
|
const payloadBytes = asBuffer(payload);
|
|
111
121
|
if (payloadBytes.length > UDP_MAX_PAYLOAD_BYTES) throw new Error('udp-packet-payload-too-large');
|
|
@@ -117,8 +127,8 @@ export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {
|
|
|
117
127
|
preamble[6] = sessionBytes.length;
|
|
118
128
|
preamble.writeUInt16BE(headerBytes.length, 7);
|
|
119
129
|
preamble.writeUInt16BE(payloadBytes.length, 9);
|
|
120
|
-
preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
|
|
121
|
-
const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
|
|
130
|
+
preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
|
|
131
|
+
const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
|
|
122
132
|
const associatedData = Buffer.concat([preamble, nonce, sessionBytes, headerBytes]);
|
|
123
133
|
const cipher = crypto.createCipheriv('aes-256-gcm', normalizeKey(key), nonce);
|
|
124
134
|
cipher.setAAD(associatedData);
|
|
@@ -169,26 +179,41 @@ export function decodeUdpPacket(packet, key) {
|
|
|
169
179
|
}
|
|
170
180
|
}
|
|
171
181
|
|
|
172
|
-
export class UdpReplayWindow {
|
|
182
|
+
export class UdpReplayWindow {
|
|
173
183
|
constructor(size = 512) {
|
|
174
184
|
this.size = Math.max(32, Math.min(4096, Math.floor(Number(size) || 512)));
|
|
175
185
|
this.highest = -1;
|
|
176
186
|
this.seen = new Set();
|
|
177
187
|
}
|
|
178
188
|
|
|
179
|
-
accept(sequence) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
accept(sequence) {
|
|
190
|
+
let value;
|
|
191
|
+
try {
|
|
192
|
+
value = normalizeSequence(sequence);
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (this.seen.has(value)) return false;
|
|
197
|
+
if (this.highest >= 0) {
|
|
198
|
+
const forward = (value - this.highest + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
|
|
199
|
+
if (forward === 0 || forward === UDP_SERIAL_HALF_RANGE) return false;
|
|
200
|
+
if (forward > UDP_SERIAL_HALF_RANGE) {
|
|
201
|
+
const age = (this.highest - value + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
|
|
202
|
+
if (age > this.size) return false;
|
|
203
|
+
} else {
|
|
204
|
+
this.highest = value;
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
this.highest = value;
|
|
208
|
+
}
|
|
209
|
+
this.seen.add(value);
|
|
210
|
+
for (const item of this.seen) {
|
|
211
|
+
const age = (this.highest - item + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
|
|
212
|
+
if (age > this.size) this.seen.delete(item);
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
192
217
|
|
|
193
218
|
export class UdpFrameReassembler {
|
|
194
219
|
constructor({ maxPending = 2, timeoutMs = 500, maxFrameBytes = 8 * 1024 * 1024 } = {}) {
|