@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,3 @@
1
+ export declare class LegacyProtocolNotImplementedError extends Error {
2
+ constructor(area: string);
3
+ }
@@ -0,0 +1,6 @@
1
+ export class LegacyProtocolNotImplementedError extends Error {
2
+ constructor(area) {
3
+ super(`${area} is not implemented: legacy Carrier/toxcore wire compatibility is still unresolved`);
4
+ this.name = "LegacyProtocolNotImplementedError";
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ export type PeerEventMap = {
2
+ friendRequest: unknown;
3
+ text: unknown;
4
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export type LifecycleState = "stopped" | "started";
2
+ export declare class Lifecycle {
3
+ #private;
4
+ get state(): LifecycleState;
5
+ markStarted(): void;
6
+ markStopped(): void;
7
+ }
@@ -0,0 +1,12 @@
1
+ export class Lifecycle {
2
+ #state = "stopped";
3
+ get state() {
4
+ return this.#state;
5
+ }
6
+ markStarted() {
7
+ this.#state = "started";
8
+ }
9
+ markStopped() {
10
+ this.#state = "stopped";
11
+ }
12
+ }
@@ -0,0 +1,2 @@
1
+ import type { PeerOptions } from "../types/peer.js";
2
+ export type StoredPeerConfig = PeerOptions;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ export type FriendRecord = {
2
+ pubkey: string;
3
+ userid?: string;
4
+ address?: string;
5
+ nospam?: number;
6
+ name?: string;
7
+ description?: string;
8
+ status: "requested" | "offline" | "online";
9
+ remoteHost?: string;
10
+ remotePort?: number;
11
+ hello?: string;
12
+ acceptedAt?: number;
13
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export type PeerState = {
2
+ pubkey?: string;
3
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export type TransportEndpoint = {
2
+ host: string;
3
+ port: number;
4
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare class TcpTransport {
2
+ start(): Promise<never>;
3
+ }
@@ -0,0 +1,5 @@
1
+ export class TcpTransport {
2
+ async start() {
3
+ throw new Error("TCP transport is not implemented yet");
4
+ }
5
+ }
@@ -0,0 +1,24 @@
1
+ import { type RemoteInfo } from "node:dgram";
2
+ import { EventEmitter } from "node:events";
3
+ export type UdpDatagram = {
4
+ data: Buffer;
5
+ remote: RemoteInfo;
6
+ };
7
+ export type UdpTransportOptions = {
8
+ host?: string;
9
+ port?: number;
10
+ };
11
+ export declare class UdpTransport extends EventEmitter {
12
+ #private;
13
+ constructor();
14
+ get bound(): boolean;
15
+ start(opts?: UdpTransportOptions): Promise<void>;
16
+ stop(): Promise<void>;
17
+ /**
18
+ * The OS-assigned local UDP port (after bind). Useful so the peer layer
19
+ * can recognize and skip its own address in friend endpoint candidates
20
+ * (some toxcore peers echo our address back inside DHT-PK extras).
21
+ */
22
+ localPort(): number | undefined;
23
+ send(data: Buffer, host: string, port: number): Promise<void>;
24
+ }
@@ -0,0 +1,90 @@
1
+ import dgram from "node:dgram";
2
+ import { EventEmitter } from "node:events";
3
+ export class UdpTransport extends EventEmitter {
4
+ #socket;
5
+ #bound = false;
6
+ constructor() {
7
+ super();
8
+ // A long-running peer connects to ~9 bootstrap nodes and ~3 TCP
9
+ // relays, each of which attaches a `datagram` listener for its
10
+ // own routing. Plus the Peer class itself, the announce loop, the
11
+ // friend connection probe, etc. Node's default of 10 trips
12
+ // immediately ("51 datagram listeners added to [UdpTransport]");
13
+ // raise on this instance to suppress the warning. Real leaks
14
+ // would still trigger via the test suite's stricter checks.
15
+ this.setMaxListeners(100);
16
+ }
17
+ get bound() {
18
+ return this.#bound;
19
+ }
20
+ async start(opts = {}) {
21
+ if (this.#socket) {
22
+ return;
23
+ }
24
+ const socket = dgram.createSocket("udp4");
25
+ this.#socket = socket;
26
+ socket.on("message", (data, remote) => {
27
+ this.emit("datagram", { data, remote });
28
+ });
29
+ socket.on("error", (error) => this.emit("error", error));
30
+ await new Promise((resolve, reject) => {
31
+ const cleanup = () => {
32
+ socket.off("listening", onListening);
33
+ socket.off("error", onError);
34
+ };
35
+ const onListening = () => {
36
+ cleanup();
37
+ this.#bound = true;
38
+ resolve();
39
+ };
40
+ const onError = (error) => {
41
+ cleanup();
42
+ reject(error);
43
+ };
44
+ socket.once("listening", onListening);
45
+ socket.once("error", onError);
46
+ socket.bind(opts.port ?? 0, opts.host);
47
+ });
48
+ }
49
+ async stop() {
50
+ const socket = this.#socket;
51
+ if (!socket) {
52
+ return;
53
+ }
54
+ this.#socket = undefined;
55
+ this.#bound = false;
56
+ await new Promise((resolve) => socket.close(() => resolve()));
57
+ }
58
+ /**
59
+ * The OS-assigned local UDP port (after bind). Useful so the peer layer
60
+ * can recognize and skip its own address in friend endpoint candidates
61
+ * (some toxcore peers echo our address back inside DHT-PK extras).
62
+ */
63
+ localPort() {
64
+ try {
65
+ const addr = this.#socket?.address();
66
+ if (addr && typeof addr === "object" && "port" in addr) {
67
+ return addr.port;
68
+ }
69
+ }
70
+ catch {
71
+ // ignore
72
+ }
73
+ return undefined;
74
+ }
75
+ async send(data, host, port) {
76
+ const socket = this.#socket;
77
+ if (!socket || !this.#bound) {
78
+ throw new Error("UDP transport is not started");
79
+ }
80
+ await new Promise((resolve, reject) => {
81
+ socket.send(data, port, host, (error) => {
82
+ if (error) {
83
+ reject(error);
84
+ return;
85
+ }
86
+ resolve();
87
+ });
88
+ });
89
+ }
90
+ }
@@ -0,0 +1,2 @@
1
+ import type { NetworkNode } from "./peer.js";
2
+ export type BootstrapNode = NetworkNode;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export type DhtPeerRecord = {
2
+ pubkey: string;
3
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export type FriendId = string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export type MessageId = string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ export type CompatibilityMode = "legacy";
2
+ export type NetworkNode = {
3
+ host: string;
4
+ port: number;
5
+ pk?: string;
6
+ /**
7
+ * True if this entry was learned from a packed-nodes record with
8
+ * family = TCP_FAMILY_IPV4 (130) or TCP_FAMILY_IPV6 (138). Such nodes
9
+ * should be added to the TCP relay client pool rather than treated
10
+ * as UDP DHT nodes — iOS Beagle peers advertise their reachable
11
+ * relays this way in DHT-PK extras.
12
+ */
13
+ isTcp?: boolean;
14
+ };
15
+ export type PeerOptions = {
16
+ keyFile: string;
17
+ friendStoreFile?: string;
18
+ bootstrapNodes: NetworkNode[];
19
+ expressNodes?: NetworkNode[];
20
+ compatibilityMode?: CompatibilityMode;
21
+ debugLabel?: string;
22
+ };
23
+ export type FriendRequest = {
24
+ pubkey: string;
25
+ address?: string;
26
+ userid?: string;
27
+ nospam?: number;
28
+ name?: string;
29
+ description?: string;
30
+ hello?: string;
31
+ };
32
+ export type FriendConnectionStatus = "connected" | "disconnected";
33
+ export type FriendConnectionEvent = {
34
+ pubkey: string;
35
+ status: FriendConnectionStatus;
36
+ };
37
+ export type FriendInfoEvent = {
38
+ pubkey: string;
39
+ userid?: string;
40
+ name?: string;
41
+ description?: string;
42
+ };
43
+ export type TextMessage = {
44
+ pubkey: string;
45
+ text: string;
46
+ };
47
+ export type LookupResult = {
48
+ pubkey: string;
49
+ status: "unknown";
50
+ records: [];
51
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export type SessionId = string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function base58ToBytes(value: string): Uint8Array;
2
+ export declare function bytesToBase58(bytes: Uint8Array): string;
@@ -0,0 +1,51 @@
1
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2
+ const BASE = BigInt(58);
3
+ const indexes = new Map([...ALPHABET].map((char, index) => [char, BigInt(index)]));
4
+ export function base58ToBytes(value) {
5
+ if (!value) {
6
+ throw new Error("base58 value is required");
7
+ }
8
+ let num = 0n;
9
+ for (const char of value) {
10
+ const digit = indexes.get(char);
11
+ if (digit === undefined) {
12
+ throw new Error(`invalid base58 character: ${char}`);
13
+ }
14
+ num = num * BASE + digit;
15
+ }
16
+ const bytes = [];
17
+ while (num > 0n) {
18
+ bytes.push(Number(num & 0xffn));
19
+ num >>= 8n;
20
+ }
21
+ bytes.reverse();
22
+ for (const char of value) {
23
+ if (char !== "1") {
24
+ break;
25
+ }
26
+ bytes.unshift(0);
27
+ }
28
+ return Uint8Array.from(bytes);
29
+ }
30
+ export function bytesToBase58(bytes) {
31
+ if (!bytes.length) {
32
+ return "";
33
+ }
34
+ let num = 0n;
35
+ for (const byte of bytes) {
36
+ num = (num << 8n) + BigInt(byte);
37
+ }
38
+ let encoded = "";
39
+ while (num > 0n) {
40
+ const rem = Number(num % BASE);
41
+ encoded = ALPHABET[rem] + encoded;
42
+ num /= BASE;
43
+ }
44
+ for (const byte of bytes) {
45
+ if (byte !== 0) {
46
+ break;
47
+ }
48
+ encoded = "1" + encoded;
49
+ }
50
+ return encoded || "1";
51
+ }
@@ -0,0 +1,4 @@
1
+ export declare function bytesToHex(bytes: Uint8Array): string;
2
+ export declare function hexToBytes(hex: string): Uint8Array;
3
+ export declare function concatBytes(parts: Uint8Array[]): Uint8Array;
4
+ export declare function randomBytes(length: number): Uint8Array;
@@ -0,0 +1,31 @@
1
+ export function bytesToHex(bytes) {
2
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
3
+ }
4
+ export function hexToBytes(hex) {
5
+ if (hex.length % 2 !== 0) {
6
+ throw new Error("hex string must have an even length");
7
+ }
8
+ const bytes = new Uint8Array(hex.length / 2);
9
+ for (let i = 0; i < bytes.length; i++) {
10
+ const value = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
11
+ if (Number.isNaN(value)) {
12
+ throw new Error("invalid hex string");
13
+ }
14
+ bytes[i] = value;
15
+ }
16
+ return bytes;
17
+ }
18
+ export function concatBytes(parts) {
19
+ const total = parts.reduce((size, part) => size + part.length, 0);
20
+ const out = new Uint8Array(total);
21
+ let offset = 0;
22
+ for (const part of parts) {
23
+ out.set(part, offset);
24
+ offset += part.length;
25
+ }
26
+ return out;
27
+ }
28
+ export function randomBytes(length) {
29
+ return nodeRandomBytes(length);
30
+ }
31
+ import { randomBytes as nodeRandomBytes } from "node:crypto";
@@ -0,0 +1,103 @@
1
+ # Installing `@decentnetwork/peer`
2
+
3
+ A pure TypeScript / Node.js implementation of the Elastos Carrier
4
+ (toxcore-derived) peer-to-peer protocol. Use it as a library in
5
+ your own Node app, or run the bundled `decent-peer` CLI for ad-hoc
6
+ identity / friend / messaging operations.
7
+
8
+ ## Requirements
9
+
10
+ - **Node.js 20 or newer** (uses modern WebStreams + AbortController).
11
+ - Any OS Node runs on. No native compilation; all crypto is pure JS.
12
+ - Outbound network — TCP and/or UDP to public Carrier bootstrap
13
+ nodes (port 33445 + 443). No inbound port needed.
14
+
15
+ ## Install (as a library)
16
+
17
+ ```bash
18
+ npm install @decentnetwork/peer
19
+ # or
20
+ pnpm add @decentnetwork/peer
21
+ # or
22
+ yarn add @decentnetwork/peer
23
+ ```
24
+
25
+ Then in your code:
26
+
27
+ ```ts
28
+ import { Peer } from "@decentnetwork/peer";
29
+
30
+ const peer = await Peer.create({
31
+ keyFile: "./my-peer.save",
32
+ compatibilityMode: "legacy",
33
+ bootstrapNodes: [
34
+ { host: "47.100.103.201", port: 33445,
35
+ pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" },
36
+ // ... more, see USAGE_GUIDE.md
37
+ ],
38
+ });
39
+ await peer.start();
40
+ await peer.joinNetwork();
41
+ console.log("my address:", peer.address());
42
+ ```
43
+
44
+ See [`USAGE_GUIDE.md`](USAGE_GUIDE.md) for the full programmatic
45
+ API with copy-paste-ready examples for every common task
46
+ (friending, text messages, binary packets, offline relay).
47
+
48
+ ## Install (CLI, globally)
49
+
50
+ ```bash
51
+ npm install -g @decentnetwork/peer
52
+ ```
53
+
54
+ That puts `decent-peer` on your `$PATH`:
55
+
56
+ ```bash
57
+ decent-peer --help
58
+ decent-peer identity # print address + userid
59
+ decent-peer join # sit in the network until killed (useful for testing)
60
+ ```
61
+
62
+ The CLI is mostly for diagnostics; production usage almost always
63
+ embeds the library directly.
64
+
65
+ ## Configuration
66
+
67
+ The library is configured per-call (no global config file).
68
+ Persistent state lives in the **key file** you pass to
69
+ `Peer.create`. That file holds:
70
+
71
+ - The keypair (Ed25519 + X25519-derived)
72
+ - A separate "friends" file alongside it (`<keyFile>.friends.json`)
73
+ with the friend store
74
+
75
+ Treat both like SSH private keys — back them up; deleting them
76
+ gives this peer a brand-new identity.
77
+
78
+ Knobs:
79
+
80
+ | option / env | what |
81
+ |---|---|
82
+ | `Peer.create({ keyFile })` | path to the identity file (created if missing) |
83
+ | `Peer.create({ bootstrapNodes })` | DHT entry points |
84
+ | `Peer.create({ expressNodes })` | optional HTTPS offline-message relays |
85
+ | `Peer.create({ compatibilityMode: "legacy" })` | required for interop with iOS Beagle and the Carrier C SDK |
86
+ | `DECENT_DEBUG=1` env var | verbose protocol logs |
87
+ | `DECENT_EXPRESS_PULL_INTERVAL_MS` | offline-relay poll cadence (default 4000) |
88
+ | `DECENT_FRIEND_CONNECTION_LOOP_MS` | friend-connection tick (default 250) |
89
+
90
+ ## Upgrading
91
+
92
+ `npm install @decentnetwork/peer@latest` (or `-g` for the CLI).
93
+ The keypair file format is stable; upgrades preserve identity and
94
+ friends.
95
+
96
+ ## See also
97
+
98
+ - [`USAGE_GUIDE.md`](USAGE_GUIDE.md) — programmatic API guide,
99
+ copy-paste examples for every common task
100
+ - Protocol-level reference (DHT / onion / TCP relay / FlatBuffers
101
+ message kinds): not shipped in this tarball — see
102
+ `docs/PROTOCOL_OVERVIEW.md` in the GitHub repo.
103
+ - Project page: https://github.com/0xli/peer