@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.
Files changed (79) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +97 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +245 -0
  5. package/dist/compat/address.d.ts +13 -0
  6. package/dist/compat/address.js +69 -0
  7. package/dist/compat/bootstrap.d.ts +29 -0
  8. package/dist/compat/bootstrap.js +178 -0
  9. package/dist/compat/dht.d.ts +3 -0
  10. package/dist/compat/dht.js +9 -0
  11. package/dist/compat/express.d.ts +21 -0
  12. package/dist/compat/express.js +263 -0
  13. package/dist/compat/friend.d.ts +4 -0
  14. package/dist/compat/friend.js +12 -0
  15. package/dist/compat/net-crypto.d.ts +84 -0
  16. package/dist/compat/net-crypto.js +278 -0
  17. package/dist/compat/packet.d.ts +55 -0
  18. package/dist/compat/packet.js +154 -0
  19. package/dist/compat/session.d.ts +3 -0
  20. package/dist/compat/session.js +7 -0
  21. package/dist/compat/tcp-relay-pool.d.ts +85 -0
  22. package/dist/compat/tcp-relay-pool.js +342 -0
  23. package/dist/compat/tcp-relay.d.ts +96 -0
  24. package/dist/compat/tcp-relay.js +489 -0
  25. package/dist/compat/text.d.ts +3 -0
  26. package/dist/compat/text.js +8 -0
  27. package/dist/compat/tox-dht-crypto.d.ts +18 -0
  28. package/dist/compat/tox-dht-crypto.js +69 -0
  29. package/dist/compat/tox-onion.d.ts +66 -0
  30. package/dist/compat/tox-onion.js +172 -0
  31. package/dist/crypto/box.d.ts +1 -0
  32. package/dist/crypto/box.js +3 -0
  33. package/dist/crypto/keypair.d.ts +5 -0
  34. package/dist/crypto/keypair.js +37 -0
  35. package/dist/crypto/nonce.d.ts +1 -0
  36. package/dist/crypto/nonce.js +1 -0
  37. package/dist/crypto/sign.d.ts +1 -0
  38. package/dist/crypto/sign.js +3 -0
  39. package/dist/index.d.ts +10 -0
  40. package/dist/index.js +6 -0
  41. package/dist/peer.d.ts +45 -0
  42. package/dist/peer.js +3425 -0
  43. package/dist/runtime/errors.d.ts +3 -0
  44. package/dist/runtime/errors.js +6 -0
  45. package/dist/runtime/events.d.ts +4 -0
  46. package/dist/runtime/events.js +1 -0
  47. package/dist/runtime/lifecycle.d.ts +7 -0
  48. package/dist/runtime/lifecycle.js +12 -0
  49. package/dist/store/config.d.ts +2 -0
  50. package/dist/store/config.js +1 -0
  51. package/dist/store/friends.d.ts +13 -0
  52. package/dist/store/friends.js +1 -0
  53. package/dist/store/state.d.ts +3 -0
  54. package/dist/store/state.js +1 -0
  55. package/dist/transport/socket.d.ts +4 -0
  56. package/dist/transport/socket.js +1 -0
  57. package/dist/transport/tcp.d.ts +3 -0
  58. package/dist/transport/tcp.js +5 -0
  59. package/dist/transport/udp.d.ts +24 -0
  60. package/dist/transport/udp.js +90 -0
  61. package/dist/types/bootstrap.d.ts +2 -0
  62. package/dist/types/bootstrap.js +1 -0
  63. package/dist/types/dht.d.ts +3 -0
  64. package/dist/types/dht.js +1 -0
  65. package/dist/types/friend.d.ts +1 -0
  66. package/dist/types/friend.js +1 -0
  67. package/dist/types/message.d.ts +1 -0
  68. package/dist/types/message.js +1 -0
  69. package/dist/types/peer.d.ts +51 -0
  70. package/dist/types/peer.js +1 -0
  71. package/dist/types/session.d.ts +1 -0
  72. package/dist/types/session.js +1 -0
  73. package/dist/utils/base58.d.ts +2 -0
  74. package/dist/utils/base58.js +51 -0
  75. package/dist/utils/bytes.d.ts +4 -0
  76. package/dist/utils/bytes.js +31 -0
  77. package/docs/INSTALL.md +103 -0
  78. package/docs/USAGE_GUIDE.md +724 -0
  79. 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