@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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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,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";
|
package/docs/INSTALL.md
ADDED
|
@@ -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
|