← Files OneGateARCHIVED FILE
skills/onegate-dapp-debug/scripts/runtime/remote-debugger-advertiser.mjs
3.8 KB · Oct 5, 2026 · 18:29 UTC
import dgram from "node:dgram";
const MULTICAST_ADDRESS = "224.0.0.251";
const MULTICAST_PORT = 5353;
const SERVICE_NAME = "_onegate-debug._tcp.local";
function u16(value) {
const result = Buffer.alloc(2);
result.writeUInt16BE(value);
return result;
}
function u32(value) {
const result = Buffer.alloc(4);
result.writeUInt32BE(value);
return result;
}
function dnsName(value) {
const labels = value.replace(/\.$/u, "").split(".");
const parts = [];
for (const label of labels) {
const bytes = Buffer.from(label, "utf8");
if (bytes.length === 0 || bytes.length > 63) throw new Error(`Invalid DNS label: ${label}`);
parts.push(Buffer.from([bytes.length]), bytes);
}
parts.push(Buffer.from([0]));
return Buffer.concat(parts);
}
function record(name, type, data, ttl = 60) {
return Buffer.concat([dnsName(name), u16(type), u16(1), u32(ttl), u16(data.length), data]);
}
function ipv4Bytes(address) {
const parts = address.split(".").map((part) => Number.parseInt(part, 10));
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
throw new Error(`Invalid IPv4 address: ${address}`);
}
return Buffer.from(parts);
}
function txtData(values) {
return Buffer.concat(values.map((value) => {
const bytes = Buffer.from(value, "utf8");
if (bytes.length > 255) throw new Error("mDNS TXT entries are limited to 255 bytes.");
return Buffer.concat([Buffer.from([bytes.length]), bytes]);
}));
}
export class RemoteDebuggerAdvertiser {
constructor({ debuggerId, debuggerName, port, addresses }) {
this.debuggerId = debuggerId;
this.debuggerName = debuggerName;
this.port = port;
this.addresses = addresses.filter((address) => /^\d+\.\d+\.\d+\.\d+$/u.test(address));
this.instanceName = `${debuggerId}.${SERVICE_NAME}`;
this.hostName = `onegate-debugger-${debuggerId}.local`;
}
async start() {
if (this.socket) return;
const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
this.socket = socket;
socket.on("error", () => undefined);
socket.on("message", (message) => {
if (message.includes(Buffer.from("_onegate-debug", "utf8"))) this.announce();
});
await new Promise((resolve, reject) => {
socket.once("error", reject);
socket.bind(MULTICAST_PORT, "0.0.0.0", () => {
socket.off("error", reject);
try {
socket.addMembership(MULTICAST_ADDRESS);
socket.setMulticastTTL(255);
} catch {
// Pairing still works through the QR endpoints when multicast is unavailable.
}
resolve();
});
});
this.announce();
this.timer = setInterval(() => this.announce(), 30_000);
this.timer.unref?.();
}
announce() {
if (!this.socket) return;
const records = [
record(SERVICE_NAME, 12, dnsName(this.instanceName)),
record(this.instanceName, 33, Buffer.concat([u16(0), u16(0), u16(this.port), dnsName(this.hostName)])),
record(this.instanceName, 16, txtData([
"v=1",
`debuggerId=${this.debuggerId}`,
`debuggerName=${this.debuggerName}`,
])),
...this.addresses.map((address) => record(this.hostName, 1, ipv4Bytes(address))),
];
const header = Buffer.concat([
u16(0),
u16(0x8400),
u16(0),
u16(records.length),
u16(0),
u16(0),
]);
this.socket.send(Buffer.concat([header, ...records]), MULTICAST_PORT, MULTICAST_ADDRESS, () => undefined);
}
async stop() {
if (!this.socket) return;
clearInterval(this.timer);
const socket = this.socket;
this.socket = undefined;
await new Promise((resolve) => socket.close(resolve));
}
}
export { SERVICE_NAME as ONEGATE_MDNS_SERVICE };
SHA-256: d79357e20219d9a33a10d10a42cb5b551ae3cf0bc707a2ad86cde8df4e169338