@decentnetwork/peer 0.1.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/LICENSE +31 -0
- package/README.md +97 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +245 -0
- package/dist/compat/address.d.ts +13 -0
- package/dist/compat/address.js +69 -0
- package/dist/compat/bootstrap.d.ts +29 -0
- package/dist/compat/bootstrap.js +178 -0
- package/dist/compat/dht.d.ts +3 -0
- package/dist/compat/dht.js +9 -0
- package/dist/compat/express.d.ts +21 -0
- package/dist/compat/express.js +263 -0
- package/dist/compat/friend.d.ts +4 -0
- package/dist/compat/friend.js +12 -0
- package/dist/compat/net-crypto.d.ts +84 -0
- package/dist/compat/net-crypto.js +278 -0
- package/dist/compat/packet.d.ts +55 -0
- package/dist/compat/packet.js +154 -0
- package/dist/compat/session.d.ts +3 -0
- package/dist/compat/session.js +7 -0
- package/dist/compat/tcp-relay-pool.d.ts +85 -0
- package/dist/compat/tcp-relay-pool.js +342 -0
- package/dist/compat/tcp-relay.d.ts +96 -0
- package/dist/compat/tcp-relay.js +489 -0
- package/dist/compat/text.d.ts +3 -0
- package/dist/compat/text.js +8 -0
- package/dist/compat/tox-dht-crypto.d.ts +18 -0
- package/dist/compat/tox-dht-crypto.js +69 -0
- package/dist/compat/tox-onion.d.ts +66 -0
- package/dist/compat/tox-onion.js +172 -0
- package/dist/crypto/box.d.ts +1 -0
- package/dist/crypto/box.js +3 -0
- package/dist/crypto/keypair.d.ts +5 -0
- package/dist/crypto/keypair.js +37 -0
- package/dist/crypto/nonce.d.ts +1 -0
- package/dist/crypto/nonce.js +1 -0
- package/dist/crypto/sign.d.ts +1 -0
- package/dist/crypto/sign.js +3 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/peer.d.ts +45 -0
- package/dist/peer.js +3425 -0
- package/dist/runtime/errors.d.ts +3 -0
- package/dist/runtime/errors.js +6 -0
- package/dist/runtime/events.d.ts +4 -0
- package/dist/runtime/events.js +1 -0
- package/dist/runtime/lifecycle.d.ts +7 -0
- package/dist/runtime/lifecycle.js +12 -0
- package/dist/store/config.d.ts +2 -0
- package/dist/store/config.js +1 -0
- package/dist/store/friends.d.ts +13 -0
- package/dist/store/friends.js +1 -0
- package/dist/store/state.d.ts +3 -0
- package/dist/store/state.js +1 -0
- package/dist/transport/socket.d.ts +4 -0
- package/dist/transport/socket.js +1 -0
- package/dist/transport/tcp.d.ts +3 -0
- package/dist/transport/tcp.js +5 -0
- package/dist/transport/udp.d.ts +24 -0
- package/dist/transport/udp.js +90 -0
- package/dist/types/bootstrap.d.ts +2 -0
- package/dist/types/bootstrap.js +1 -0
- package/dist/types/dht.d.ts +3 -0
- package/dist/types/dht.js +1 -0
- package/dist/types/friend.d.ts +1 -0
- package/dist/types/friend.js +1 -0
- package/dist/types/message.d.ts +1 -0
- package/dist/types/message.js +1 -0
- package/dist/types/peer.d.ts +51 -0
- package/dist/types/peer.js +1 -0
- package/dist/types/session.d.ts +1 -0
- package/dist/types/session.js +1 -0
- package/dist/utils/base58.d.ts +2 -0
- package/dist/utils/base58.js +51 -0
- package/dist/utils/bytes.d.ts +4 -0
- package/dist/utils/bytes.js +31 -0
- package/docs/INSTALL.md +103 -0
- package/docs/USAGE_GUIDE.md +724 -0
- package/package.json +77 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
# Using `@decentnetwork/peer` in a Node.js application
|
|
2
|
+
|
|
3
|
+
A practical, copy-paste-ready guide for building Node.js apps on top
|
|
4
|
+
of `@decentnetwork/peer`. Every code block is a complete runnable file —
|
|
5
|
+
copy it, save it as `.mjs` or `.ts`, install the dep, and run.
|
|
6
|
+
|
|
7
|
+
If you haven't read the protocol docs yet, this guide stands on its
|
|
8
|
+
own. For deeper reference once you have a working app:
|
|
9
|
+
|
|
10
|
+
- [`PROTOCOL_OVERVIEW.md`](PROTOCOL_OVERVIEW.md) — actors, lifecycle,
|
|
11
|
+
message kinds
|
|
12
|
+
- [`DISCOVERY_FLOW.md`](DISCOVERY_FLOW.md) — what happens between
|
|
13
|
+
`peer.start()` and "friend shows online"
|
|
14
|
+
- [`IOS_INTEROP_PLAYBOOK.md`](IOS_INTEROP_PLAYBOOK.md) — debugging
|
|
15
|
+
guide when something goes wrong
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Requires Node.js 20 or later (we use modern WebStreams/AbortController).
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add @decentnetwork/peer
|
|
25
|
+
# or
|
|
26
|
+
npm install @decentnetwork/peer
|
|
27
|
+
# or
|
|
28
|
+
yarn add @decentnetwork/peer
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
There are **no** native dependencies. Pure JS via `tweetnacl` for
|
|
32
|
+
NaCl crypto and `flatbuffers` for app payloads.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Concepts in 60 seconds
|
|
37
|
+
|
|
38
|
+
1. **A peer has a long-term keypair** (loaded from / saved to a file
|
|
39
|
+
on disk). The pubkey is your identity. Sharing your **address**
|
|
40
|
+
(`pubkey + nospam + checksum`, base58-encoded) lets anyone send
|
|
41
|
+
you a friend request.
|
|
42
|
+
2. **Friends are pubkeys you've added**. After both sides have
|
|
43
|
+
added each other, an encrypted session establishes automatically
|
|
44
|
+
in the background. `peer.onText(...)` fires when they message you.
|
|
45
|
+
3. **Bootstrap nodes are operator-run servers** that introduce you
|
|
46
|
+
to the DHT and act as TCP relays when direct UDP isn't possible.
|
|
47
|
+
You don't run them — you list them in your config.
|
|
48
|
+
4. **The protocol handles offline delivery**: friend requests and
|
|
49
|
+
text messages can ride an HTTP "Express" relay so they arrive
|
|
50
|
+
when the recipient next comes online.
|
|
51
|
+
|
|
52
|
+
That's it. The rest is event handlers and lifecycle.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Sample 1 — Hello world: start, print address, stop
|
|
57
|
+
|
|
58
|
+
The simplest possible peer: starts, prints your address (so you can
|
|
59
|
+
give it to a friend), waits 5 seconds, stops cleanly.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
// hello.mjs
|
|
63
|
+
//
|
|
64
|
+
// Run with:
|
|
65
|
+
// node hello.mjs
|
|
66
|
+
//
|
|
67
|
+
// On first run this creates `peer.save` (your keypair). Keep that
|
|
68
|
+
// file safe — it IS your peer identity. Delete it and you become a
|
|
69
|
+
// new peer with no friends and no message history.
|
|
70
|
+
|
|
71
|
+
import { Peer } from "@decentnetwork/peer";
|
|
72
|
+
|
|
73
|
+
// `Peer.create(...)` only constructs the object. Network IO is
|
|
74
|
+
// deferred to `start()` so you can attach event listeners first
|
|
75
|
+
// (see Sample 2).
|
|
76
|
+
const peer = await Peer.create({
|
|
77
|
+
// Where to load/save the long-term keypair. Created on first run
|
|
78
|
+
// if not present.
|
|
79
|
+
keyFile: "./peer.save",
|
|
80
|
+
|
|
81
|
+
// Bootstrap nodes — these are operator-run servers that introduce
|
|
82
|
+
// us to the DHT. The list is synced with iOS Beagle's hardcoded
|
|
83
|
+
// bootstraps. See `legacy-bootstraps.mjs` for the full list; one
|
|
84
|
+
// node here is enough to bootstrap, but more = better resilience.
|
|
85
|
+
bootstrapNodes: [
|
|
86
|
+
{
|
|
87
|
+
host: "47.100.103.201",
|
|
88
|
+
port: 33445,
|
|
89
|
+
pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
host: "144.202.113.167",
|
|
93
|
+
port: 33445,
|
|
94
|
+
pk: "EfT4YMq6qfHdDsCiBCgsEmA78E2NxVYVKVUS9bD6w9GH"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
host: "13.58.208.50",
|
|
98
|
+
port: 33445,
|
|
99
|
+
pk: "89vny8MrKdDKs7Uta9RdVmspPjnRMdwMmaiEW27pZ7gh"
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
|
|
103
|
+
// "legacy" = wire-compatible with iOS Beagle / Carrier C SDK.
|
|
104
|
+
// Currently the only supported mode.
|
|
105
|
+
compatibilityMode: "legacy"
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Bind UDP socket, load persisted friends.json (if any).
|
|
109
|
+
await peer.start();
|
|
110
|
+
|
|
111
|
+
// Send a `getnodes` to each bootstrap; whichever responds first
|
|
112
|
+
// becomes our entry into the DHT. Returns once we've joined.
|
|
113
|
+
const result = await peer.joinNetwork();
|
|
114
|
+
console.log(`joined via ${result.respondingNode.host}:${result.respondingNode.port}`);
|
|
115
|
+
console.log(`discovered ${result.discoveredNodes.length} neighbors`);
|
|
116
|
+
|
|
117
|
+
// `address` = pubkey + nospam + checksum, base58. Share this with
|
|
118
|
+
// friends so they can `sendFriendRequest` you.
|
|
119
|
+
console.log(`my address: ${peer.address()}`);
|
|
120
|
+
|
|
121
|
+
// `userid` = just the 32-byte pubkey, base58. Used internally for
|
|
122
|
+
// friend lookups; you generally pass the address, not the userid.
|
|
123
|
+
console.log(`my userid: ${peer.userid()}`);
|
|
124
|
+
|
|
125
|
+
// Idle for 5 seconds so the DHT-PK / self-announce loops can do a
|
|
126
|
+
// few rounds. In a real app you'd run forever.
|
|
127
|
+
await new Promise((r) => setTimeout(r, 5_000));
|
|
128
|
+
|
|
129
|
+
// Tear down cleanly: closes UDP socket, closes TCP relay
|
|
130
|
+
// connections, persists friends.json.
|
|
131
|
+
await peer.stop();
|
|
132
|
+
console.log("stopped.");
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Sample 2 — Receive friend requests and accept
|
|
138
|
+
|
|
139
|
+
Run this on machine A; share the printed address; have machine B
|
|
140
|
+
call `peer.sendFriendRequest(addr, "hi")` (Sample 3 below).
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
// receiver.mjs
|
|
144
|
+
|
|
145
|
+
import { Peer } from "@decentnetwork/peer";
|
|
146
|
+
import { defaultBootstrapNodes } from "@decentnetwork/peer/scripts/legacy-bootstraps.mjs";
|
|
147
|
+
// ^ for production code, just inline a few bootstraps from the README
|
|
148
|
+
|
|
149
|
+
const peer = await Peer.create({
|
|
150
|
+
keyFile: "./receiver.save",
|
|
151
|
+
bootstrapNodes: defaultBootstrapNodes,
|
|
152
|
+
compatibilityMode: "legacy"
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Attach event listeners BEFORE start() so we don't miss anything
|
|
156
|
+
// that happens early (e.g. a friend already tried to reach us
|
|
157
|
+
// before we could bind the listener).
|
|
158
|
+
|
|
159
|
+
peer.onFriendRequest((request) => {
|
|
160
|
+
// request fields:
|
|
161
|
+
// pubkey — friend's pubkey (= userid)
|
|
162
|
+
// userid — same as pubkey (alias)
|
|
163
|
+
// address — friend's full address (when known)
|
|
164
|
+
// nospam — friend's nospam u32 (when known)
|
|
165
|
+
// name — display name in their friend-request payload
|
|
166
|
+
// description — status message in their payload
|
|
167
|
+
// hello — the optional greeting they included
|
|
168
|
+
console.log(`friend request from ${request.userid}`);
|
|
169
|
+
console.log(` name: ${request.name}`);
|
|
170
|
+
console.log(` description: ${request.description}`);
|
|
171
|
+
console.log(` hello: ${request.hello}`);
|
|
172
|
+
|
|
173
|
+
// Auto-accept everyone for this demo. In a real app you'd queue
|
|
174
|
+
// these for user approval.
|
|
175
|
+
void peer.acceptFriendRequest(request.pubkey).then(() => {
|
|
176
|
+
console.log(`accepted ${request.userid}`);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
peer.onFriendConnection((event) => {
|
|
181
|
+
// Fires when a friend's online/offline state changes. event:
|
|
182
|
+
// pubkey — friend
|
|
183
|
+
// status — "connected" | "disconnected"
|
|
184
|
+
console.log(`${event.pubkey} is now ${event.status}`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
peer.onFriendInfo((event) => {
|
|
188
|
+
// Fires when we learn a friend's display name / status message.
|
|
189
|
+
console.log(`${event.pubkey} updated: name="${event.name}" desc="${event.description}"`);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
peer.onText((msg) => {
|
|
193
|
+
// Inbound text message: msg.pubkey, msg.text.
|
|
194
|
+
console.log(`<${msg.pubkey}> ${msg.text}`);
|
|
195
|
+
|
|
196
|
+
// Echo back. `sendText` returns a Promise; resolves when the
|
|
197
|
+
// message is queued via TCP relay or Express. May throw if the
|
|
198
|
+
// friend isn't a friend (we never accepted/added them).
|
|
199
|
+
void peer.sendText(msg.pubkey, `echo: ${msg.text}`)
|
|
200
|
+
.catch((err) => console.error(`send failed: ${err.message}`));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await peer.start();
|
|
204
|
+
await peer.joinNetwork();
|
|
205
|
+
console.log(`address (give this to senders): ${peer.address()}`);
|
|
206
|
+
|
|
207
|
+
// Run forever — Ctrl+C to stop. In a long-running daemon, register
|
|
208
|
+
// a SIGINT handler that calls peer.stop() so friends.json gets
|
|
209
|
+
// flushed cleanly.
|
|
210
|
+
process.on("SIGINT", async () => {
|
|
211
|
+
console.log("\nstopping...");
|
|
212
|
+
await peer.stop();
|
|
213
|
+
process.exit(0);
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Sample 3 — Send a friend request and a message
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
// sender.mjs
|
|
223
|
+
|
|
224
|
+
import { Peer } from "@decentnetwork/peer";
|
|
225
|
+
|
|
226
|
+
// Paste the address printed by receiver.mjs:
|
|
227
|
+
const FRIEND_ADDRESS = "<paste address here>";
|
|
228
|
+
|
|
229
|
+
const peer = await Peer.create({
|
|
230
|
+
keyFile: "./sender.save",
|
|
231
|
+
bootstrapNodes: [
|
|
232
|
+
{ host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" },
|
|
233
|
+
{ host: "154.64.235.176", port: 33445, pk: "GdNtV2N74fZnLjhH7NhQ18nGdxb1k8jRM9dQaK7WnxmL" }
|
|
234
|
+
],
|
|
235
|
+
compatibilityMode: "legacy"
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Watch for the receiver's confirmation.
|
|
239
|
+
peer.onFriendConnection((event) => {
|
|
240
|
+
if (event.status === "connected") {
|
|
241
|
+
console.log(`✓ session up with ${event.pubkey}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
peer.onText((msg) => {
|
|
245
|
+
console.log(`<reply from ${msg.pubkey}> ${msg.text}`);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await peer.start();
|
|
249
|
+
await peer.joinNetwork();
|
|
250
|
+
|
|
251
|
+
// `sendFriendRequest`:
|
|
252
|
+
// - validates the address (parses pubkey + nospam + checksum)
|
|
253
|
+
// - adds the friend to our list with status="requested"
|
|
254
|
+
// - sends the request via onion routing AND posts to the express
|
|
255
|
+
// offline relay in parallel (whichever delivers first wins)
|
|
256
|
+
//
|
|
257
|
+
// Returns once both dispatches are queued. The receiver's
|
|
258
|
+
// `onFriendRequest` event fires on their side; once they call
|
|
259
|
+
// `acceptFriendRequest` and a session establishes, we'll see a
|
|
260
|
+
// `friendConnection { status: "connected" }` event.
|
|
261
|
+
console.log(`sending friend request to ${FRIEND_ADDRESS}...`);
|
|
262
|
+
await peer.sendFriendRequest(FRIEND_ADDRESS, "hi from sender.mjs");
|
|
263
|
+
|
|
264
|
+
console.log(`waiting for accept...`);
|
|
265
|
+
|
|
266
|
+
// Wait up to 60s for the session to come up. waitForFriendConnected
|
|
267
|
+
// extracts the userid from the address and resolves true on the
|
|
268
|
+
// first "connected" event.
|
|
269
|
+
import { carrierIdFromAddress } from "@decentnetwork/peer";
|
|
270
|
+
const friendUserId = carrierIdFromAddress(FRIEND_ADDRESS);
|
|
271
|
+
const ok = await peer.waitForFriendConnected(friendUserId, 60_000);
|
|
272
|
+
if (!ok) {
|
|
273
|
+
console.log("timed out waiting for accept; try again later");
|
|
274
|
+
await peer.stop();
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(`✓ sending text...`);
|
|
279
|
+
await peer.sendText(friendUserId, "first message");
|
|
280
|
+
await peer.sendText(friendUserId, "second message");
|
|
281
|
+
|
|
282
|
+
// Give the receiver a few seconds to reply before we shut down.
|
|
283
|
+
await new Promise((r) => setTimeout(r, 8_000));
|
|
284
|
+
|
|
285
|
+
await peer.stop();
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Sample 4 — Echo bot (long-running daemon)
|
|
291
|
+
|
|
292
|
+
A reusable headless bot that:
|
|
293
|
+
- accepts every incoming friend request
|
|
294
|
+
- echoes back whatever is sent to it
|
|
295
|
+
- handles graceful shutdown
|
|
296
|
+
- logs to a file
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
// echobot.mjs
|
|
300
|
+
//
|
|
301
|
+
// A long-running bot. Run with:
|
|
302
|
+
// node echobot.mjs &
|
|
303
|
+
//
|
|
304
|
+
// Logs go to ./echobot.log. Stop with `kill <pid>`.
|
|
305
|
+
|
|
306
|
+
import { Peer } from "@decentnetwork/peer";
|
|
307
|
+
import { appendFile } from "node:fs/promises";
|
|
308
|
+
|
|
309
|
+
const LOG_FILE = "./echobot.log";
|
|
310
|
+
|
|
311
|
+
async function log(line) {
|
|
312
|
+
const stamp = new Date().toISOString();
|
|
313
|
+
await appendFile(LOG_FILE, `${stamp} ${line}\n`);
|
|
314
|
+
console.log(line);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const peer = await Peer.create({
|
|
318
|
+
keyFile: "./echobot.save",
|
|
319
|
+
bootstrapNodes: [
|
|
320
|
+
{ host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" },
|
|
321
|
+
{ host: "154.64.235.176", port: 33445, pk: "GdNtV2N74fZnLjhH7NhQ18nGdxb1k8jRM9dQaK7WnxmL" },
|
|
322
|
+
{ host: "45.207.220.155", port: 33445, pk: "37PXijTXkyX9rUcRFjGo7PYpwf732KXQJgMtcKuMu3ym" }
|
|
323
|
+
],
|
|
324
|
+
// Optional debug label that shows in logs (handy when you run
|
|
325
|
+
// multiple peers in the same process):
|
|
326
|
+
debugLabel: "echobot",
|
|
327
|
+
compatibilityMode: "legacy"
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
peer.onFriendRequest(async (req) => {
|
|
331
|
+
await log(`friend request from ${req.userid} ("${req.hello}"); accepting`);
|
|
332
|
+
await peer.acceptFriendRequest(req.pubkey).catch((err) =>
|
|
333
|
+
log(`accept failed: ${err.message}`)
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
peer.onFriendConnection(async (e) => {
|
|
338
|
+
await log(`${e.pubkey} -> ${e.status}`);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
peer.onText(async (msg) => {
|
|
342
|
+
await log(`<${msg.pubkey}> ${msg.text}`);
|
|
343
|
+
// Sleep 200ms before replying so iOS UI has time to render the
|
|
344
|
+
// incoming bubble before the auto-reply lands.
|
|
345
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
346
|
+
try {
|
|
347
|
+
await peer.sendText(msg.pubkey, `echo: ${msg.text}`);
|
|
348
|
+
await log(`replied to ${msg.pubkey}`);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
await log(`reply failed: ${err.message}`);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await peer.start();
|
|
355
|
+
await peer.joinNetwork();
|
|
356
|
+
await log(`started; address=${peer.address()}`);
|
|
357
|
+
|
|
358
|
+
// Graceful shutdown so friends.json is flushed and TCP relays
|
|
359
|
+
// closed cleanly (otherwise relays keep our slot for a while).
|
|
360
|
+
const shutdown = async (sig) => {
|
|
361
|
+
await log(`received ${sig}; stopping`);
|
|
362
|
+
await peer.stop();
|
|
363
|
+
process.exit(0);
|
|
364
|
+
};
|
|
365
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
366
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
367
|
+
|
|
368
|
+
// Keep the process alive forever.
|
|
369
|
+
await new Promise(() => {});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Sample 5 — Interactive CLI chat
|
|
375
|
+
|
|
376
|
+
A two-pane REPL where you type messages to a single friend and see
|
|
377
|
+
their replies inline. Uses Node's built-in `readline`.
|
|
378
|
+
|
|
379
|
+
```js
|
|
380
|
+
// chat.mjs
|
|
381
|
+
//
|
|
382
|
+
// Usage:
|
|
383
|
+
// node chat.mjs <friend-address> # connect to friend, then type
|
|
384
|
+
//
|
|
385
|
+
// Or omit the argument to just run as a receiver and let someone
|
|
386
|
+
// add you (we print our own address at startup).
|
|
387
|
+
|
|
388
|
+
import { Peer, carrierIdFromAddress } from "@decentnetwork/peer";
|
|
389
|
+
import { createInterface } from "node:readline/promises";
|
|
390
|
+
import { stdin, stdout } from "node:process";
|
|
391
|
+
|
|
392
|
+
const FRIEND_ADDRESS = process.argv[2];
|
|
393
|
+
|
|
394
|
+
const peer = await Peer.create({
|
|
395
|
+
keyFile: "./chat.save",
|
|
396
|
+
bootstrapNodes: [
|
|
397
|
+
{ host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" },
|
|
398
|
+
{ host: "154.64.235.176", port: 33445, pk: "GdNtV2N74fZnLjhH7NhQ18nGdxb1k8jRM9dQaK7WnxmL" }
|
|
399
|
+
],
|
|
400
|
+
compatibilityMode: "legacy"
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
let activeFriend; // friend userid we're chatting with
|
|
404
|
+
|
|
405
|
+
peer.onFriendRequest(async (req) => {
|
|
406
|
+
console.log(`\n[friend request from ${req.userid}: "${req.hello}"]`);
|
|
407
|
+
console.log(`accepting automatically.`);
|
|
408
|
+
await peer.acceptFriendRequest(req.pubkey);
|
|
409
|
+
if (!activeFriend) {
|
|
410
|
+
activeFriend = req.pubkey;
|
|
411
|
+
console.log(`active chat: ${activeFriend}`);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
peer.onFriendConnection((e) => {
|
|
416
|
+
console.log(`\n[${e.pubkey} ${e.status}]`);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
peer.onText((msg) => {
|
|
420
|
+
// The "\r\x1b[K" clears the current line so the inbound bubble
|
|
421
|
+
// doesn't collide with the readline prompt the user is typing on.
|
|
422
|
+
process.stdout.write(`\r\x1b[K<${msg.pubkey.slice(0, 8)}…> ${msg.text}\n`);
|
|
423
|
+
process.stdout.write("> ");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await peer.start();
|
|
427
|
+
await peer.joinNetwork();
|
|
428
|
+
console.log(`my address: ${peer.address()}`);
|
|
429
|
+
|
|
430
|
+
if (FRIEND_ADDRESS) {
|
|
431
|
+
console.log(`adding friend ${FRIEND_ADDRESS}`);
|
|
432
|
+
await peer.sendFriendRequest(FRIEND_ADDRESS, "let's chat");
|
|
433
|
+
activeFriend = carrierIdFromAddress(FRIEND_ADDRESS);
|
|
434
|
+
console.log(`waiting for them to accept...`);
|
|
435
|
+
const ok = await peer.waitForFriendConnected(activeFriend, 60_000);
|
|
436
|
+
if (!ok) {
|
|
437
|
+
console.log(`timed out — they may accept later`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
442
|
+
console.log(`type messages, blank line to quit:`);
|
|
443
|
+
while (true) {
|
|
444
|
+
const line = (await rl.question("> ")).trim();
|
|
445
|
+
if (line === "") break;
|
|
446
|
+
if (!activeFriend) {
|
|
447
|
+
console.log(`no active friend yet — wait for someone to add you, or pass <address> on the command line`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
await peer.sendText(activeFriend, line);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.error(`send failed: ${err.message}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
rl.close();
|
|
458
|
+
await peer.stop();
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Sample 6 — Custom bootstrap nodes
|
|
464
|
+
|
|
465
|
+
For private deployments you can run your own bootstrap server
|
|
466
|
+
(`bootstrapd` from the Carrier source) and point peers at it:
|
|
467
|
+
|
|
468
|
+
```js
|
|
469
|
+
import { Peer } from "@decentnetwork/peer";
|
|
470
|
+
|
|
471
|
+
const peer = await Peer.create({
|
|
472
|
+
keyFile: "./peer.save",
|
|
473
|
+
bootstrapNodes: [
|
|
474
|
+
// Your private bootstrap (no public DHT participation):
|
|
475
|
+
{
|
|
476
|
+
host: "bootstrap.example.com",
|
|
477
|
+
port: 33445,
|
|
478
|
+
pk: "<base58 pubkey of your bootstrap>"
|
|
479
|
+
}
|
|
480
|
+
// Optionally include the public ones too for redundancy:
|
|
481
|
+
// { host: "47.100.103.201", port: 33445, pk: "CX1XH419..." }
|
|
482
|
+
],
|
|
483
|
+
// For an air-gapped private network, disable Express to avoid
|
|
484
|
+
// talking to lens.beagle.chat:
|
|
485
|
+
expressNodes: [],
|
|
486
|
+
compatibilityMode: "legacy"
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await peer.start();
|
|
490
|
+
await peer.joinNetwork();
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
To deploy your own bootstrap, see the
|
|
494
|
+
[Carrier bootstrap source](https://github.com/elastos/Elastos.CarrierClassic.Bootstrap).
|
|
495
|
+
A single bootstrap on a $5/month VPS handles thousands of peers.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Sample 7 — TypeScript with strict types
|
|
500
|
+
|
|
501
|
+
The package ships full `.d.ts`. Every exported symbol has a
|
|
502
|
+
TypeScript type:
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
// chat.ts
|
|
506
|
+
|
|
507
|
+
import {
|
|
508
|
+
Peer,
|
|
509
|
+
type PeerOptions,
|
|
510
|
+
type FriendRequest,
|
|
511
|
+
type FriendConnectionEvent,
|
|
512
|
+
type TextMessage,
|
|
513
|
+
carrierIdFromAddress
|
|
514
|
+
} from "@decentnetwork/peer";
|
|
515
|
+
|
|
516
|
+
const opts: PeerOptions = {
|
|
517
|
+
keyFile: "./peer.save",
|
|
518
|
+
bootstrapNodes: [
|
|
519
|
+
{ host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" }
|
|
520
|
+
],
|
|
521
|
+
compatibilityMode: "legacy"
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const peer = await Peer.create(opts);
|
|
525
|
+
|
|
526
|
+
peer.onFriendRequest((req: FriendRequest) => {
|
|
527
|
+
console.log(req.userid, req.hello);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
peer.onFriendConnection((ev: FriendConnectionEvent) => {
|
|
531
|
+
console.log(ev.pubkey, ev.status);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
peer.onText((msg: TextMessage) => {
|
|
535
|
+
console.log(msg.pubkey, msg.text);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
await peer.start();
|
|
539
|
+
await peer.joinNetwork();
|
|
540
|
+
console.log(peer.address());
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Public API reference
|
|
546
|
+
|
|
547
|
+
### `Peer.create(opts: PeerOptions): Promise<Peer>`
|
|
548
|
+
|
|
549
|
+
Constructs a peer. Doesn't open any sockets — call `start()` next.
|
|
550
|
+
|
|
551
|
+
`PeerOptions`:
|
|
552
|
+
|
|
553
|
+
| Field | Type | Required | Notes |
|
|
554
|
+
|---|---|---|---|
|
|
555
|
+
| `keyFile` | `string` | yes | Path to the long-term keypair file (auto-created on first run) |
|
|
556
|
+
| `bootstrapNodes` | `NetworkNode[]` | yes | At least one DHT bootstrap; more = better resilience |
|
|
557
|
+
| `expressNodes` | `NetworkNode[]` | no | HTTP offline relay (default: `lens.beagle.chat:443`) |
|
|
558
|
+
| `compatibilityMode` | `"legacy"` | no | Only "legacy" supported. Defaults to legacy. |
|
|
559
|
+
| `friendStoreFile` | `string` | no | Defaults to `<keyFile>.friends.json` |
|
|
560
|
+
| `debugLabel` | `string` | no | Prepended to debug log lines |
|
|
561
|
+
|
|
562
|
+
### Lifecycle
|
|
563
|
+
|
|
564
|
+
- `peer.start(): Promise<void>` — bind UDP, open TCP relays, load friends
|
|
565
|
+
- `peer.stop(): Promise<void>` — flush friends, close sockets/relays
|
|
566
|
+
- `peer.joinNetwork(): Promise<BootstrapResult>` — DHT bootstrap
|
|
567
|
+
|
|
568
|
+
### Identity
|
|
569
|
+
|
|
570
|
+
- `peer.pubkey(): string` — 32-byte pubkey, base58 — same as `userid()`
|
|
571
|
+
- `peer.userid(): string` — same as `pubkey()`
|
|
572
|
+
- `peer.address(): string` — pubkey + nospam + checksum, base58 — share this
|
|
573
|
+
|
|
574
|
+
### Friends (data plane)
|
|
575
|
+
|
|
576
|
+
- `peer.sendFriendRequest(address: string, hello?: string): Promise<void>`
|
|
577
|
+
- `peer.acceptFriendRequest(pubkey: string): Promise<void>`
|
|
578
|
+
- `peer.rejectFriendRequest(pubkey: string): void`
|
|
579
|
+
- `peer.removeFriend(addressOrUserid: string): boolean` — drops a friend, returns true if existed
|
|
580
|
+
- `peer.sendText(pubkey: string, text: string): Promise<void>`
|
|
581
|
+
- `peer.friends(): FriendRecord[]` — current friends list
|
|
582
|
+
- `peer.waitForFriendRequest(timeoutMs?: number): Promise<FriendRequest>`
|
|
583
|
+
- `peer.waitForFriendConnected(pubkey: string, timeoutMs?: number): Promise<boolean>`
|
|
584
|
+
|
|
585
|
+
### Events
|
|
586
|
+
|
|
587
|
+
| Method | Event payload |
|
|
588
|
+
|---|---|
|
|
589
|
+
| `peer.onFriendRequest(cb)` | `FriendRequest` — incoming pending request |
|
|
590
|
+
| `peer.onFriendConnection(cb)` | `FriendConnectionEvent` — `{pubkey, status}` |
|
|
591
|
+
| `peer.onFriendInfo(cb)` | `FriendInfoEvent` — `{pubkey, userid, name, description}` |
|
|
592
|
+
| `peer.onText(cb)` | `TextMessage` — `{pubkey, text}` |
|
|
593
|
+
|
|
594
|
+
### Helpers (re-exported from `compat/address.ts`)
|
|
595
|
+
|
|
596
|
+
- `carrierAddressFromPublicKey(publicKey, nospam?): string`
|
|
597
|
+
- `carrierIdFromAddress(address): string` — strip nospam+checksum
|
|
598
|
+
- `carrierIdFromPublicKey(publicKey): string`
|
|
599
|
+
- `parseCarrierAddress(address): CarrierAddressParts`
|
|
600
|
+
|
|
601
|
+
### Constants
|
|
602
|
+
|
|
603
|
+
- `CARRIER_PUBLIC_KEY_SIZE = 32`
|
|
604
|
+
- `CARRIER_ADDRESS_SIZE = 38`
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## Configuration via environment
|
|
609
|
+
|
|
610
|
+
All operator-tuneable knobs are listed in the main README under
|
|
611
|
+
"Configuration". Most users won't need any of them — defaults match
|
|
612
|
+
toxcore / iOS Beagle.
|
|
613
|
+
|
|
614
|
+
The most useful ones during development:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
# Enable debug logs
|
|
618
|
+
DECENT_DEBUG=1 node app.mjs
|
|
619
|
+
|
|
620
|
+
# Disable Express (offline relay) for cleaner test traces
|
|
621
|
+
DECENT_DISABLE_EXPRESS=1 node app.mjs
|
|
622
|
+
|
|
623
|
+
# Auto-accept incoming friend requests (useful for bots/tests)
|
|
624
|
+
DECENT_AUTO_ACCEPT=1 node app.mjs
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## Common patterns
|
|
630
|
+
|
|
631
|
+
### Wait until a friend's session is established
|
|
632
|
+
|
|
633
|
+
```js
|
|
634
|
+
// Returns true once the friend's session is up, false on timeout.
|
|
635
|
+
const userid = carrierIdFromAddress(theirAddress);
|
|
636
|
+
const ok = await peer.waitForFriendConnected(userid, 60_000);
|
|
637
|
+
if (!ok) {
|
|
638
|
+
console.error("friend didn't come online in 60s");
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Persist incoming messages to your own database
|
|
643
|
+
|
|
644
|
+
```js
|
|
645
|
+
import { open } from "node:sqlite"; // or any DB driver
|
|
646
|
+
|
|
647
|
+
const db = await open({ filename: "./messages.db" });
|
|
648
|
+
await db.run(`create table if not exists msg (id integer primary key, peer text, text text, ts integer)`);
|
|
649
|
+
|
|
650
|
+
peer.onText(async (msg) => {
|
|
651
|
+
await db.run(`insert into msg(peer,text,ts) values (?,?,?)`, msg.pubkey, msg.text, Date.now());
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Multi-process: one Peer per process
|
|
656
|
+
|
|
657
|
+
`@decentnetwork/peer` binds a UDP socket; you can only have one peer per
|
|
658
|
+
keyfile per port. If you want multiple peer identities on the same
|
|
659
|
+
machine, spawn separate processes each with its own `keyFile`. Use
|
|
660
|
+
`debugLabel` to tag their logs.
|
|
661
|
+
|
|
662
|
+
### Custom UDP port
|
|
663
|
+
|
|
664
|
+
The transport tries 33445, 33446, 33447, … falling back to
|
|
665
|
+
ephemeral. To force a specific port, override the bootstrap setup
|
|
666
|
+
or set `DECENT_UDP_PORT_PREFER=N` (env-only currently).
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## Error handling
|
|
671
|
+
|
|
672
|
+
All async methods reject with an `Error` (or subclass). Handle
|
|
673
|
+
them as you would any Promise rejection. The most common failure
|
|
674
|
+
modes:
|
|
675
|
+
|
|
676
|
+
| Error message | Likely cause |
|
|
677
|
+
|---|---|
|
|
678
|
+
| `Peer is not started` | `joinNetwork` / `pubkey` called before `start` |
|
|
679
|
+
| `bootstrap timed out after 30000ms` | All bootstraps unreachable; check `bootstrapNodes` |
|
|
680
|
+
| `Carrier address checksum mismatch` | The `address` string passed to `sendFriendRequest` is malformed |
|
|
681
|
+
| `Not a friend: <userid>` | `sendText` called for someone you haven't added |
|
|
682
|
+
| `friend is offline and no express node is configured` | Receiver is offline, you disabled Express, no other path |
|
|
683
|
+
| `friend session unavailable for <userid>` | Internal — session was torn down between check and send |
|
|
684
|
+
|
|
685
|
+
For deeper diagnosis, run with `DECENT_DEBUG=1` and consult the
|
|
686
|
+
debug markers in [`IOS_INTEROP_PLAYBOOK.md`](IOS_INTEROP_PLAYBOOK.md) §5.
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## What you can build
|
|
691
|
+
|
|
692
|
+
This package gives you a P2P encrypted messaging substrate. Apps
|
|
693
|
+
others have built / are building on it:
|
|
694
|
+
|
|
695
|
+
- Peer-to-peer chat clients (CLI, Electron, mobile)
|
|
696
|
+
- Decentralized notification systems (peer-to-peer push)
|
|
697
|
+
- Off-grid messaging companions for IoT/robotics
|
|
698
|
+
- Backend bots (echo, indexing, monitoring)
|
|
699
|
+
- File-transfer prototypes (chunk text messages, or roll your own
|
|
700
|
+
on top of `peer.sendText` with framing)
|
|
701
|
+
|
|
702
|
+
Future capabilities the package doesn't yet offer (PRs welcome):
|
|
703
|
+
|
|
704
|
+
- Group chats / conferences
|
|
705
|
+
- Native file-transfer (`tox_file_send` equivalent)
|
|
706
|
+
- Voice/video (TURN/STUN integration)
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
## Getting help
|
|
711
|
+
|
|
712
|
+
- **Issues**: <https://github.com/0xli/peer/issues>
|
|
713
|
+
- **Bug catalog from real iOS interop testing**: [`IOS_INTEROP_PLAYBOOK.md`](IOS_INTEROP_PLAYBOOK.md)
|
|
714
|
+
- **Protocol questions**: read [`PROTOCOL_OVERVIEW.md`](PROTOCOL_OVERVIEW.md)
|
|
715
|
+
and [`CARRIER_VS_TOX.md`](CARRIER_VS_TOX.md) — most "why doesn't X
|
|
716
|
+
work?" questions are answered by the protocol semantics
|
|
717
|
+
|
|
718
|
+
When opening an issue, please include:
|
|
719
|
+
|
|
720
|
+
1. The `Peer.create(...)` options (with the keyFile path
|
|
721
|
+
redacted)
|
|
722
|
+
2. A `DECENT_DEBUG=1` log of the failure, with the relevant
|
|
723
|
+
~30 lines around the unexpected behavior
|
|
724
|
+
3. Your Node version and OS
|