@unagents/messenger-broker 0.10.0 → 0.11.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/dist/index.d.mts
CHANGED
|
@@ -22,6 +22,11 @@ interface BrokerOptions {
|
|
|
22
22
|
discoveryPort?: number;
|
|
23
23
|
/** UDP bind address when pinned (`AGENT_MESSENGER_DISCOVERY_ADDR`, default `""` = all interfaces). */
|
|
24
24
|
discoveryAddr?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Explicit discovery send targets, `host:port` each (`AGENT_MESSENGER_DISCOVERY_TARGETS`, comma separated).
|
|
27
|
+
* When non-empty, probes and announces go to these instead of broadcast (tests; LAN without broadcast).
|
|
28
|
+
*/
|
|
29
|
+
discoveryTargets?: string[];
|
|
25
30
|
/**
|
|
26
31
|
* Run as a local fallback broker (`AGENT_MESSENGER_FALLBACK=1`, set by the client when it spawns one).
|
|
27
32
|
* A fallback probes for other brokers every `fallbackIntervalMs`; when it finds one it relays its
|
|
@@ -38,16 +43,16 @@ interface BrokerOptions {
|
|
|
38
43
|
*/
|
|
39
44
|
mesh?: boolean;
|
|
40
45
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
46
|
+
* Test-only: a fixed list of mesh members that replaces discovery. Empty (default) = members are
|
|
47
|
+
* found by UDP discovery (announce / probe reply, token-checked). Not documented for users.
|
|
43
48
|
*/
|
|
44
49
|
meshBrokers?: string[];
|
|
50
|
+
/** Mesh announce interval in ms (`AGENT_MESSENGER_MESH_ANNOUNCE_MS`, default 10000). */
|
|
51
|
+
meshAnnounceMs?: number;
|
|
45
52
|
/** Mesh sync interval in ms (`AGENT_MESSENGER_MESH_INTERVAL_MS`, default 5000). */
|
|
46
53
|
meshIntervalMs?: number;
|
|
47
54
|
/** Replicated peers disappear this long after their origin stopped syncing (`AGENT_MESSENGER_MESH_PEER_TTL_MS`, default 300000). */
|
|
48
55
|
meshPeerTtlMs?: number;
|
|
49
|
-
/** Client config file read for the mesh member list (`AGENT_MESSENGER_CONFIG`). */
|
|
50
|
-
configFile?: string;
|
|
51
56
|
}
|
|
52
57
|
type BrokerConfig = Required<Omit<BrokerOptions, "onSupersede">> & Pick<BrokerOptions, "onSupersede">;
|
|
53
58
|
/** Marks a broker as a local fallback. Broker-private (not in the protocol ENV table, which mirrors legacy). */
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ interface BrokerOptions {
|
|
|
22
22
|
discoveryPort?: number;
|
|
23
23
|
/** UDP bind address when pinned (`AGENT_MESSENGER_DISCOVERY_ADDR`, default `""` = all interfaces). */
|
|
24
24
|
discoveryAddr?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Explicit discovery send targets, `host:port` each (`AGENT_MESSENGER_DISCOVERY_TARGETS`, comma separated).
|
|
27
|
+
* When non-empty, probes and announces go to these instead of broadcast (tests; LAN without broadcast).
|
|
28
|
+
*/
|
|
29
|
+
discoveryTargets?: string[];
|
|
25
30
|
/**
|
|
26
31
|
* Run as a local fallback broker (`AGENT_MESSENGER_FALLBACK=1`, set by the client when it spawns one).
|
|
27
32
|
* A fallback probes for other brokers every `fallbackIntervalMs`; when it finds one it relays its
|
|
@@ -38,16 +43,16 @@ interface BrokerOptions {
|
|
|
38
43
|
*/
|
|
39
44
|
mesh?: boolean;
|
|
40
45
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
46
|
+
* Test-only: a fixed list of mesh members that replaces discovery. Empty (default) = members are
|
|
47
|
+
* found by UDP discovery (announce / probe reply, token-checked). Not documented for users.
|
|
43
48
|
*/
|
|
44
49
|
meshBrokers?: string[];
|
|
50
|
+
/** Mesh announce interval in ms (`AGENT_MESSENGER_MESH_ANNOUNCE_MS`, default 10000). */
|
|
51
|
+
meshAnnounceMs?: number;
|
|
45
52
|
/** Mesh sync interval in ms (`AGENT_MESSENGER_MESH_INTERVAL_MS`, default 5000). */
|
|
46
53
|
meshIntervalMs?: number;
|
|
47
54
|
/** Replicated peers disappear this long after their origin stopped syncing (`AGENT_MESSENGER_MESH_PEER_TTL_MS`, default 300000). */
|
|
48
55
|
meshPeerTtlMs?: number;
|
|
49
|
-
/** Client config file read for the mesh member list (`AGENT_MESSENGER_CONFIG`). */
|
|
50
|
-
configFile?: string;
|
|
51
56
|
}
|
|
52
57
|
type BrokerConfig = Required<Omit<BrokerOptions, "onSupersede">> & Pick<BrokerOptions, "onSupersede">;
|
|
53
58
|
/** Marks a broker as a local fallback. Broker-private (not in the protocol ENV table, which mirrors legacy). */
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'listhen';
|
|
2
2
|
import 'h3';
|
|
3
3
|
import '@unagents/messenger-protocol';
|
|
4
|
-
export { D as DEFAULT_STATE_FILE, F as FALLBACK_ENV, a as FALLBACK_INTERVAL_ENV, M as MESH_ENV, S as STATE_DIR, V as VERSION, c as configFromEnv, r as resolveConfig, s as startBroker } from './shared/messenger-broker.
|
|
4
|
+
export { D as DEFAULT_STATE_FILE, F as FALLBACK_ENV, a as FALLBACK_INTERVAL_ENV, M as MESH_ENV, S as STATE_DIR, V as VERSION, c as configFromEnv, r as resolveConfig, s as startBroker } from './shared/messenger-broker.BPu-Wz2y.mjs';
|
|
5
5
|
import 'node:os';
|
|
6
6
|
import 'node:path';
|
|
7
7
|
import 'defu';
|
package/dist/main.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { listen } from 'listhen';
|
|
2
2
|
import { createRouter, defineEventHandler, createApp, send, setResponseStatus, getRequestHeader, readRawBody, toNodeListener } from 'h3';
|
|
3
|
-
import { DEFAULT_BIND, ENV, DEFAULT_DISCOVERY_PORT, DEFAULT_HTTP_PORT, PROTOCOL_NAME_PATTERN, PROTOCOL_META_MAX_LENGTH, PROTOCOL_MANIFEST_MAX_BYTES, PROTOCOL_DOC_MAX_BYTES, PROTOCOL_TOTAL_MAX_BYTES, canonicalJSON, HEARTBEAT_TIMEOUT_MS, PROTOCOL_MAX_STORED, MESH_SYNC_ROUTE, HEALTH_ROUTE, BROKER_POST_ROUTES, PROTOCOL_ROUTES, RELAY_ROUTE,
|
|
3
|
+
import { DEFAULT_BIND, MESH_ANNOUNCE_ENV, DISCOVERY_TARGETS_ENV, ENV, DEFAULT_DISCOVERY_PORT, DEFAULT_HTTP_PORT, PROTOCOL_NAME_PATTERN, PROTOCOL_META_MAX_LENGTH, PROTOCOL_MANIFEST_MAX_BYTES, PROTOCOL_DOC_MAX_BYTES, PROTOCOL_TOTAL_MAX_BYTES, canonicalJSON, HEARTBEAT_TIMEOUT_MS, PROTOCOL_MAX_STORED, MESH_SYNC_ROUTE, DISCOVERY_KEY, DISCOVERY_ANNOUNCE_VALUE, MESH_ANNOUNCE_MAX_SKEW_MS, DISCOVERY_REPLY_VALUE, meshMacEqual, meshMac, HEALTH_ROUTE, BROKER_POST_ROUTES, PROTOCOL_ROUTES, RELAY_ROUTE, DISCOVERY_PROBE_VALUE } from '@unagents/messenger-protocol';
|
|
4
4
|
import { homedir, hostname, networkInterfaces } from 'node:os';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { defu } from 'defu';
|
|
7
7
|
import { readFileSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
|
|
8
8
|
import { createConsola } from 'consola';
|
|
9
|
-
import { createHash } from 'node:crypto';
|
|
9
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
10
10
|
import { request } from 'node:http';
|
|
11
11
|
import { createSocket } from 'node:dgram';
|
|
12
12
|
|
|
@@ -30,13 +30,14 @@ function configFromEnv(env = process.env) {
|
|
|
30
30
|
discovery: (env[ENV.DISCOVERY] ?? "on").toLowerCase() !== "off",
|
|
31
31
|
discoveryPort: parseInt(env[ENV.DISCOVERY_PORT] ?? String(DEFAULT_DISCOVERY_PORT), 10),
|
|
32
32
|
discoveryAddr: env[ENV.DISCOVERY_ADDR] ?? "",
|
|
33
|
+
discoveryTargets: (env[DISCOVERY_TARGETS_ENV] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
33
34
|
fallback: ["1", "true", "on", "yes"].includes((env[FALLBACK_ENV] ?? "").toLowerCase()),
|
|
34
35
|
fallbackIntervalMs: parseInt(env[FALLBACK_INTERVAL_ENV] ?? String(DEFAULT_FALLBACK_INTERVAL_MS), 10),
|
|
35
36
|
mesh: (env[MESH_ENV.MESH] ?? "off").toLowerCase() === "on",
|
|
36
37
|
meshBrokers: [],
|
|
38
|
+
meshAnnounceMs: parseInt(env[MESH_ANNOUNCE_ENV] ?? "10000", 10),
|
|
37
39
|
meshIntervalMs: parseInt(env[MESH_ENV.INTERVAL_MS] ?? "5000", 10),
|
|
38
|
-
meshPeerTtlMs: parseInt(env[MESH_ENV.PEER_TTL_MS] ?? "300000", 10)
|
|
39
|
-
configFile: env[ENV.CONFIG] ?? join(STATE_DIR, "config.json")
|
|
40
|
+
meshPeerTtlMs: parseInt(env[MESH_ENV.PEER_TTL_MS] ?? "300000", 10)
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
function resolveConfig(options = {}) {
|
|
@@ -225,6 +226,8 @@ function compareVersions(a, b) {
|
|
|
225
226
|
const PENDING_RELAY_TTL_MS = 60 * 60 * 1e3;
|
|
226
227
|
const DEFAULT_MESH_PEER_TTL_MS = 3e5;
|
|
227
228
|
const MESH_KEY_MEMORY = 2e3;
|
|
229
|
+
class BadRequestError extends Error {
|
|
230
|
+
}
|
|
228
231
|
class BrokerHandlers {
|
|
229
232
|
constructor(state, opts = {}) {
|
|
230
233
|
this.state = state;
|
|
@@ -426,6 +429,9 @@ class BrokerHandlers {
|
|
|
426
429
|
return list.map(({ origin: _origin, ...p }) => p);
|
|
427
430
|
}
|
|
428
431
|
sendMessage(body) {
|
|
432
|
+
const rawText = body.text;
|
|
433
|
+
if (rawText !== void 0 && typeof rawText !== "string") throw new BadRequestError("invalid text: expected a string");
|
|
434
|
+
if (body.mode !== "ping" && (rawText === void 0 || rawText === "")) throw new BadRequestError("invalid text: expected a non-empty string");
|
|
429
435
|
const resolved = this.resolvePeer(body.to);
|
|
430
436
|
if (resolved.error !== void 0) return { ok: false, error: resolved.error };
|
|
431
437
|
const { peer } = resolved;
|
|
@@ -780,6 +786,15 @@ class Mesh {
|
|
|
780
786
|
if (this.timer) clearInterval(this.timer);
|
|
781
787
|
this.timer = null;
|
|
782
788
|
}
|
|
789
|
+
/** True while the last sync with `url` succeeded. */
|
|
790
|
+
linkUp(url) {
|
|
791
|
+
return this.links.get(url) === "up";
|
|
792
|
+
}
|
|
793
|
+
/** Forget everything learned about `url` (its broker left or moved). */
|
|
794
|
+
forget(url) {
|
|
795
|
+
this.links.delete(url);
|
|
796
|
+
this.origins.delete(url);
|
|
797
|
+
}
|
|
783
798
|
/** Sync soon (coalesced). */
|
|
784
799
|
kick() {
|
|
785
800
|
if (this.running) this.again = true;
|
|
@@ -843,24 +858,6 @@ class Mesh {
|
|
|
843
858
|
this.setLink(url, "up");
|
|
844
859
|
}
|
|
845
860
|
}
|
|
846
|
-
function readMeshBrokers(configFile) {
|
|
847
|
-
try {
|
|
848
|
-
const parsed = JSON.parse(readFileSync(configFile, "utf8"));
|
|
849
|
-
const list = Array.isArray(parsed.brokers) ? parsed.brokers : parsed.broker ? [parsed.broker] : [];
|
|
850
|
-
const out = [];
|
|
851
|
-
for (const s of list) {
|
|
852
|
-
if (typeof s !== "string" || !s.trim()) continue;
|
|
853
|
-
try {
|
|
854
|
-
out.push(normalizeBrokerUrl(s));
|
|
855
|
-
} catch {
|
|
856
|
-
log.warn(`mesh: ignoring unparsable broker entry "${s}"`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
return out;
|
|
860
|
-
} catch {
|
|
861
|
-
return [];
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
861
|
function normalizeBrokerUrl(s) {
|
|
865
862
|
s = s.trim();
|
|
866
863
|
if (!/^https?:\/\//.test(s)) s = `http://${s}`;
|
|
@@ -882,6 +879,81 @@ function postJson(url, headers, body, timeoutMs) {
|
|
|
882
879
|
});
|
|
883
880
|
}
|
|
884
881
|
|
|
882
|
+
function meshExpireMs(announceMs) {
|
|
883
|
+
return announceMs * 3 + 5e3;
|
|
884
|
+
}
|
|
885
|
+
class MeshPeerTable {
|
|
886
|
+
constructor(opts) {
|
|
887
|
+
this.opts = opts;
|
|
888
|
+
this.now = opts.now ?? Date.now;
|
|
889
|
+
}
|
|
890
|
+
byId = /* @__PURE__ */ new Map();
|
|
891
|
+
mismatchLogged = /* @__PURE__ */ new Set();
|
|
892
|
+
now;
|
|
893
|
+
/** Current sync targets. */
|
|
894
|
+
urls() {
|
|
895
|
+
return [...this.byId.values()].map((e) => e.url);
|
|
896
|
+
}
|
|
897
|
+
entries() {
|
|
898
|
+
return [...this.byId.values()];
|
|
899
|
+
}
|
|
900
|
+
/** `announce` packet from `address` (UDP source). Returns true when accepted. */
|
|
901
|
+
handleAnnounce(m, address) {
|
|
902
|
+
if (m[DISCOVERY_KEY] !== DISCOVERY_ANNOUNCE_VALUE || m.mesh !== true) return false;
|
|
903
|
+
const ts = m.ts;
|
|
904
|
+
if (typeof ts !== "number" || !Number.isFinite(ts) || Math.abs(this.now() - ts) > MESH_ANNOUNCE_MAX_SKEW_MS) return false;
|
|
905
|
+
return this.accept(m, address, String(ts));
|
|
906
|
+
}
|
|
907
|
+
/** `broker` reply to our probe that carried `nonce`. Returns true when accepted. */
|
|
908
|
+
handleReply(m, address, nonce) {
|
|
909
|
+
if (m[DISCOVERY_KEY] !== DISCOVERY_REPLY_VALUE || m.mesh !== true) return false;
|
|
910
|
+
return this.accept(m, address, nonce);
|
|
911
|
+
}
|
|
912
|
+
accept(m, address, nonce) {
|
|
913
|
+
const { id, port, version, mac } = m;
|
|
914
|
+
if (typeof id !== "string" || !id || typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535) return false;
|
|
915
|
+
if (typeof mac !== "string") return false;
|
|
916
|
+
if (!meshMacEqual(meshMac(this.opts.token, nonce, id, port), mac)) {
|
|
917
|
+
const key = `${id}@${address}`;
|
|
918
|
+
if (!this.mismatchLogged.has(key)) {
|
|
919
|
+
this.mismatchLogged.add(key);
|
|
920
|
+
log.warn(`mesh: ignoring ${id}@${address} (token mismatch)`);
|
|
921
|
+
}
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
if (id === this.opts.selfId) return false;
|
|
925
|
+
const url = `http://${address}:${port}`;
|
|
926
|
+
const prev = this.byId.get(id);
|
|
927
|
+
const entry = { id, url, version: typeof version === "string" ? version : "", lastSeen: this.now() };
|
|
928
|
+
this.byId.set(id, entry);
|
|
929
|
+
if (!prev) {
|
|
930
|
+
log.info(`mesh: found ${id} at ${url}`);
|
|
931
|
+
this.opts.onJoin?.(entry);
|
|
932
|
+
} else if (prev.url !== url) {
|
|
933
|
+
log.info(`mesh: ${id} moved ${prev.url} -> ${url}`);
|
|
934
|
+
this.opts.onDrop?.(prev.url);
|
|
935
|
+
this.opts.onJoin?.(entry);
|
|
936
|
+
}
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Drop entries not vouched for within `expireMs` whose last sync did not succeed (`linkUp`): a link that
|
|
941
|
+
* still syncs stays even when UDP is lost. Returns the removed entries.
|
|
942
|
+
*/
|
|
943
|
+
sweep(linkUp) {
|
|
944
|
+
const gone = [];
|
|
945
|
+
const t = this.now();
|
|
946
|
+
for (const e of this.byId.values()) {
|
|
947
|
+
if (t - e.lastSeen <= this.opts.expireMs || linkUp(e.url)) continue;
|
|
948
|
+
this.byId.delete(e.id);
|
|
949
|
+
gone.push(e);
|
|
950
|
+
log.info(`mesh: ${e.url} left`);
|
|
951
|
+
this.opts.onDrop?.(e.url);
|
|
952
|
+
}
|
|
953
|
+
return gone;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
885
957
|
function createBrokerApp(opts) {
|
|
886
958
|
const routes = opts.handlers.routes();
|
|
887
959
|
const protocolRoutes = opts.handlers.protocolRoutes();
|
|
@@ -892,14 +964,15 @@ function createBrokerApp(opts) {
|
|
|
892
964
|
id: opts.id,
|
|
893
965
|
port: opts.port(),
|
|
894
966
|
peers: opts.handlers.alivePeerCount(),
|
|
895
|
-
...opts.fallback ? { fallback: true } : {}
|
|
967
|
+
...opts.fallback ? { fallback: true } : {},
|
|
968
|
+
...opts.mesh ? { mesh: true } : {}
|
|
896
969
|
})));
|
|
897
970
|
const post = (route, run) => router.post(route, defineEventHandler(async (event) => {
|
|
898
971
|
if (!authorized(event, opts.token)) return unauthorized(event);
|
|
899
972
|
try {
|
|
900
973
|
return run(await readJson(event));
|
|
901
974
|
} catch (e) {
|
|
902
|
-
setResponseStatus(event, 500);
|
|
975
|
+
setResponseStatus(event, e instanceof BadRequestError ? 400 : 500);
|
|
903
976
|
return { error: e.message };
|
|
904
977
|
}
|
|
905
978
|
}));
|
|
@@ -936,12 +1009,16 @@ function startDiscoveryResponder(opts) {
|
|
|
936
1009
|
const disco = createSocket({ type: "udp4", reuseAddr: true });
|
|
937
1010
|
disco.on("error", (e) => log.warn(`discovery responder error: ${e.message}`));
|
|
938
1011
|
disco.on("message", (msg, rinfo) => {
|
|
1012
|
+
let m;
|
|
939
1013
|
try {
|
|
940
|
-
|
|
1014
|
+
m = JSON.parse(msg.toString());
|
|
941
1015
|
} catch {
|
|
942
1016
|
return;
|
|
943
1017
|
}
|
|
944
|
-
|
|
1018
|
+
if (m[DISCOVERY_KEY] === DISCOVERY_ANNOUNCE_VALUE) return opts.onAnnounce?.(m, rinfo);
|
|
1019
|
+
if (m[DISCOVERY_KEY] !== DISCOVERY_PROBE_VALUE) return;
|
|
1020
|
+
const nonce = typeof m.n === "string" && m.n.length > 0 && m.n.length <= 128 ? m.n : void 0;
|
|
1021
|
+
const reply = Buffer.from(JSON.stringify(opts.reply(nonce)));
|
|
945
1022
|
disco.send(reply, rinfo.port, rinfo.address, () => {
|
|
946
1023
|
});
|
|
947
1024
|
});
|
|
@@ -958,6 +1035,48 @@ function startDiscoveryResponder(opts) {
|
|
|
958
1035
|
});
|
|
959
1036
|
});
|
|
960
1037
|
}
|
|
1038
|
+
function parseDiscoveryTargets(list) {
|
|
1039
|
+
const out = [];
|
|
1040
|
+
for (const s of list) {
|
|
1041
|
+
const i = s.lastIndexOf(":");
|
|
1042
|
+
const port = Number(s.slice(i + 1));
|
|
1043
|
+
if (i <= 0 || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1044
|
+
log.warn(`discovery: ignoring invalid target "${s}" (want host:port)`);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
out.push({ host: s.slice(0, i), port });
|
|
1048
|
+
}
|
|
1049
|
+
return out;
|
|
1050
|
+
}
|
|
1051
|
+
function sendTargets(opts) {
|
|
1052
|
+
const explicit = parseDiscoveryTargets(opts.targets ?? []);
|
|
1053
|
+
if (explicit.length) return explicit;
|
|
1054
|
+
return probeTargets(opts.addr).map((host) => ({ host, port: opts.port }));
|
|
1055
|
+
}
|
|
1056
|
+
function buildAnnounce(a) {
|
|
1057
|
+
return {
|
|
1058
|
+
[DISCOVERY_KEY]: DISCOVERY_ANNOUNCE_VALUE,
|
|
1059
|
+
id: a.id,
|
|
1060
|
+
port: a.port,
|
|
1061
|
+
version: a.version,
|
|
1062
|
+
mesh: true,
|
|
1063
|
+
ts: a.ts,
|
|
1064
|
+
mac: meshMac(a.token, String(a.ts), a.id, a.port)
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function sendAnnounce(sock, packet, targets) {
|
|
1068
|
+
const buf = Buffer.from(JSON.stringify(packet));
|
|
1069
|
+
for (const t of targets) {
|
|
1070
|
+
try {
|
|
1071
|
+
sock.send(buf, t.port, t.host, () => {
|
|
1072
|
+
});
|
|
1073
|
+
} catch {
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function newNonce() {
|
|
1078
|
+
return randomBytes(16).toString("hex");
|
|
1079
|
+
}
|
|
961
1080
|
function probeTargets(addr) {
|
|
962
1081
|
if (addr) return [addr];
|
|
963
1082
|
const targets = /* @__PURE__ */ new Set(["255.255.255.255"]);
|
|
@@ -1001,7 +1120,16 @@ function probeBrokers(opts) {
|
|
|
1001
1120
|
if (m[DISCOVERY_KEY] !== DISCOVERY_REPLY_VALUE || !m.port) return;
|
|
1002
1121
|
const url = `http://${rinfo.address}:${m.port}`;
|
|
1003
1122
|
if (!found.has(url)) {
|
|
1004
|
-
found.set(url, {
|
|
1123
|
+
found.set(url, {
|
|
1124
|
+
url,
|
|
1125
|
+
host: rinfo.address,
|
|
1126
|
+
port: m.port,
|
|
1127
|
+
id: m.id ?? rinfo.address,
|
|
1128
|
+
version: m.version ?? "",
|
|
1129
|
+
fallback: m.fallback === true,
|
|
1130
|
+
mesh: m.mesh === true,
|
|
1131
|
+
...typeof m.mac === "string" ? { mac: m.mac } : {}
|
|
1132
|
+
});
|
|
1005
1133
|
}
|
|
1006
1134
|
} catch {
|
|
1007
1135
|
}
|
|
@@ -1011,8 +1139,8 @@ function probeBrokers(opts) {
|
|
|
1011
1139
|
sock.setBroadcast(true);
|
|
1012
1140
|
} catch {
|
|
1013
1141
|
}
|
|
1014
|
-
const probe = Buffer.from(JSON.stringify({ [DISCOVERY_KEY]: DISCOVERY_PROBE_VALUE }));
|
|
1015
|
-
for (const t of
|
|
1142
|
+
const probe = Buffer.from(JSON.stringify({ [DISCOVERY_KEY]: DISCOVERY_PROBE_VALUE, ...opts.nonce ? { n: opts.nonce } : {} }));
|
|
1143
|
+
for (const t of sendTargets(opts)) sock.send(probe, t.port, t.host, () => {
|
|
1016
1144
|
});
|
|
1017
1145
|
});
|
|
1018
1146
|
});
|
|
@@ -1068,7 +1196,7 @@ async function relayUndelivered(state, url, token) {
|
|
|
1068
1196
|
return { sent, dropped };
|
|
1069
1197
|
}
|
|
1070
1198
|
|
|
1071
|
-
const version = "0.
|
|
1199
|
+
const version = "0.11.1";
|
|
1072
1200
|
const pkg = {
|
|
1073
1201
|
version: version};
|
|
1074
1202
|
|
|
@@ -1114,21 +1242,61 @@ async function startBroker(options = {}) {
|
|
|
1114
1242
|
throw e;
|
|
1115
1243
|
}
|
|
1116
1244
|
port = listener.address.port;
|
|
1117
|
-
const reply = () => ({
|
|
1245
|
+
const reply = (nonce) => ({
|
|
1246
|
+
[DISCOVERY_KEY]: DISCOVERY_REPLY_VALUE,
|
|
1247
|
+
id: cfg.id,
|
|
1248
|
+
port,
|
|
1249
|
+
version: VERSION,
|
|
1250
|
+
...cfg.fallback ? { fallback: true } : {},
|
|
1251
|
+
...cfg.mesh ? { mesh: true, ...nonce ? { mac: meshMac(cfg.token, nonce, cfg.id, port) } : {} } : {}
|
|
1252
|
+
});
|
|
1253
|
+
const fixed = cfg.meshBrokers.map(normalizeBrokerUrl);
|
|
1254
|
+
const peerTable = cfg.mesh ? new MeshPeerTable({
|
|
1255
|
+
selfId: cfg.id,
|
|
1256
|
+
token: cfg.token,
|
|
1257
|
+
expireMs: meshExpireMs(cfg.meshAnnounceMs),
|
|
1258
|
+
onDrop: (url) => mesh?.forget(url),
|
|
1259
|
+
onJoin: () => mesh?.kick()
|
|
1260
|
+
}) : null;
|
|
1118
1261
|
let disco = null;
|
|
1119
1262
|
if (cfg.discovery) {
|
|
1120
|
-
disco = await startDiscoveryResponder({
|
|
1263
|
+
disco = await startDiscoveryResponder({
|
|
1264
|
+
port: cfg.discoveryPort,
|
|
1265
|
+
addr: cfg.discoveryAddr,
|
|
1266
|
+
reply,
|
|
1267
|
+
...peerTable ? { onAnnounce: (m, rinfo) => void peerTable.handleAnnounce(m, rinfo.address) } : {}
|
|
1268
|
+
});
|
|
1121
1269
|
}
|
|
1270
|
+
let announceTimer = null;
|
|
1122
1271
|
if (cfg.mesh) {
|
|
1123
|
-
const fixed = cfg.meshBrokers.map(normalizeBrokerUrl);
|
|
1124
1272
|
mesh = new Mesh({
|
|
1125
1273
|
id: cfg.id,
|
|
1126
1274
|
token: cfg.token,
|
|
1127
1275
|
handlers,
|
|
1128
|
-
urls: () => fixed.length ? fixed :
|
|
1276
|
+
urls: () => fixed.length ? fixed : peerTable.urls(),
|
|
1129
1277
|
intervalMs: cfg.meshIntervalMs
|
|
1130
1278
|
});
|
|
1131
1279
|
mesh.start();
|
|
1280
|
+
if (!cfg.discovery && !fixed.length) {
|
|
1281
|
+
log.warn("mesh: AGENT_MESSENGER_MESH=on but AGENT_MESSENGER_DISCOVERY=off; no other mesh broker can be found");
|
|
1282
|
+
}
|
|
1283
|
+
if (cfg.discovery && disco && !fixed.length) {
|
|
1284
|
+
const sock = disco;
|
|
1285
|
+
const targets = sendTargets({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, targets: cfg.discoveryTargets });
|
|
1286
|
+
const announceAndSweep = () => {
|
|
1287
|
+
sendAnnounce(sock, buildAnnounce({ token: cfg.token, id: cfg.id, port, version: VERSION, ts: Date.now() }), targets);
|
|
1288
|
+
peerTable.sweep((url) => mesh?.linkUp(url) ?? false);
|
|
1289
|
+
};
|
|
1290
|
+
announceAndSweep();
|
|
1291
|
+
announceTimer = setInterval(announceAndSweep, cfg.meshAnnounceMs);
|
|
1292
|
+
announceTimer.unref();
|
|
1293
|
+
const nonce = newNonce();
|
|
1294
|
+
void probeBrokers({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, targets: cfg.discoveryTargets, nonce, timeoutMs: 1e3 }).then((found) => {
|
|
1295
|
+
for (const b of found) {
|
|
1296
|
+
peerTable.handleReply({ [DISCOVERY_KEY]: DISCOVERY_REPLY_VALUE, id: b.id, port: b.port, version: b.version, mesh: b.mesh, mac: b.mac }, b.host, nonce);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1132
1300
|
}
|
|
1133
1301
|
log.info(`v${VERSION} id=${cfg.id} listening on ${cfg.bind}:${port} (state: ${cfg.stateFile})`);
|
|
1134
1302
|
if (cfg.fallback) log.info(`FALLBACK mode: looking for another broker every ${cfg.fallbackIntervalMs} ms (discovery ${cfg.discovery ? "on" : "off"})`);
|
|
@@ -1140,6 +1308,7 @@ async function startBroker(options = {}) {
|
|
|
1140
1308
|
closed = true;
|
|
1141
1309
|
clearInterval(cleaner);
|
|
1142
1310
|
if (fallbackTimer) clearInterval(fallbackTimer);
|
|
1311
|
+
if (announceTimer) clearInterval(announceTimer);
|
|
1143
1312
|
mesh?.stop();
|
|
1144
1313
|
if (disco) {
|
|
1145
1314
|
await new Promise((resolve) => disco.close(() => resolve()));
|
|
@@ -1154,7 +1323,7 @@ async function startBroker(options = {}) {
|
|
|
1154
1323
|
if (busy || closed) return;
|
|
1155
1324
|
busy = true;
|
|
1156
1325
|
try {
|
|
1157
|
-
const found = await probeBrokers({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, timeoutMs: 1e3 });
|
|
1326
|
+
const found = await probeBrokers({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, targets: cfg.discoveryTargets, timeoutMs: 1e3 });
|
|
1158
1327
|
const other = found.find((b) => !b.fallback && !(b.port === port && mine.has(b.host)));
|
|
1159
1328
|
if (!other || closed) return;
|
|
1160
1329
|
log.info(`fallback: found broker ${other.id} at ${other.url} \u2014 relaying messages and exiting`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unagents/messenger-broker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "agent-messenger broker: HTTP daemon that tracks peers and routes messages between Claude Code sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test": "vitest run"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@unagents/messenger-protocol": "0.
|
|
31
|
+
"@unagents/messenger-protocol": "0.11.1",
|
|
32
32
|
"consola": "^3.4.2",
|
|
33
33
|
"defu": "^6.1.7",
|
|
34
34
|
"h3": "^1.15.11",
|