@dmsdc-ai/aigentry-telepty 0.5.9 → 0.6.1
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/CHANGELOG.md +86 -0
- package/cli.js +404 -30
- package/cross-machine.js +124 -1
- package/daemon-control.js +9 -0
- package/daemon.js +495 -23
- package/install.js +156 -26
- package/package.json +5 -5
- package/src/audit/inject-log.js +234 -0
- package/src/audit/provenance.js +86 -0
- package/src/protocol/http-auth.js +36 -1
- package/src/submit-gate.js +130 -5
- package/src/transport/broker-client.js +498 -0
- package/src/transport/broker-protocol.js +155 -0
- package/src/transport/broker-server.js +531 -0
- package/src/win-resolve-executable.js +6 -1
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const https = require('node:https');
|
|
5
|
+
const net = require('node:net');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
buildAck,
|
|
9
|
+
buildInjectEnvelope,
|
|
10
|
+
createMessageIdDeduper,
|
|
11
|
+
parseInjectEnvelope,
|
|
12
|
+
parseSseFrame,
|
|
13
|
+
} = require('./broker-protocol');
|
|
14
|
+
|
|
15
|
+
function createBrokerClient(options) {
|
|
16
|
+
return new BrokerClient(options);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class BrokerClient {
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.url = new URL(requireString(options.url, 'url'));
|
|
22
|
+
if (this.url.protocol !== 'https:') {
|
|
23
|
+
throw new Error('Broker URL must use https');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.node = requireString(options.node || options.nodeName, 'node');
|
|
27
|
+
this.nodeJwt = requireString(options.nodeJwt || options.jwt, 'nodeJwt');
|
|
28
|
+
this.pin = normalizePin(options.pin || null);
|
|
29
|
+
this.deliver = requireFunction(options.deliver, 'deliver');
|
|
30
|
+
this.getSession = typeof options.getSession === 'function'
|
|
31
|
+
? options.getSession
|
|
32
|
+
: createSessionGetter(options.sessions);
|
|
33
|
+
this.getSessions = typeof options.getSessions === 'function'
|
|
34
|
+
? options.getSessions
|
|
35
|
+
: createSessionsGetter(options.sessions);
|
|
36
|
+
this.acceptFrom = options.acceptFrom === undefined ? options.accept_from : options.acceptFrom;
|
|
37
|
+
|
|
38
|
+
this.heartbeatMs = numberOr(options.heartbeatMs, 23000);
|
|
39
|
+
this.reconnect = options.reconnect !== false;
|
|
40
|
+
this.reconnectInitialMs = numberOr(options.reconnectInitialMs, 500);
|
|
41
|
+
this.reconnectMaxMs = numberOr(options.reconnectMaxMs, 10000);
|
|
42
|
+
this.reconnectJitterMs = numberOr(options.reconnectJitterMs, 250);
|
|
43
|
+
this.requestTimeoutMs = numberOr(options.requestTimeoutMs, 10000);
|
|
44
|
+
this.random = typeof options.random === 'function' ? options.random : Math.random;
|
|
45
|
+
this.agent = options.agent || new https.Agent({ keepAlive: false, maxCachedSessions: 0 });
|
|
46
|
+
|
|
47
|
+
this.lastEventId = options.lastEventId === undefined || options.lastEventId === null
|
|
48
|
+
? null
|
|
49
|
+
: String(options.lastEventId);
|
|
50
|
+
this.deduper = options.deduper || createMessageIdDeduper(options.deduperOptions);
|
|
51
|
+
this.processing = Promise.resolve();
|
|
52
|
+
this.started = false;
|
|
53
|
+
this.stopped = true;
|
|
54
|
+
this.currentRequest = null;
|
|
55
|
+
this.currentStream = null;
|
|
56
|
+
this.reconnectTimer = null;
|
|
57
|
+
this.heartbeatTimer = null;
|
|
58
|
+
this.reconnectDelayMs = this.reconnectInitialMs;
|
|
59
|
+
this.lastError = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async start() {
|
|
63
|
+
if (this.started) return this;
|
|
64
|
+
this.started = true;
|
|
65
|
+
this.stopped = false;
|
|
66
|
+
this.startHeartbeat();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await this.connectStream();
|
|
70
|
+
this.reconnectDelayMs = this.reconnectInitialMs;
|
|
71
|
+
return this;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
this.lastError = error;
|
|
74
|
+
this.scheduleReconnect();
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stop() {
|
|
80
|
+
this.started = false;
|
|
81
|
+
this.stopped = true;
|
|
82
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
83
|
+
if (this.heartbeatTimer) clearTimeout(this.heartbeatTimer);
|
|
84
|
+
this.reconnectTimer = null;
|
|
85
|
+
this.heartbeatTimer = null;
|
|
86
|
+
if (this.currentRequest) this.currentRequest.destroy();
|
|
87
|
+
if (this.currentStream) this.currentStream.destroy();
|
|
88
|
+
this.currentRequest = null;
|
|
89
|
+
this.currentStream = null;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async inject(input) {
|
|
94
|
+
const envelope = buildInjectEnvelope(input);
|
|
95
|
+
const response = await this.postJson('/broker/inject', envelope);
|
|
96
|
+
return response.body;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async register() {
|
|
100
|
+
return this.postJson('/broker/register', {
|
|
101
|
+
node: this.node,
|
|
102
|
+
sessions: normalizeSessions(this.getSessions()),
|
|
103
|
+
last_event_id: this.lastEventId,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async heartbeat() {
|
|
108
|
+
return this.postJson('/broker/heartbeat', {
|
|
109
|
+
node: this.node,
|
|
110
|
+
sessions: normalizeSessions(this.getSessions()),
|
|
111
|
+
last_event_id: this.lastEventId,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
startHeartbeat() {
|
|
116
|
+
if (!this.heartbeatMs || this.heartbeatMs < 1) return;
|
|
117
|
+
const tick = async () => {
|
|
118
|
+
if (this.stopped) return;
|
|
119
|
+
try {
|
|
120
|
+
await this.heartbeat();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
this.lastError = error;
|
|
123
|
+
}
|
|
124
|
+
this.startHeartbeat();
|
|
125
|
+
};
|
|
126
|
+
this.heartbeatTimer = setTimeout(tick, this.heartbeatMs);
|
|
127
|
+
this.heartbeatTimer.unref?.();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async connectStream() {
|
|
131
|
+
await this.register();
|
|
132
|
+
if (this.stopped) return;
|
|
133
|
+
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
let settled = false;
|
|
136
|
+
let disconnected = false;
|
|
137
|
+
let buffer = '';
|
|
138
|
+
const headers = this.authHeaders({ accept: 'text/event-stream' });
|
|
139
|
+
if (this.lastEventId !== null) {
|
|
140
|
+
headers['last-event-id'] = this.lastEventId;
|
|
141
|
+
}
|
|
142
|
+
const req = this.createRequest('GET', '/broker/stream', headers, (res) => {
|
|
143
|
+
if (res.statusCode !== 200) {
|
|
144
|
+
collectResponse(res).then((body) => {
|
|
145
|
+
rejectOnce(httpError('Broker stream failed', res.statusCode, body));
|
|
146
|
+
}, rejectOnce);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.currentStream = res;
|
|
151
|
+
resolveOnce();
|
|
152
|
+
res.setEncoding('utf8');
|
|
153
|
+
res.on('data', (chunk) => {
|
|
154
|
+
buffer += chunk;
|
|
155
|
+
buffer = this.consumeSseBuffer(buffer);
|
|
156
|
+
});
|
|
157
|
+
res.on('end', () => disconnectOnce());
|
|
158
|
+
res.on('close', () => disconnectOnce());
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
this.currentRequest = req;
|
|
162
|
+
req.on('error', (error) => {
|
|
163
|
+
if (settled) {
|
|
164
|
+
disconnectOnce(error);
|
|
165
|
+
} else {
|
|
166
|
+
rejectOnce(error);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
req.end();
|
|
170
|
+
|
|
171
|
+
const resolveOnce = () => {
|
|
172
|
+
if (settled) return;
|
|
173
|
+
settled = true;
|
|
174
|
+
resolve();
|
|
175
|
+
};
|
|
176
|
+
const rejectOnce = (error) => {
|
|
177
|
+
if (settled) return;
|
|
178
|
+
settled = true;
|
|
179
|
+
this.currentRequest = null;
|
|
180
|
+
reject(error);
|
|
181
|
+
};
|
|
182
|
+
const disconnectOnce = (error) => {
|
|
183
|
+
if (disconnected || this.stopped) return;
|
|
184
|
+
disconnected = true;
|
|
185
|
+
this.currentRequest = null;
|
|
186
|
+
this.currentStream = null;
|
|
187
|
+
if (error) this.lastError = error;
|
|
188
|
+
this.scheduleReconnect();
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
consumeSseBuffer(buffer) {
|
|
194
|
+
let remaining = buffer;
|
|
195
|
+
for (;;) {
|
|
196
|
+
const boundary = findFrameBoundary(remaining);
|
|
197
|
+
if (!boundary) return remaining;
|
|
198
|
+
const rawFrame = remaining.slice(0, boundary.index);
|
|
199
|
+
remaining = remaining.slice(boundary.index + boundary.length);
|
|
200
|
+
if (!rawFrame.trim()) continue;
|
|
201
|
+
const frame = parseSseFrame(rawFrame);
|
|
202
|
+
if (frame.id !== null && frame.id !== undefined && frame.id !== '') {
|
|
203
|
+
this.lastEventId = String(frame.id);
|
|
204
|
+
}
|
|
205
|
+
this.enqueueFrame(frame);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
enqueueFrame(frame) {
|
|
210
|
+
this.processing = this.processing
|
|
211
|
+
.then(() => this.handleFrame(frame))
|
|
212
|
+
.catch((error) => {
|
|
213
|
+
this.lastError = error;
|
|
214
|
+
});
|
|
215
|
+
return this.processing;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async handleFrame(frame) {
|
|
219
|
+
if (frame.event !== 'inject') return;
|
|
220
|
+
const envelope = parseInjectEnvelope(frame.data);
|
|
221
|
+
await this.handleInjectEnvelope(envelope);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async handleInjectEnvelope(envelope) {
|
|
225
|
+
if (!this.deduper.accept(envelope)) {
|
|
226
|
+
await this.ack({ inject_id: envelope.inject_id, success: true });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!acceptsFrom(this.acceptFrom, envelope.from_node, envelope)) {
|
|
231
|
+
await this.ack({
|
|
232
|
+
inject_id: envelope.inject_id,
|
|
233
|
+
success: false,
|
|
234
|
+
code: 'ACCEPT_FROM_DENIED',
|
|
235
|
+
error: `Broker inject from ${envelope.from_node} denied by accept_from`,
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const session = this.getSession(envelope.to_session, envelope);
|
|
241
|
+
if (!session) {
|
|
242
|
+
await this.ack({
|
|
243
|
+
inject_id: envelope.inject_id,
|
|
244
|
+
success: false,
|
|
245
|
+
code: 'SESSION_NOT_FOUND',
|
|
246
|
+
error: `Session not found: ${envelope.to_session}`,
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const payload = envelope.payload || {};
|
|
252
|
+
if (typeof payload.prompt !== 'string') {
|
|
253
|
+
await this.ack({
|
|
254
|
+
inject_id: envelope.inject_id,
|
|
255
|
+
success: false,
|
|
256
|
+
code: 'INVALID_REQUEST',
|
|
257
|
+
error: 'prompt is required',
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const delivery = await this.callDeliver(envelope, session, payload);
|
|
263
|
+
await this.ack({
|
|
264
|
+
inject_id: envelope.inject_id,
|
|
265
|
+
success: delivery.success === true,
|
|
266
|
+
code: delivery.success === true ? null : delivery.code || 'DELIVERY_FAILED',
|
|
267
|
+
error: delivery.success === true ? null : delivery.error || 'Broker inject delivery failed',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async callDeliver(envelope, session, payload) {
|
|
272
|
+
const options = {
|
|
273
|
+
noEnter: payload.no_enter === true,
|
|
274
|
+
source: 'broker',
|
|
275
|
+
from: payload.from || envelope.from_node || 'broker',
|
|
276
|
+
};
|
|
277
|
+
if (payload.reply_to) options.replyTo = payload.reply_to;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
return await this.deliver(envelope.to_session, session, payload.prompt, options);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
code: 'DELIVERY_ERROR',
|
|
285
|
+
error: error && error.message ? error.message : String(error),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async ack(input) {
|
|
291
|
+
try {
|
|
292
|
+
await this.postJson('/broker/ack', buildAck(input));
|
|
293
|
+
} catch (error) {
|
|
294
|
+
this.lastError = error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
scheduleReconnect() {
|
|
299
|
+
if (this.stopped || !this.reconnect || this.reconnectTimer) return;
|
|
300
|
+
const delay = this.nextReconnectDelay();
|
|
301
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
302
|
+
this.reconnectTimer = null;
|
|
303
|
+
if (this.stopped) return;
|
|
304
|
+
try {
|
|
305
|
+
await this.connectStream();
|
|
306
|
+
this.reconnectDelayMs = this.reconnectInitialMs;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
this.lastError = error;
|
|
309
|
+
this.scheduleReconnect();
|
|
310
|
+
}
|
|
311
|
+
}, delay);
|
|
312
|
+
this.reconnectTimer.unref?.();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
nextReconnectDelay() {
|
|
316
|
+
const jitter = this.reconnectJitterMs > 0 ? Math.floor(this.random() * this.reconnectJitterMs) : 0;
|
|
317
|
+
const delay = Math.min(this.reconnectDelayMs, this.reconnectMaxMs) + jitter;
|
|
318
|
+
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, this.reconnectMaxMs);
|
|
319
|
+
return delay;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async postJson(path, body) {
|
|
323
|
+
const payload = JSON.stringify(body || {});
|
|
324
|
+
const headers = this.authHeaders({
|
|
325
|
+
accept: 'application/json',
|
|
326
|
+
'content-type': 'application/json',
|
|
327
|
+
'content-length': Buffer.byteLength(payload),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
const req = this.createRequest('POST', path, headers, (res) => {
|
|
332
|
+
collectResponse(res).then((rawBody) => {
|
|
333
|
+
const parsedBody = parseJsonBody(rawBody);
|
|
334
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
335
|
+
reject(httpError('Broker request failed', res.statusCode, parsedBody));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
resolve({ statusCode: res.statusCode, headers: res.headers, body: parsedBody });
|
|
339
|
+
}, reject);
|
|
340
|
+
});
|
|
341
|
+
req.setTimeout(this.requestTimeoutMs, () => {
|
|
342
|
+
req.destroy(new Error('Broker request timed out'));
|
|
343
|
+
});
|
|
344
|
+
req.on('error', reject);
|
|
345
|
+
req.end(payload);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
createRequest(method, path, headers, onResponse) {
|
|
350
|
+
const requestUrl = new URL(path, this.url);
|
|
351
|
+
const req = https.request({
|
|
352
|
+
protocol: requestUrl.protocol,
|
|
353
|
+
hostname: requestUrl.hostname,
|
|
354
|
+
port: requestUrl.port,
|
|
355
|
+
path: `${requestUrl.pathname}${requestUrl.search}`,
|
|
356
|
+
method,
|
|
357
|
+
headers,
|
|
358
|
+
servername: net.isIP(requestUrl.hostname) ? undefined : requestUrl.hostname,
|
|
359
|
+
rejectUnauthorized: this.pin ? false : true,
|
|
360
|
+
agent: this.agent,
|
|
361
|
+
}, onResponse);
|
|
362
|
+
|
|
363
|
+
if (this.pin) {
|
|
364
|
+
req.on('socket', (socket) => {
|
|
365
|
+
socket.once('secureConnect', () => {
|
|
366
|
+
const cert = socket.getPeerCertificate(true);
|
|
367
|
+
const actualPin = certificateFingerprint(cert);
|
|
368
|
+
if (actualPin !== this.pin) {
|
|
369
|
+
req.destroy(new Error('TLS pin mismatch'));
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return req;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
authHeaders(extra = {}) {
|
|
379
|
+
return {
|
|
380
|
+
...extra,
|
|
381
|
+
authorization: `Bearer ${this.nodeJwt}`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function createSessionGetter(sessions) {
|
|
387
|
+
return (id) => {
|
|
388
|
+
if (!sessions) return null;
|
|
389
|
+
if (sessions instanceof Map) return sessions.get(id) || null;
|
|
390
|
+
return sessions[id] || null;
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function createSessionsGetter(sessions) {
|
|
395
|
+
return () => sessions || [];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizeSessions(value) {
|
|
399
|
+
if (!value) return [];
|
|
400
|
+
if (Array.isArray(value)) return value;
|
|
401
|
+
if (value instanceof Map) return [...value.values()];
|
|
402
|
+
if (typeof value === 'object') return Object.values(value);
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function acceptsFrom(acceptFrom, fromNode, envelope) {
|
|
407
|
+
if (acceptFrom === null || acceptFrom === undefined) return true;
|
|
408
|
+
if (typeof acceptFrom === 'function') return acceptFrom(fromNode, envelope) === true;
|
|
409
|
+
if (Array.isArray(acceptFrom) || acceptFrom instanceof Set) {
|
|
410
|
+
return listHas(acceptFrom, fromNode);
|
|
411
|
+
}
|
|
412
|
+
if (typeof acceptFrom !== 'object') return true;
|
|
413
|
+
|
|
414
|
+
const deny = acceptFrom.deny || acceptFrom.deny_list || acceptFrom.denylist;
|
|
415
|
+
if (deny && listHas(deny, fromNode)) return false;
|
|
416
|
+
const allow = acceptFrom.allow || acceptFrom.allow_list || acceptFrom.allowlist;
|
|
417
|
+
if (allow) return listHas(allow, fromNode);
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function listHas(list, value) {
|
|
422
|
+
if (list instanceof Set) return list.has(value) || list.has('*');
|
|
423
|
+
if (!Array.isArray(list)) return false;
|
|
424
|
+
return list.includes(value) || list.includes('*');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function findFrameBoundary(buffer) {
|
|
428
|
+
const lf = buffer.indexOf('\n\n');
|
|
429
|
+
const crlf = buffer.indexOf('\r\n\r\n');
|
|
430
|
+
if (lf === -1 && crlf === -1) return null;
|
|
431
|
+
if (lf !== -1 && (crlf === -1 || lf < crlf)) return { index: lf, length: 2 };
|
|
432
|
+
return { index: crlf, length: 4 };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function collectResponse(res) {
|
|
436
|
+
return new Promise((resolve, reject) => {
|
|
437
|
+
const chunks = [];
|
|
438
|
+
res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
439
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
440
|
+
res.on('error', reject);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function parseJsonBody(rawBody) {
|
|
445
|
+
if (!rawBody) return null;
|
|
446
|
+
try {
|
|
447
|
+
return JSON.parse(rawBody);
|
|
448
|
+
} catch {
|
|
449
|
+
return rawBody;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function normalizePin(pin) {
|
|
454
|
+
if (!pin) return null;
|
|
455
|
+
let value = String(pin).trim().toLowerCase();
|
|
456
|
+
if (value.startsWith('sha256:')) value = value.slice('sha256:'.length);
|
|
457
|
+
value = value.replace(/:/g, '');
|
|
458
|
+
if (!/^[0-9a-f]{64}$/.test(value)) {
|
|
459
|
+
throw new Error('Invalid broker TLS pin');
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function certificateFingerprint(cert) {
|
|
465
|
+
if (!cert) return false;
|
|
466
|
+
return cert.fingerprint256
|
|
467
|
+
? cert.fingerprint256.toLowerCase().replace(/:/g, '')
|
|
468
|
+
: cert.raw && crypto.createHash('sha256').update(cert.raw).digest('hex');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function httpError(message, statusCode, body) {
|
|
472
|
+
const error = new Error(`${message}: ${statusCode}`);
|
|
473
|
+
error.statusCode = statusCode;
|
|
474
|
+
error.body = body;
|
|
475
|
+
return error;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function numberOr(value, fallback) {
|
|
479
|
+
return Number.isFinite(value) ? value : fallback;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function requireString(value, name) {
|
|
483
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
484
|
+
throw new Error(`Missing ${name}`);
|
|
485
|
+
}
|
|
486
|
+
return value;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function requireFunction(value, name) {
|
|
490
|
+
if (typeof value !== 'function') {
|
|
491
|
+
throw new Error(`Missing ${name}`);
|
|
492
|
+
}
|
|
493
|
+
return value;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
module.exports = {
|
|
497
|
+
createBrokerClient,
|
|
498
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function buildInjectEnvelope(input) {
|
|
4
|
+
const payload = input.payload || {};
|
|
5
|
+
const toNode = requireString(input.to_node, 'to_node');
|
|
6
|
+
const toSession = requireString(input.to_session, 'to_session');
|
|
7
|
+
const fromNode = requireString(input.from_node, 'from_node');
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
type: 'inject',
|
|
11
|
+
message_id: requireString(input.message_id, 'message_id'),
|
|
12
|
+
inject_id: requireString(input.inject_id, 'inject_id'),
|
|
13
|
+
target: input.target || `${toSession}@${toNode}`,
|
|
14
|
+
to_node: toNode,
|
|
15
|
+
to_session: toSession,
|
|
16
|
+
from_node: fromNode,
|
|
17
|
+
source_host: input.source_host || fromNode,
|
|
18
|
+
payload: {
|
|
19
|
+
prompt: payload.prompt,
|
|
20
|
+
from: payload.from,
|
|
21
|
+
reply_to: payload.reply_to,
|
|
22
|
+
no_enter: payload.no_enter === true,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseInjectEnvelope(value) {
|
|
28
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
29
|
+
if (!parsed || parsed.type !== 'inject') {
|
|
30
|
+
throw new Error('Expected inject envelope');
|
|
31
|
+
}
|
|
32
|
+
return buildInjectEnvelope(parsed);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createMessageIdDeduper(options = {}) {
|
|
36
|
+
const seen = options.seen || new Set();
|
|
37
|
+
const maxSize = options.maxSize || 10000;
|
|
38
|
+
const trimSize = options.trimSize || 5000;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
accept(messageOrId) {
|
|
42
|
+
const messageId = getMessageId(messageOrId);
|
|
43
|
+
if (!messageId) return false;
|
|
44
|
+
if (seen.has(messageId)) return false;
|
|
45
|
+
seen.add(messageId);
|
|
46
|
+
|
|
47
|
+
if (seen.size > maxSize) {
|
|
48
|
+
const ids = [...seen];
|
|
49
|
+
ids.splice(trimSize);
|
|
50
|
+
for (const id of ids) seen.delete(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
has(messageOrId) {
|
|
56
|
+
return seen.has(getMessageId(messageOrId));
|
|
57
|
+
},
|
|
58
|
+
get size() {
|
|
59
|
+
return seen.size;
|
|
60
|
+
},
|
|
61
|
+
seen,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createSseSequencer(options = {}) {
|
|
66
|
+
let current = parseLastEventId(options.initial) || 0;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
next() {
|
|
70
|
+
current += 1;
|
|
71
|
+
return current;
|
|
72
|
+
},
|
|
73
|
+
get current() {
|
|
74
|
+
return current;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseLastEventId(value) {
|
|
80
|
+
if (value === null || value === undefined || value === '') return null;
|
|
81
|
+
const seq = Number(value);
|
|
82
|
+
if (!Number.isSafeInteger(seq) || seq < 0) return null;
|
|
83
|
+
return seq;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildSseInjectFrame(seq, envelope) {
|
|
87
|
+
const eventId = parseLastEventId(seq);
|
|
88
|
+
if (eventId === null) throw new Error('Invalid SSE sequence id');
|
|
89
|
+
return `id: ${eventId}\nevent: inject\ndata: ${JSON.stringify(envelope)}\n\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseSseFrame(frame) {
|
|
93
|
+
const fields = { id: null, event: null, data: [] };
|
|
94
|
+
for (const line of String(frame).split(/\r?\n/)) {
|
|
95
|
+
if (!line || line.startsWith(':')) continue;
|
|
96
|
+
const delimiter = line.indexOf(':');
|
|
97
|
+
const name = delimiter === -1 ? line : line.slice(0, delimiter);
|
|
98
|
+
let value = delimiter === -1 ? '' : line.slice(delimiter + 1);
|
|
99
|
+
if (value.startsWith(' ')) value = value.slice(1);
|
|
100
|
+
|
|
101
|
+
if (name === 'id') fields.id = value;
|
|
102
|
+
if (name === 'event') fields.event = value;
|
|
103
|
+
if (name === 'data') fields.data.push(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = fields.data.join('\n');
|
|
107
|
+
return {
|
|
108
|
+
id: fields.id,
|
|
109
|
+
event: fields.event,
|
|
110
|
+
data: parseFrameData(data),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildAck(input) {
|
|
115
|
+
return {
|
|
116
|
+
type: 'ack',
|
|
117
|
+
inject_id: requireString(input.inject_id, 'inject_id'),
|
|
118
|
+
success: input.success === true,
|
|
119
|
+
code: input.code || null,
|
|
120
|
+
error: input.error || null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getMessageId(messageOrId) {
|
|
125
|
+
if (typeof messageOrId === 'string') return messageOrId;
|
|
126
|
+
if (messageOrId && typeof messageOrId.message_id === 'string') return messageOrId.message_id;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseFrameData(data) {
|
|
131
|
+
if (!data) return null;
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(data);
|
|
134
|
+
} catch {
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function requireString(value, name) {
|
|
140
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
141
|
+
throw new Error(`Missing ${name}`);
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
buildAck,
|
|
148
|
+
buildInjectEnvelope,
|
|
149
|
+
buildSseInjectFrame,
|
|
150
|
+
createMessageIdDeduper,
|
|
151
|
+
createSseSequencer,
|
|
152
|
+
parseInjectEnvelope,
|
|
153
|
+
parseLastEventId,
|
|
154
|
+
parseSseFrame,
|
|
155
|
+
};
|