@decentnetwork/beagle 0.1.57 → 0.1.59
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/bridge-roster.d.ts +20 -0
- package/dist/bridge-roster.js +105 -0
- package/dist/cli.js +9 -0
- package/dist/desktop/app.js +4761 -4900
- package/dist/embedded-host.js +5 -0
- package/dist/peer-host.d.ts +2 -0
- package/dist/peer-host.js +29 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +70 -5
- package/package.json +7 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface RosterPeer {
|
|
2
|
+
userid: string;
|
|
3
|
+
address: string | null;
|
|
4
|
+
name: string | null;
|
|
5
|
+
descr: string | null;
|
|
6
|
+
punk: number | null;
|
|
7
|
+
online: boolean;
|
|
8
|
+
lastSeen: number;
|
|
9
|
+
}
|
|
10
|
+
/** Who else is on this bridge. Null when the bridge speaks the older protocol
|
|
11
|
+
* and never issues a ticket — an absence, not an error. */
|
|
12
|
+
export declare function bridgeRoster(wsUrl: string, keyFile: string, profile: {
|
|
13
|
+
name?: string;
|
|
14
|
+
descr?: string;
|
|
15
|
+
address?: string;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
bridge: string;
|
|
18
|
+
online: number;
|
|
19
|
+
peers: RosterPeer[];
|
|
20
|
+
} | null>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Reading a bridge's roster without using the bridge.
|
|
2
|
+
//
|
|
3
|
+
// A browser peer gets the roster because it already holds a bridge ticket: the
|
|
4
|
+
// ticket is issued during the WebSocket handshake it has to do anyway to reach
|
|
5
|
+
// a relay. A desktop peer connects to relays directly and never handshakes
|
|
6
|
+
// with a bridge, so it has no ticket and the venue is invisible to it.
|
|
7
|
+
//
|
|
8
|
+
// It does not have to stay that way. The handshake proves one thing — that we
|
|
9
|
+
// hold the secret key for the userid we claim — and that proof is XEdDSA over
|
|
10
|
+
// a nonce, which this process can produce from its own keyfile. So we open a
|
|
11
|
+
// WebSocket purely to complete the handshake, take the ticket, and close it.
|
|
12
|
+
// No relay traffic ever crosses that socket; the bridge is a directory here,
|
|
13
|
+
// not a transport.
|
|
14
|
+
//
|
|
15
|
+
// The alternative is the directory peer (add it as a friend, message it), and
|
|
16
|
+
// that one is better for a client with no public bridge at all. This exists so
|
|
17
|
+
// the desktop and the browser can render the SAME roster from the same shape,
|
|
18
|
+
// which is what lets discover.jsx be one file instead of two.
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import WebSocket from "ws";
|
|
21
|
+
import { signDetached } from "@decentnetwork/peer";
|
|
22
|
+
const HANDSHAKE_MS = 12_000;
|
|
23
|
+
/** Tickets outlive a single call; re-handshaking per poll would be absurd. */
|
|
24
|
+
const tickets = new Map();
|
|
25
|
+
const TICKET_TTL_MS = 60 * 60_000;
|
|
26
|
+
const hex = (u8) => Buffer.from(u8).toString("hex");
|
|
27
|
+
function loadKeys(keyFile) {
|
|
28
|
+
const raw = JSON.parse(readFileSync(keyFile, "utf-8"));
|
|
29
|
+
return { pk: raw.publicKey, sk: new Uint8Array(Buffer.from(raw.secretKey, "hex")) };
|
|
30
|
+
}
|
|
31
|
+
/** ws(s)://host/relay-ws → https://host */
|
|
32
|
+
function httpBase(wsUrl) {
|
|
33
|
+
const u = new URL(wsUrl);
|
|
34
|
+
return `${u.protocol === "wss:" ? "https:" : "http:"}//${u.host}`;
|
|
35
|
+
}
|
|
36
|
+
/** Complete the handshake and return a ticket. Opens a socket to an
|
|
37
|
+
* allowlisted relay because the bridge needs a target to accept the
|
|
38
|
+
* connection at all — but nothing is ever sent over it. */
|
|
39
|
+
function fetchTicket(wsUrl, keyFile, profile) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const { pk, sk } = loadKeys(keyFile);
|
|
42
|
+
const ws = new WebSocket(`${wsUrl}?host=127.0.0.1&port=33445`);
|
|
43
|
+
const done = (err, ticket) => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
try {
|
|
46
|
+
ws.close();
|
|
47
|
+
}
|
|
48
|
+
catch { /* already closing */ }
|
|
49
|
+
err ? reject(err) : resolve(ticket);
|
|
50
|
+
};
|
|
51
|
+
const timer = setTimeout(() => done(new Error("bridge handshake timed out")), HANDSHAKE_MS);
|
|
52
|
+
ws.on("open", () => ws.send(JSON.stringify({
|
|
53
|
+
t: "hello", ver: 1, tox: pk,
|
|
54
|
+
address: profile.address || undefined,
|
|
55
|
+
name: profile.name || undefined,
|
|
56
|
+
descr: profile.descr || undefined,
|
|
57
|
+
})));
|
|
58
|
+
ws.on("message", (data, isBinary) => {
|
|
59
|
+
if (isBinary)
|
|
60
|
+
return; // relay bytes: not ours
|
|
61
|
+
let msg;
|
|
62
|
+
try {
|
|
63
|
+
msg = JSON.parse(data.toString());
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (msg.t === "challenge") {
|
|
69
|
+
const message = new TextEncoder().encode(`decent-bridge\n${msg.origin}\n${msg.n}`);
|
|
70
|
+
ws.send(JSON.stringify({ t: "proof", sig: hex(signDetached(sk, message)) }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (msg.t === "ticket" && msg.ticket)
|
|
74
|
+
done(null, msg.ticket);
|
|
75
|
+
});
|
|
76
|
+
ws.on("error", (e) => done(e));
|
|
77
|
+
ws.on("close", () => done(new Error("bridge closed before issuing a ticket")));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Who else is on this bridge. Null when the bridge speaks the older protocol
|
|
81
|
+
* and never issues a ticket — an absence, not an error. */
|
|
82
|
+
export async function bridgeRoster(wsUrl, keyFile, profile) {
|
|
83
|
+
const base = httpBase(wsUrl);
|
|
84
|
+
let cached = tickets.get(wsUrl);
|
|
85
|
+
if (cached && Date.now() - cached.at > TICKET_TTL_MS)
|
|
86
|
+
cached = undefined;
|
|
87
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
88
|
+
if (!cached) {
|
|
89
|
+
const ticket = await fetchTicket(wsUrl, keyFile, profile);
|
|
90
|
+
cached = { ticket, at: Date.now() };
|
|
91
|
+
tickets.set(wsUrl, cached);
|
|
92
|
+
}
|
|
93
|
+
const res = await fetch(`${base}/roster`, { headers: { authorization: `Bearer ${cached.ticket}` } });
|
|
94
|
+
if (res.status === 401) { // stale ticket: re-handshake once
|
|
95
|
+
tickets.delete(wsUrl);
|
|
96
|
+
cached = undefined;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
throw new Error(`bridge roster ${res.status}`);
|
|
101
|
+
const d = (await res.json());
|
|
102
|
+
return { bridge: d.bridge || base, online: d.online ?? 0, peers: d.peers ?? [] };
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -27,7 +27,9 @@ function parseArgs(argv) {
|
|
|
27
27
|
" --host <addr> bind address (default 127.0.0.1)",
|
|
28
28
|
" --config-dir <p> identity/config dir (default ~/.agentnet)",
|
|
29
29
|
" --dora-dir <p> dora roster dir, if this machine runs a dora",
|
|
30
|
+
" --bridge <wss> bridge whose roster to show (default: bridge.beagle.chat)",
|
|
30
31
|
" --backend <k> force 'daemon' or 'embedded' (default: auto)",
|
|
32
|
+
" --wait-daemon <s> with --backend daemon, wait this long for it (default 60)",
|
|
31
33
|
" -h, --help this text",
|
|
32
34
|
].join("\n"));
|
|
33
35
|
process.exit(0);
|
|
@@ -39,7 +41,9 @@ function parseArgs(argv) {
|
|
|
39
41
|
host: get("--host") ?? "127.0.0.1",
|
|
40
42
|
configDir: get("--config-dir") ?? defaultConfigDir(),
|
|
41
43
|
doraDir: get("--dora-dir"),
|
|
44
|
+
bridge: get("--bridge"),
|
|
42
45
|
backend: get("--backend"),
|
|
46
|
+
waitDaemon: get("--wait-daemon"),
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
49
|
const readJsonVer = (file) => {
|
|
@@ -109,6 +113,7 @@ async function main() {
|
|
|
109
113
|
onAutoAcceptChange: (enabled) => saveAutoAccept(args.configDir, enabled),
|
|
110
114
|
peerVersion: resolveVer("@decentnetwork/peer"),
|
|
111
115
|
force: args.backend,
|
|
116
|
+
waitForDaemonMs: args.waitDaemon ? Number(args.waitDaemon) * 1000 : undefined,
|
|
112
117
|
}));
|
|
113
118
|
}
|
|
114
119
|
catch (error) {
|
|
@@ -154,6 +159,10 @@ async function main() {
|
|
|
154
159
|
keyFile: resolve(decentlanCarrierDir(args.configDir), "keypair.json"),
|
|
155
160
|
bootstrapNodes,
|
|
156
161
|
},
|
|
162
|
+
// The venue this node shows in Here. A default rather than a required flag:
|
|
163
|
+
// the roster is only useful if it is populated, and everyone starts on the
|
|
164
|
+
// same one until they have a reason not to.
|
|
165
|
+
bridgeWs: args.bridge || "wss://bridge.beagle.chat/relay-ws",
|
|
157
166
|
meExtra: {
|
|
158
167
|
// The panel reads "lan <x> · peer <y>". Report the real decentlan version
|
|
159
168
|
// powering this backend, not beagle's own — conflating them made the
|