@unagents/messenger-broker 0.10.0 → 0.11.0
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.Co46RRtX.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 = {}) {
|
|
@@ -780,6 +781,15 @@ class Mesh {
|
|
|
780
781
|
if (this.timer) clearInterval(this.timer);
|
|
781
782
|
this.timer = null;
|
|
782
783
|
}
|
|
784
|
+
/** True while the last sync with `url` succeeded. */
|
|
785
|
+
linkUp(url) {
|
|
786
|
+
return this.links.get(url) === "up";
|
|
787
|
+
}
|
|
788
|
+
/** Forget everything learned about `url` (its broker left or moved). */
|
|
789
|
+
forget(url) {
|
|
790
|
+
this.links.delete(url);
|
|
791
|
+
this.origins.delete(url);
|
|
792
|
+
}
|
|
783
793
|
/** Sync soon (coalesced). */
|
|
784
794
|
kick() {
|
|
785
795
|
if (this.running) this.again = true;
|
|
@@ -843,24 +853,6 @@ class Mesh {
|
|
|
843
853
|
this.setLink(url, "up");
|
|
844
854
|
}
|
|
845
855
|
}
|
|
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
856
|
function normalizeBrokerUrl(s) {
|
|
865
857
|
s = s.trim();
|
|
866
858
|
if (!/^https?:\/\//.test(s)) s = `http://${s}`;
|
|
@@ -882,6 +874,81 @@ function postJson(url, headers, body, timeoutMs) {
|
|
|
882
874
|
});
|
|
883
875
|
}
|
|
884
876
|
|
|
877
|
+
function meshExpireMs(announceMs) {
|
|
878
|
+
return announceMs * 3 + 5e3;
|
|
879
|
+
}
|
|
880
|
+
class MeshPeerTable {
|
|
881
|
+
constructor(opts) {
|
|
882
|
+
this.opts = opts;
|
|
883
|
+
this.now = opts.now ?? Date.now;
|
|
884
|
+
}
|
|
885
|
+
byId = /* @__PURE__ */ new Map();
|
|
886
|
+
mismatchLogged = /* @__PURE__ */ new Set();
|
|
887
|
+
now;
|
|
888
|
+
/** Current sync targets. */
|
|
889
|
+
urls() {
|
|
890
|
+
return [...this.byId.values()].map((e) => e.url);
|
|
891
|
+
}
|
|
892
|
+
entries() {
|
|
893
|
+
return [...this.byId.values()];
|
|
894
|
+
}
|
|
895
|
+
/** `announce` packet from `address` (UDP source). Returns true when accepted. */
|
|
896
|
+
handleAnnounce(m, address) {
|
|
897
|
+
if (m[DISCOVERY_KEY] !== DISCOVERY_ANNOUNCE_VALUE || m.mesh !== true) return false;
|
|
898
|
+
const ts = m.ts;
|
|
899
|
+
if (typeof ts !== "number" || !Number.isFinite(ts) || Math.abs(this.now() - ts) > MESH_ANNOUNCE_MAX_SKEW_MS) return false;
|
|
900
|
+
return this.accept(m, address, String(ts));
|
|
901
|
+
}
|
|
902
|
+
/** `broker` reply to our probe that carried `nonce`. Returns true when accepted. */
|
|
903
|
+
handleReply(m, address, nonce) {
|
|
904
|
+
if (m[DISCOVERY_KEY] !== DISCOVERY_REPLY_VALUE || m.mesh !== true) return false;
|
|
905
|
+
return this.accept(m, address, nonce);
|
|
906
|
+
}
|
|
907
|
+
accept(m, address, nonce) {
|
|
908
|
+
const { id, port, version, mac } = m;
|
|
909
|
+
if (typeof id !== "string" || !id || typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535) return false;
|
|
910
|
+
if (typeof mac !== "string") return false;
|
|
911
|
+
if (!meshMacEqual(meshMac(this.opts.token, nonce, id, port), mac)) {
|
|
912
|
+
const key = `${id}@${address}`;
|
|
913
|
+
if (!this.mismatchLogged.has(key)) {
|
|
914
|
+
this.mismatchLogged.add(key);
|
|
915
|
+
log.warn(`mesh: ignoring ${id}@${address} (token mismatch)`);
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
if (id === this.opts.selfId) return false;
|
|
920
|
+
const url = `http://${address}:${port}`;
|
|
921
|
+
const prev = this.byId.get(id);
|
|
922
|
+
const entry = { id, url, version: typeof version === "string" ? version : "", lastSeen: this.now() };
|
|
923
|
+
this.byId.set(id, entry);
|
|
924
|
+
if (!prev) {
|
|
925
|
+
log.info(`mesh: found ${id} at ${url}`);
|
|
926
|
+
this.opts.onJoin?.(entry);
|
|
927
|
+
} else if (prev.url !== url) {
|
|
928
|
+
log.info(`mesh: ${id} moved ${prev.url} -> ${url}`);
|
|
929
|
+
this.opts.onDrop?.(prev.url);
|
|
930
|
+
this.opts.onJoin?.(entry);
|
|
931
|
+
}
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Drop entries not vouched for within `expireMs` whose last sync did not succeed (`linkUp`): a link that
|
|
936
|
+
* still syncs stays even when UDP is lost. Returns the removed entries.
|
|
937
|
+
*/
|
|
938
|
+
sweep(linkUp) {
|
|
939
|
+
const gone = [];
|
|
940
|
+
const t = this.now();
|
|
941
|
+
for (const e of this.byId.values()) {
|
|
942
|
+
if (t - e.lastSeen <= this.opts.expireMs || linkUp(e.url)) continue;
|
|
943
|
+
this.byId.delete(e.id);
|
|
944
|
+
gone.push(e);
|
|
945
|
+
log.info(`mesh: ${e.url} left`);
|
|
946
|
+
this.opts.onDrop?.(e.url);
|
|
947
|
+
}
|
|
948
|
+
return gone;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
885
952
|
function createBrokerApp(opts) {
|
|
886
953
|
const routes = opts.handlers.routes();
|
|
887
954
|
const protocolRoutes = opts.handlers.protocolRoutes();
|
|
@@ -892,7 +959,8 @@ function createBrokerApp(opts) {
|
|
|
892
959
|
id: opts.id,
|
|
893
960
|
port: opts.port(),
|
|
894
961
|
peers: opts.handlers.alivePeerCount(),
|
|
895
|
-
...opts.fallback ? { fallback: true } : {}
|
|
962
|
+
...opts.fallback ? { fallback: true } : {},
|
|
963
|
+
...opts.mesh ? { mesh: true } : {}
|
|
896
964
|
})));
|
|
897
965
|
const post = (route, run) => router.post(route, defineEventHandler(async (event) => {
|
|
898
966
|
if (!authorized(event, opts.token)) return unauthorized(event);
|
|
@@ -936,12 +1004,16 @@ function startDiscoveryResponder(opts) {
|
|
|
936
1004
|
const disco = createSocket({ type: "udp4", reuseAddr: true });
|
|
937
1005
|
disco.on("error", (e) => log.warn(`discovery responder error: ${e.message}`));
|
|
938
1006
|
disco.on("message", (msg, rinfo) => {
|
|
1007
|
+
let m;
|
|
939
1008
|
try {
|
|
940
|
-
|
|
1009
|
+
m = JSON.parse(msg.toString());
|
|
941
1010
|
} catch {
|
|
942
1011
|
return;
|
|
943
1012
|
}
|
|
944
|
-
|
|
1013
|
+
if (m[DISCOVERY_KEY] === DISCOVERY_ANNOUNCE_VALUE) return opts.onAnnounce?.(m, rinfo);
|
|
1014
|
+
if (m[DISCOVERY_KEY] !== DISCOVERY_PROBE_VALUE) return;
|
|
1015
|
+
const nonce = typeof m.n === "string" && m.n.length > 0 && m.n.length <= 128 ? m.n : void 0;
|
|
1016
|
+
const reply = Buffer.from(JSON.stringify(opts.reply(nonce)));
|
|
945
1017
|
disco.send(reply, rinfo.port, rinfo.address, () => {
|
|
946
1018
|
});
|
|
947
1019
|
});
|
|
@@ -958,6 +1030,48 @@ function startDiscoveryResponder(opts) {
|
|
|
958
1030
|
});
|
|
959
1031
|
});
|
|
960
1032
|
}
|
|
1033
|
+
function parseDiscoveryTargets(list) {
|
|
1034
|
+
const out = [];
|
|
1035
|
+
for (const s of list) {
|
|
1036
|
+
const i = s.lastIndexOf(":");
|
|
1037
|
+
const port = Number(s.slice(i + 1));
|
|
1038
|
+
if (i <= 0 || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1039
|
+
log.warn(`discovery: ignoring invalid target "${s}" (want host:port)`);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
out.push({ host: s.slice(0, i), port });
|
|
1043
|
+
}
|
|
1044
|
+
return out;
|
|
1045
|
+
}
|
|
1046
|
+
function sendTargets(opts) {
|
|
1047
|
+
const explicit = parseDiscoveryTargets(opts.targets ?? []);
|
|
1048
|
+
if (explicit.length) return explicit;
|
|
1049
|
+
return probeTargets(opts.addr).map((host) => ({ host, port: opts.port }));
|
|
1050
|
+
}
|
|
1051
|
+
function buildAnnounce(a) {
|
|
1052
|
+
return {
|
|
1053
|
+
[DISCOVERY_KEY]: DISCOVERY_ANNOUNCE_VALUE,
|
|
1054
|
+
id: a.id,
|
|
1055
|
+
port: a.port,
|
|
1056
|
+
version: a.version,
|
|
1057
|
+
mesh: true,
|
|
1058
|
+
ts: a.ts,
|
|
1059
|
+
mac: meshMac(a.token, String(a.ts), a.id, a.port)
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function sendAnnounce(sock, packet, targets) {
|
|
1063
|
+
const buf = Buffer.from(JSON.stringify(packet));
|
|
1064
|
+
for (const t of targets) {
|
|
1065
|
+
try {
|
|
1066
|
+
sock.send(buf, t.port, t.host, () => {
|
|
1067
|
+
});
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function newNonce() {
|
|
1073
|
+
return randomBytes(16).toString("hex");
|
|
1074
|
+
}
|
|
961
1075
|
function probeTargets(addr) {
|
|
962
1076
|
if (addr) return [addr];
|
|
963
1077
|
const targets = /* @__PURE__ */ new Set(["255.255.255.255"]);
|
|
@@ -1001,7 +1115,16 @@ function probeBrokers(opts) {
|
|
|
1001
1115
|
if (m[DISCOVERY_KEY] !== DISCOVERY_REPLY_VALUE || !m.port) return;
|
|
1002
1116
|
const url = `http://${rinfo.address}:${m.port}`;
|
|
1003
1117
|
if (!found.has(url)) {
|
|
1004
|
-
found.set(url, {
|
|
1118
|
+
found.set(url, {
|
|
1119
|
+
url,
|
|
1120
|
+
host: rinfo.address,
|
|
1121
|
+
port: m.port,
|
|
1122
|
+
id: m.id ?? rinfo.address,
|
|
1123
|
+
version: m.version ?? "",
|
|
1124
|
+
fallback: m.fallback === true,
|
|
1125
|
+
mesh: m.mesh === true,
|
|
1126
|
+
...typeof m.mac === "string" ? { mac: m.mac } : {}
|
|
1127
|
+
});
|
|
1005
1128
|
}
|
|
1006
1129
|
} catch {
|
|
1007
1130
|
}
|
|
@@ -1011,8 +1134,8 @@ function probeBrokers(opts) {
|
|
|
1011
1134
|
sock.setBroadcast(true);
|
|
1012
1135
|
} catch {
|
|
1013
1136
|
}
|
|
1014
|
-
const probe = Buffer.from(JSON.stringify({ [DISCOVERY_KEY]: DISCOVERY_PROBE_VALUE }));
|
|
1015
|
-
for (const t of
|
|
1137
|
+
const probe = Buffer.from(JSON.stringify({ [DISCOVERY_KEY]: DISCOVERY_PROBE_VALUE, ...opts.nonce ? { n: opts.nonce } : {} }));
|
|
1138
|
+
for (const t of sendTargets(opts)) sock.send(probe, t.port, t.host, () => {
|
|
1016
1139
|
});
|
|
1017
1140
|
});
|
|
1018
1141
|
});
|
|
@@ -1068,7 +1191,7 @@ async function relayUndelivered(state, url, token) {
|
|
|
1068
1191
|
return { sent, dropped };
|
|
1069
1192
|
}
|
|
1070
1193
|
|
|
1071
|
-
const version = "0.
|
|
1194
|
+
const version = "0.11.0";
|
|
1072
1195
|
const pkg = {
|
|
1073
1196
|
version: version};
|
|
1074
1197
|
|
|
@@ -1114,21 +1237,61 @@ async function startBroker(options = {}) {
|
|
|
1114
1237
|
throw e;
|
|
1115
1238
|
}
|
|
1116
1239
|
port = listener.address.port;
|
|
1117
|
-
const reply = () => ({
|
|
1240
|
+
const reply = (nonce) => ({
|
|
1241
|
+
[DISCOVERY_KEY]: DISCOVERY_REPLY_VALUE,
|
|
1242
|
+
id: cfg.id,
|
|
1243
|
+
port,
|
|
1244
|
+
version: VERSION,
|
|
1245
|
+
...cfg.fallback ? { fallback: true } : {},
|
|
1246
|
+
...cfg.mesh ? { mesh: true, ...nonce ? { mac: meshMac(cfg.token, nonce, cfg.id, port) } : {} } : {}
|
|
1247
|
+
});
|
|
1248
|
+
const fixed = cfg.meshBrokers.map(normalizeBrokerUrl);
|
|
1249
|
+
const peerTable = cfg.mesh ? new MeshPeerTable({
|
|
1250
|
+
selfId: cfg.id,
|
|
1251
|
+
token: cfg.token,
|
|
1252
|
+
expireMs: meshExpireMs(cfg.meshAnnounceMs),
|
|
1253
|
+
onDrop: (url) => mesh?.forget(url),
|
|
1254
|
+
onJoin: () => mesh?.kick()
|
|
1255
|
+
}) : null;
|
|
1118
1256
|
let disco = null;
|
|
1119
1257
|
if (cfg.discovery) {
|
|
1120
|
-
disco = await startDiscoveryResponder({
|
|
1258
|
+
disco = await startDiscoveryResponder({
|
|
1259
|
+
port: cfg.discoveryPort,
|
|
1260
|
+
addr: cfg.discoveryAddr,
|
|
1261
|
+
reply,
|
|
1262
|
+
...peerTable ? { onAnnounce: (m, rinfo) => void peerTable.handleAnnounce(m, rinfo.address) } : {}
|
|
1263
|
+
});
|
|
1121
1264
|
}
|
|
1265
|
+
let announceTimer = null;
|
|
1122
1266
|
if (cfg.mesh) {
|
|
1123
|
-
const fixed = cfg.meshBrokers.map(normalizeBrokerUrl);
|
|
1124
1267
|
mesh = new Mesh({
|
|
1125
1268
|
id: cfg.id,
|
|
1126
1269
|
token: cfg.token,
|
|
1127
1270
|
handlers,
|
|
1128
|
-
urls: () => fixed.length ? fixed :
|
|
1271
|
+
urls: () => fixed.length ? fixed : peerTable.urls(),
|
|
1129
1272
|
intervalMs: cfg.meshIntervalMs
|
|
1130
1273
|
});
|
|
1131
1274
|
mesh.start();
|
|
1275
|
+
if (!cfg.discovery && !fixed.length) {
|
|
1276
|
+
log.warn("mesh: AGENT_MESSENGER_MESH=on but AGENT_MESSENGER_DISCOVERY=off; no other mesh broker can be found");
|
|
1277
|
+
}
|
|
1278
|
+
if (cfg.discovery && disco && !fixed.length) {
|
|
1279
|
+
const sock = disco;
|
|
1280
|
+
const targets = sendTargets({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, targets: cfg.discoveryTargets });
|
|
1281
|
+
const announceAndSweep = () => {
|
|
1282
|
+
sendAnnounce(sock, buildAnnounce({ token: cfg.token, id: cfg.id, port, version: VERSION, ts: Date.now() }), targets);
|
|
1283
|
+
peerTable.sweep((url) => mesh?.linkUp(url) ?? false);
|
|
1284
|
+
};
|
|
1285
|
+
announceAndSweep();
|
|
1286
|
+
announceTimer = setInterval(announceAndSweep, cfg.meshAnnounceMs);
|
|
1287
|
+
announceTimer.unref();
|
|
1288
|
+
const nonce = newNonce();
|
|
1289
|
+
void probeBrokers({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, targets: cfg.discoveryTargets, nonce, timeoutMs: 1e3 }).then((found) => {
|
|
1290
|
+
for (const b of found) {
|
|
1291
|
+
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);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1132
1295
|
}
|
|
1133
1296
|
log.info(`v${VERSION} id=${cfg.id} listening on ${cfg.bind}:${port} (state: ${cfg.stateFile})`);
|
|
1134
1297
|
if (cfg.fallback) log.info(`FALLBACK mode: looking for another broker every ${cfg.fallbackIntervalMs} ms (discovery ${cfg.discovery ? "on" : "off"})`);
|
|
@@ -1140,6 +1303,7 @@ async function startBroker(options = {}) {
|
|
|
1140
1303
|
closed = true;
|
|
1141
1304
|
clearInterval(cleaner);
|
|
1142
1305
|
if (fallbackTimer) clearInterval(fallbackTimer);
|
|
1306
|
+
if (announceTimer) clearInterval(announceTimer);
|
|
1143
1307
|
mesh?.stop();
|
|
1144
1308
|
if (disco) {
|
|
1145
1309
|
await new Promise((resolve) => disco.close(() => resolve()));
|
|
@@ -1154,7 +1318,7 @@ async function startBroker(options = {}) {
|
|
|
1154
1318
|
if (busy || closed) return;
|
|
1155
1319
|
busy = true;
|
|
1156
1320
|
try {
|
|
1157
|
-
const found = await probeBrokers({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, timeoutMs: 1e3 });
|
|
1321
|
+
const found = await probeBrokers({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, targets: cfg.discoveryTargets, timeoutMs: 1e3 });
|
|
1158
1322
|
const other = found.find((b) => !b.fallback && !(b.port === port && mine.has(b.host)));
|
|
1159
1323
|
if (!other || closed) return;
|
|
1160
1324
|
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.0",
|
|
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.0",
|
|
32
32
|
"consola": "^3.4.2",
|
|
33
33
|
"defu": "^6.1.7",
|
|
34
34
|
"h3": "^1.15.11",
|