@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
package/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
GNU GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 29 June 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
This package is licensed under the GNU General Public License v3.0 or any
|
|
9
|
+
later version. You may redistribute and/or modify it under the terms of
|
|
10
|
+
the GNU General Public License as published by the Free Software
|
|
11
|
+
Foundation, either version 3 of the License, or (at your option) any
|
|
12
|
+
later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful, but
|
|
15
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
17
|
+
Public License for more details.
|
|
18
|
+
|
|
19
|
+
The full text of the GPL-3.0-or-later license is available at
|
|
20
|
+
<https://www.gnu.org/licenses/gpl-3.0.html>.
|
|
21
|
+
|
|
22
|
+
This package derives in part from the Elastos.NET.Carrier.Native.SDK,
|
|
23
|
+
which itself derives from the toxcore project — both also licensed under
|
|
24
|
+
GPL-3.0-or-later. The protocol-level parity with those upstreams is
|
|
25
|
+
intentional; the wire format implementations in `compat/` reference the
|
|
26
|
+
toxcore C source by file:line in their top comments.
|
|
27
|
+
|
|
28
|
+
- Elastos.NET.Carrier.Native.SDK:
|
|
29
|
+
https://github.com/elastos/Elastos.NET.Carrier.Native.SDK
|
|
30
|
+
- c-toxcore:
|
|
31
|
+
https://github.com/TokTok/c-toxcore
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# `@decentnetwork/peer`
|
|
2
|
+
|
|
3
|
+
Pure TypeScript / Node.js port of the [Elastos Carrier Native SDK][carrier-c]
|
|
4
|
+
(Carrier-flavored toxcore). Wire-compatible with the C SDK and iOS Beagle —
|
|
5
|
+
the same DHT, onion routing, FlatBuffers app payloads, TCP relay protocol,
|
|
6
|
+
and Express HTTP store-and-forward relay. End-to-end interop with iOS Beagle
|
|
7
|
+
on iPad is verified working.
|
|
8
|
+
|
|
9
|
+
[carrier-c]: https://github.com/elastos/Elastos.NET.Carrier.Native.SDK
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @decentnetwork/peer
|
|
13
|
+
# or
|
|
14
|
+
npm install @decentnetwork/peer
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 30-second example
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Peer } from "@decentnetwork/peer";
|
|
21
|
+
|
|
22
|
+
const peer = await Peer.create({
|
|
23
|
+
keyFile: "./peer.save",
|
|
24
|
+
bootstrapNodes: [
|
|
25
|
+
{
|
|
26
|
+
host: "47.100.103.201",
|
|
27
|
+
port: 33445,
|
|
28
|
+
pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd"
|
|
29
|
+
}
|
|
30
|
+
// (more bootstraps recommended — see docs/USAGE_GUIDE.md)
|
|
31
|
+
],
|
|
32
|
+
compatibilityMode: "legacy"
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await peer.start();
|
|
36
|
+
await peer.joinNetwork();
|
|
37
|
+
|
|
38
|
+
console.log("my address:", peer.address());
|
|
39
|
+
|
|
40
|
+
peer.onText((msg) => {
|
|
41
|
+
console.log(`from ${msg.pubkey}: ${msg.text}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Send a friend request to a peer (their address comes from their UI):
|
|
45
|
+
await peer.sendFriendRequest(
|
|
46
|
+
"ZJxuWL9SDqdvunnCSMLUd5jyGCaBV44G6THYaQS7ZaZAz1wmt4nz",
|
|
47
|
+
"hello!"
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Once they accept and the session establishes, send a message:
|
|
51
|
+
await peer.sendText(
|
|
52
|
+
"FhbohSLrj5UjdyFKCEYNeEWAPq3QD9hRg6hsso5ipag2",
|
|
53
|
+
"first message via @decentnetwork/peer"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await peer.stop();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Optional CLI
|
|
60
|
+
|
|
61
|
+
For users who just want to run a peer without writing JS, the package
|
|
62
|
+
also ships an optional `decent-peer` binary (the library export is
|
|
63
|
+
unchanged — the CLI is additive):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install -g @decentnetwork/peer
|
|
67
|
+
decent-peer init # creates ~/.decent-peer/
|
|
68
|
+
decent-peer address # print your address
|
|
69
|
+
decent-peer listen # daemon mode (Ctrl-C to stop)
|
|
70
|
+
decent-peer send <addr> "hello" # one-shot send
|
|
71
|
+
decent-peer add-friend <addr> "hi from me" # one-shot friend request
|
|
72
|
+
decent-peer accept <pubkey> # accept a pending request
|
|
73
|
+
|
|
74
|
+
# zero-install:
|
|
75
|
+
npx @decentnetwork/peer listen
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Config lives at `~/.decent-peer/config.json` (override with the
|
|
79
|
+
`DECENT_PEER_CONFIG` env var). The keyfile at `~/.decent-peer/peer.save`
|
|
80
|
+
is your identity — back it up.
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
- **Usage guide**: full developer-facing tutorial with sample apps —
|
|
85
|
+
see [`docs/USAGE_GUIDE.md`](https://github.com/0xli/peer/blob/main/docs/USAGE_GUIDE.md)
|
|
86
|
+
- **Protocol overview**: actors, identities, lifecycle —
|
|
87
|
+
[`docs/PROTOCOL_OVERVIEW.md`](https://github.com/0xli/peer/blob/main/docs/PROTOCOL_OVERVIEW.md)
|
|
88
|
+
- **Carrier vs Tox/Onion**: where Carrier diverges from upstream toxcore —
|
|
89
|
+
[`docs/CARRIER_VS_TOX.md`](https://github.com/0xli/peer/blob/main/docs/CARRIER_VS_TOX.md)
|
|
90
|
+
- **Discovery flow**: the four discovery stages with timing budgets —
|
|
91
|
+
[`docs/DISCOVERY_FLOW.md`](https://github.com/0xli/peer/blob/main/docs/DISCOVERY_FLOW.md)
|
|
92
|
+
- **iOS interop playbook**: real-world bug catalog and debug markers —
|
|
93
|
+
[`docs/IOS_INTEROP_PLAYBOOK.md`](https://github.com/0xli/peer/blob/main/docs/IOS_INTEROP_PLAYBOOK.md)
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
GPL-3.0-or-later. Same as upstream toxcore.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Optional command-line interface for @decentnetwork/peer.
|
|
3
|
+
//
|
|
4
|
+
// Library users do NOT need this — `import { Peer } from "@decentnetwork/peer"`
|
|
5
|
+
// stays the canonical way to embed a peer in a Node.js app. This CLI is for
|
|
6
|
+
// people who want to run a peer (one-shot or as a daemon) without writing JS.
|
|
7
|
+
//
|
|
8
|
+
// Install: npm install -g @decentnetwork/peer
|
|
9
|
+
// One-shot: npx @decentnetwork/peer <command>
|
|
10
|
+
// Subcommands:
|
|
11
|
+
// init create ~/.decent-peer/ with a default config
|
|
12
|
+
// address print this peer's address and exit
|
|
13
|
+
// userid print this peer's userid (short address form)
|
|
14
|
+
// listen run as daemon — print incoming text until Ctrl-C
|
|
15
|
+
// send <addr> <text> send one text message and exit
|
|
16
|
+
// add-friend <addr> [hello] send a friend request and exit
|
|
17
|
+
// accept <pubkey> accept a pending friend request and exit
|
|
18
|
+
// list-friends print known friends as JSON and exit
|
|
19
|
+
// help print this help
|
|
20
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { dirname, join, resolve } from "node:path";
|
|
23
|
+
import { Peer } from "./peer.js";
|
|
24
|
+
const DEFAULT_CONFIG_DIR = join(homedir(), ".decent-peer");
|
|
25
|
+
const DEFAULT_CONFIG_PATH = join(DEFAULT_CONFIG_DIR, "config.json");
|
|
26
|
+
// Conservative default set — same list as packages/peer/README.md.
|
|
27
|
+
// Users should drop more bootstraps into ~/.decent-peer/config.json for
|
|
28
|
+
// robustness.
|
|
29
|
+
const DEFAULT_BOOTSTRAPS = [
|
|
30
|
+
{
|
|
31
|
+
host: "47.100.103.201",
|
|
32
|
+
port: 33445,
|
|
33
|
+
pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd"
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
function expandHome(p) {
|
|
37
|
+
if (p.startsWith("~/") || p === "~") {
|
|
38
|
+
return join(homedir(), p.slice(2));
|
|
39
|
+
}
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
function configPath() {
|
|
43
|
+
return process.env.DECENT_PEER_CONFIG
|
|
44
|
+
? resolve(expandHome(process.env.DECENT_PEER_CONFIG))
|
|
45
|
+
: DEFAULT_CONFIG_PATH;
|
|
46
|
+
}
|
|
47
|
+
function loadConfig() {
|
|
48
|
+
const path = configPath();
|
|
49
|
+
if (!existsSync(path)) {
|
|
50
|
+
// Fall back to in-memory defaults so `address`, `listen`, etc. just work
|
|
51
|
+
// even if the user never ran `init`. The keyfile is auto-created on first
|
|
52
|
+
// start by Peer.create() when it doesn't exist.
|
|
53
|
+
return {
|
|
54
|
+
keyFile: join(DEFAULT_CONFIG_DIR, "peer.save"),
|
|
55
|
+
compatibilityMode: "legacy",
|
|
56
|
+
bootstrapNodes: DEFAULT_BOOTSTRAPS
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
60
|
+
return {
|
|
61
|
+
keyFile: expandHome(raw.keyFile ?? join(DEFAULT_CONFIG_DIR, "peer.save")),
|
|
62
|
+
compatibilityMode: raw.compatibilityMode ?? "legacy",
|
|
63
|
+
bootstrapNodes: raw.bootstrapNodes ?? DEFAULT_BOOTSTRAPS
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function ensureDirFor(path) {
|
|
67
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
async function withPeer(cfg, fn, opts = {}) {
|
|
70
|
+
ensureDirFor(cfg.keyFile);
|
|
71
|
+
const peer = await Peer.create({
|
|
72
|
+
keyFile: cfg.keyFile,
|
|
73
|
+
bootstrapNodes: cfg.bootstrapNodes,
|
|
74
|
+
compatibilityMode: cfg.compatibilityMode
|
|
75
|
+
});
|
|
76
|
+
await peer.start();
|
|
77
|
+
if (opts.join !== false) {
|
|
78
|
+
await peer.joinNetwork();
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return await fn(peer);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await peer.stop().catch(() => { });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ──────────────────────────── subcommands ────────────────────────────
|
|
88
|
+
async function cmdInit() {
|
|
89
|
+
const path = configPath();
|
|
90
|
+
if (existsSync(path)) {
|
|
91
|
+
console.log(`config already exists at ${path}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
ensureDirFor(path);
|
|
95
|
+
const cfg = {
|
|
96
|
+
keyFile: join(DEFAULT_CONFIG_DIR, "peer.save"),
|
|
97
|
+
compatibilityMode: "legacy",
|
|
98
|
+
bootstrapNodes: DEFAULT_BOOTSTRAPS
|
|
99
|
+
};
|
|
100
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
101
|
+
console.log(`wrote ${path}`);
|
|
102
|
+
console.log(`keyfile will be created at ${cfg.keyFile} on first run.`);
|
|
103
|
+
}
|
|
104
|
+
async function cmdAddress(cfg) {
|
|
105
|
+
await withPeer(cfg, async (peer) => {
|
|
106
|
+
console.log(peer.address());
|
|
107
|
+
}, { join: false });
|
|
108
|
+
}
|
|
109
|
+
async function cmdUserid(cfg) {
|
|
110
|
+
await withPeer(cfg, async (peer) => {
|
|
111
|
+
console.log(peer.userid());
|
|
112
|
+
}, { join: false });
|
|
113
|
+
}
|
|
114
|
+
async function cmdListen(cfg) {
|
|
115
|
+
// Long-running. Resolves only on SIGINT.
|
|
116
|
+
const peer = await Peer.create({
|
|
117
|
+
keyFile: cfg.keyFile,
|
|
118
|
+
bootstrapNodes: cfg.bootstrapNodes,
|
|
119
|
+
compatibilityMode: cfg.compatibilityMode
|
|
120
|
+
});
|
|
121
|
+
await peer.start();
|
|
122
|
+
console.log(`my address: ${peer.address()}`);
|
|
123
|
+
console.log(`joining network…`);
|
|
124
|
+
await peer.joinNetwork();
|
|
125
|
+
console.log(`joined. listening for messages (Ctrl-C to quit).`);
|
|
126
|
+
peer.onFriendConnection((event) => {
|
|
127
|
+
console.log(`[${new Date().toISOString()}] friend ${event.pubkey} ${event.status}`);
|
|
128
|
+
});
|
|
129
|
+
peer.onText((message) => {
|
|
130
|
+
console.log(`[${new Date().toISOString()}] ${message.pubkey}: ${message.text}`);
|
|
131
|
+
});
|
|
132
|
+
await new Promise((res) => {
|
|
133
|
+
const shutdown = async () => {
|
|
134
|
+
console.log("\nshutting down…");
|
|
135
|
+
await peer.stop().catch(() => { });
|
|
136
|
+
res();
|
|
137
|
+
};
|
|
138
|
+
process.once("SIGINT", shutdown);
|
|
139
|
+
process.once("SIGTERM", shutdown);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function cmdSend(cfg, addr, text) {
|
|
143
|
+
await withPeer(cfg, async (peer) => {
|
|
144
|
+
await peer.sendText(addr, text);
|
|
145
|
+
console.log(`sent ${text.length} chars to ${addr}`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function cmdAddFriend(cfg, addr, hello) {
|
|
149
|
+
await withPeer(cfg, async (peer) => {
|
|
150
|
+
await peer.sendFriendRequest(addr, hello);
|
|
151
|
+
console.log(`friend request sent to ${addr}`);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async function cmdAccept(cfg, pubkey) {
|
|
155
|
+
await withPeer(cfg, async (peer) => {
|
|
156
|
+
await peer.acceptFriendRequest(pubkey);
|
|
157
|
+
console.log(`accepted ${pubkey}`);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async function cmdListFriends(cfg) {
|
|
161
|
+
await withPeer(cfg, async (peer) => {
|
|
162
|
+
console.log(JSON.stringify(peer.friends(), null, 2));
|
|
163
|
+
}, { join: false });
|
|
164
|
+
}
|
|
165
|
+
function printHelp() {
|
|
166
|
+
process.stdout.write(`decent-peer — CLI for @decentnetwork/peer\n\n` +
|
|
167
|
+
`usage: decent-peer <command> [args]\n\n` +
|
|
168
|
+
`commands:\n` +
|
|
169
|
+
` init create ~/.decent-peer/config.json\n` +
|
|
170
|
+
` address print this peer's address\n` +
|
|
171
|
+
` userid print this peer's userid (short address)\n` +
|
|
172
|
+
` listen run as daemon, print incoming text\n` +
|
|
173
|
+
` send <addr> <text> send one message and exit\n` +
|
|
174
|
+
` add-friend <addr> [hello] send a friend request and exit\n` +
|
|
175
|
+
` accept <pubkey> accept a pending friend request\n` +
|
|
176
|
+
` list-friends print known friends as JSON\n` +
|
|
177
|
+
` help this help\n\n` +
|
|
178
|
+
`config: ~/.decent-peer/config.json (override with DECENT_PEER_CONFIG)\n` +
|
|
179
|
+
`keyfile: defaults to ~/.decent-peer/peer.save — KEEP THIS, it's your identity.\n\n` +
|
|
180
|
+
`for SDK usage instead, see docs/USAGE_GUIDE.md and import { Peer } from\n` +
|
|
181
|
+
`"@decentnetwork/peer".\n`);
|
|
182
|
+
}
|
|
183
|
+
// ──────────────────────────── entry ────────────────────────────
|
|
184
|
+
async function main() {
|
|
185
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
186
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
187
|
+
printHelp();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (cmd === "init") {
|
|
191
|
+
await cmdInit();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const cfg = loadConfig();
|
|
195
|
+
switch (cmd) {
|
|
196
|
+
case "address":
|
|
197
|
+
await cmdAddress(cfg);
|
|
198
|
+
return;
|
|
199
|
+
case "userid":
|
|
200
|
+
await cmdUserid(cfg);
|
|
201
|
+
return;
|
|
202
|
+
case "listen":
|
|
203
|
+
await cmdListen(cfg);
|
|
204
|
+
return;
|
|
205
|
+
case "send": {
|
|
206
|
+
const [addr, ...textParts] = rest;
|
|
207
|
+
if (!addr || textParts.length === 0) {
|
|
208
|
+
console.error("usage: decent-peer send <addr> <text>");
|
|
209
|
+
process.exit(2);
|
|
210
|
+
}
|
|
211
|
+
await cmdSend(cfg, addr, textParts.join(" "));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
case "add-friend": {
|
|
215
|
+
const [addr, ...helloParts] = rest;
|
|
216
|
+
if (!addr) {
|
|
217
|
+
console.error("usage: decent-peer add-friend <addr> [hello]");
|
|
218
|
+
process.exit(2);
|
|
219
|
+
}
|
|
220
|
+
const hello = helloParts.join(" ") || "hello";
|
|
221
|
+
await cmdAddFriend(cfg, addr, hello);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
case "accept": {
|
|
225
|
+
const [pubkey] = rest;
|
|
226
|
+
if (!pubkey) {
|
|
227
|
+
console.error("usage: decent-peer accept <pubkey>");
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
await cmdAccept(cfg, pubkey);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
case "list-friends":
|
|
234
|
+
await cmdListFriends(cfg);
|
|
235
|
+
return;
|
|
236
|
+
default:
|
|
237
|
+
console.error(`unknown command: ${cmd}`);
|
|
238
|
+
printHelp();
|
|
239
|
+
process.exit(2);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
main().catch((err) => {
|
|
243
|
+
console.error(err instanceof Error ? err.stack ?? err.message : err);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const CARRIER_PUBLIC_KEY_SIZE = 32;
|
|
2
|
+
export declare const CARRIER_NOSPAM_SIZE = 4;
|
|
3
|
+
export declare const CARRIER_ADDRESS_CHECKSUM_SIZE = 2;
|
|
4
|
+
export declare const CARRIER_ADDRESS_SIZE: number;
|
|
5
|
+
export type CarrierAddressParts = {
|
|
6
|
+
publicKey: Uint8Array;
|
|
7
|
+
nospam: number;
|
|
8
|
+
checksum: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function carrierAddressFromPublicKey(publicKey: Uint8Array, nospam?: number): string;
|
|
11
|
+
export declare function parseCarrierAddress(address: string): CarrierAddressParts;
|
|
12
|
+
export declare function carrierIdFromAddress(address: string): string;
|
|
13
|
+
export declare function carrierIdFromPublicKey(publicKey: Uint8Array): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { base58ToBytes, bytesToBase58 } from "../utils/base58.js";
|
|
2
|
+
export const CARRIER_PUBLIC_KEY_SIZE = 32;
|
|
3
|
+
export const CARRIER_NOSPAM_SIZE = 4;
|
|
4
|
+
export const CARRIER_ADDRESS_CHECKSUM_SIZE = 2;
|
|
5
|
+
export const CARRIER_ADDRESS_SIZE = CARRIER_PUBLIC_KEY_SIZE + CARRIER_NOSPAM_SIZE + CARRIER_ADDRESS_CHECKSUM_SIZE;
|
|
6
|
+
export function carrierAddressFromPublicKey(publicKey, nospam = 0) {
|
|
7
|
+
if (publicKey.length !== CARRIER_PUBLIC_KEY_SIZE) {
|
|
8
|
+
throw new Error(`Carrier public key must be ${CARRIER_PUBLIC_KEY_SIZE} bytes`);
|
|
9
|
+
}
|
|
10
|
+
const address = new Uint8Array(CARRIER_ADDRESS_SIZE);
|
|
11
|
+
address.set(publicKey, 0);
|
|
12
|
+
writeUint32LE(address, CARRIER_PUBLIC_KEY_SIZE, nospam);
|
|
13
|
+
writeUint16LE(address, CARRIER_PUBLIC_KEY_SIZE + CARRIER_NOSPAM_SIZE, addressChecksum(address.subarray(0, -2)));
|
|
14
|
+
return bytesToBase58(address);
|
|
15
|
+
}
|
|
16
|
+
export function parseCarrierAddress(address) {
|
|
17
|
+
const bytes = base58ToBytes(address);
|
|
18
|
+
if (bytes.length !== CARRIER_ADDRESS_SIZE) {
|
|
19
|
+
throw new Error(`Carrier address must decode to ${CARRIER_ADDRESS_SIZE} bytes`);
|
|
20
|
+
}
|
|
21
|
+
const actual = readUint16LE(bytes, CARRIER_PUBLIC_KEY_SIZE + CARRIER_NOSPAM_SIZE);
|
|
22
|
+
const expected = addressChecksum(bytes.subarray(0, -2));
|
|
23
|
+
if (actual !== expected) {
|
|
24
|
+
throw new Error("Carrier address checksum mismatch");
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
publicKey: bytes.slice(0, CARRIER_PUBLIC_KEY_SIZE),
|
|
28
|
+
nospam: readUint32LE(bytes, CARRIER_PUBLIC_KEY_SIZE),
|
|
29
|
+
checksum: actual
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function carrierIdFromAddress(address) {
|
|
33
|
+
return bytesToBase58(parseCarrierAddress(address).publicKey);
|
|
34
|
+
}
|
|
35
|
+
export function carrierIdFromPublicKey(publicKey) {
|
|
36
|
+
if (publicKey.length !== CARRIER_PUBLIC_KEY_SIZE) {
|
|
37
|
+
throw new Error(`Carrier public key must be ${CARRIER_PUBLIC_KEY_SIZE} bytes`);
|
|
38
|
+
}
|
|
39
|
+
return bytesToBase58(publicKey);
|
|
40
|
+
}
|
|
41
|
+
function addressChecksum(bytes) {
|
|
42
|
+
const checksum = [0, 0];
|
|
43
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
44
|
+
checksum[i % 2] ^= bytes[i];
|
|
45
|
+
}
|
|
46
|
+
return checksum[0] | (checksum[1] << 8);
|
|
47
|
+
}
|
|
48
|
+
function writeUint16LE(bytes, offset, value) {
|
|
49
|
+
bytes[offset] = value & 0xff;
|
|
50
|
+
bytes[offset + 1] = (value >>> 8) & 0xff;
|
|
51
|
+
}
|
|
52
|
+
function readUint16LE(bytes, offset) {
|
|
53
|
+
return bytes[offset] | (bytes[offset + 1] << 8);
|
|
54
|
+
}
|
|
55
|
+
function writeUint32LE(bytes, offset, value) {
|
|
56
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
|
|
57
|
+
throw new Error("Carrier nospam must be a uint32");
|
|
58
|
+
}
|
|
59
|
+
bytes[offset] = value & 0xff;
|
|
60
|
+
bytes[offset + 1] = (value >>> 8) & 0xff;
|
|
61
|
+
bytes[offset + 2] = (value >>> 16) & 0xff;
|
|
62
|
+
bytes[offset + 3] = (value >>> 24) & 0xff;
|
|
63
|
+
}
|
|
64
|
+
function readUint32LE(bytes, offset) {
|
|
65
|
+
return (bytes[offset] |
|
|
66
|
+
(bytes[offset + 1] << 8) |
|
|
67
|
+
(bytes[offset + 2] << 16) |
|
|
68
|
+
(bytes[offset + 3] << 24)) >>> 0;
|
|
69
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { NetworkNode } from "../types/peer.js";
|
|
2
|
+
import type { KeyPair } from "../crypto/keypair.js";
|
|
3
|
+
import { UdpTransport } from "../transport/udp.js";
|
|
4
|
+
type DhtNode = {
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
publicKey: Uint8Array;
|
|
8
|
+
transport: "udp4" | "udp6" | "tcp4" | "tcp6";
|
|
9
|
+
};
|
|
10
|
+
export type BootstrapClientOptions = {
|
|
11
|
+
nodes: NetworkNode[];
|
|
12
|
+
keyPair: KeyPair;
|
|
13
|
+
transport: UdpTransport;
|
|
14
|
+
};
|
|
15
|
+
export type BootstrapAttempt = {
|
|
16
|
+
node: NetworkNode;
|
|
17
|
+
status: "queued" | "sent" | "responded";
|
|
18
|
+
};
|
|
19
|
+
export type BootstrapResult = {
|
|
20
|
+
respondingNode: NetworkNode;
|
|
21
|
+
discoveredNodes: DhtNode[];
|
|
22
|
+
};
|
|
23
|
+
export declare class LegacyBootstrapClient {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(opts: BootstrapClientOptions);
|
|
26
|
+
attempts(): BootstrapAttempt[];
|
|
27
|
+
join(timeoutMs?: number): Promise<BootstrapResult>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { base58ToBytes } from "../utils/base58.js";
|
|
2
|
+
import { concatBytes, randomBytes } from "../utils/bytes.js";
|
|
3
|
+
import nacl from "tweetnacl";
|
|
4
|
+
const NET_PACKET_GET_NODES = 0x02;
|
|
5
|
+
const NET_PACKET_SEND_NODES_IPV6 = 0x04;
|
|
6
|
+
const CARRIER_MAGIC = Uint8Array.of(0x69, 0x76, 0x65, 0x67);
|
|
7
|
+
const PUBLIC_KEY_SIZE = 32;
|
|
8
|
+
const NONCE_SIZE = 24;
|
|
9
|
+
const MAC_SIZE = 16;
|
|
10
|
+
const SENDBACK_SIZE = 8;
|
|
11
|
+
const MAX_SENT_NODES = 4;
|
|
12
|
+
export class LegacyBootstrapClient {
|
|
13
|
+
#nodes;
|
|
14
|
+
#keyPair;
|
|
15
|
+
#transport;
|
|
16
|
+
constructor(opts) {
|
|
17
|
+
this.#nodes = opts.nodes;
|
|
18
|
+
this.#keyPair = opts.keyPair;
|
|
19
|
+
this.#transport = opts.transport;
|
|
20
|
+
}
|
|
21
|
+
attempts() {
|
|
22
|
+
return this.#nodes.map((node) => ({ node, status: "queued" }));
|
|
23
|
+
}
|
|
24
|
+
async join(timeoutMs = 30000) {
|
|
25
|
+
if (!this.#transport.bound) {
|
|
26
|
+
throw new Error("UDP transport must be started before bootstrap join");
|
|
27
|
+
}
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const sendbacks = new Map();
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
cleanup();
|
|
32
|
+
reject(new Error(`legacy bootstrap timed out after ${timeoutMs}ms`));
|
|
33
|
+
}, timeoutMs);
|
|
34
|
+
const cleanup = () => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
this.#transport.off("datagram", onDatagram);
|
|
37
|
+
};
|
|
38
|
+
const onDatagram = (datagram) => {
|
|
39
|
+
const parsed = this.#parseSendNodes(datagram.data, sendbacks);
|
|
40
|
+
if (!parsed) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
cleanup();
|
|
44
|
+
resolve(parsed);
|
|
45
|
+
};
|
|
46
|
+
this.#transport.on("datagram", onDatagram);
|
|
47
|
+
void this.#sendGetNodes(sendbacks).catch((error) => {
|
|
48
|
+
cleanup();
|
|
49
|
+
reject(error);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async #sendGetNodes(sendbacks) {
|
|
54
|
+
// Stagger getnodes probes so the bootstrap list priority actually matters:
|
|
55
|
+
// the first listed node gets a head start, and if it responds within the
|
|
56
|
+
// stagger window the join() promise resolves before later probes are even
|
|
57
|
+
// sent. The first stagger is intentionally larger (covers transpacific
|
|
58
|
+
// RTT to Alibaba/China bootstraps); subsequent stagger is short so a
|
|
59
|
+
// dead first node falls through quickly. Total worst-case fan-out for a
|
|
60
|
+
// 14-node list: 800 + 13*150 = ~2.7s, still well under the 30s join
|
|
61
|
+
// timeout. Operators can tune via DECENT_BOOTSTRAP_STAGGER_FIRST_MS and
|
|
62
|
+
// DECENT_BOOTSTRAP_STAGGER_MS.
|
|
63
|
+
const FIRST_STAGGER_MS = Number.parseInt(process.env.DECENT_BOOTSTRAP_STAGGER_FIRST_MS ?? "800", 10);
|
|
64
|
+
const STAGGER_MS = Number.parseInt(process.env.DECENT_BOOTSTRAP_STAGGER_MS ?? "150", 10);
|
|
65
|
+
for (let i = 0; i < this.#nodes.length; i++) {
|
|
66
|
+
const node = this.#nodes[i];
|
|
67
|
+
if (!node.pk) {
|
|
68
|
+
throw new Error(`bootstrap node ${node.host}:${node.port} is missing pk`);
|
|
69
|
+
}
|
|
70
|
+
const publicKey = base58ToBytes(node.pk);
|
|
71
|
+
if (publicKey.length !== PUBLIC_KEY_SIZE) {
|
|
72
|
+
throw new Error(`bootstrap node ${node.host}:${node.port} public key decoded to ${publicKey.length} bytes`);
|
|
73
|
+
}
|
|
74
|
+
const sendback = randomBytes(SENDBACK_SIZE);
|
|
75
|
+
sendbacks.set(Buffer.from(sendback).toString("hex"), node);
|
|
76
|
+
const packet = this.#createCarrierPacket(this.#createGetNodesPacket(publicKey, this.#keyPair.publicKey, sendback));
|
|
77
|
+
await this.#transport.send(Buffer.from(packet), node.host, node.port);
|
|
78
|
+
if (i + 1 < this.#nodes.length) {
|
|
79
|
+
const delay = i === 0 ? FIRST_STAGGER_MS : STAGGER_MS;
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
#createCarrierPacket(dhtPacket) {
|
|
85
|
+
return concatBytes([CARRIER_MAGIC, dhtPacket]);
|
|
86
|
+
}
|
|
87
|
+
#createGetNodesPacket(nodePublicKey, requestedNodeId, sendback) {
|
|
88
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
89
|
+
const plain = concatBytes([requestedNodeId, sendback]);
|
|
90
|
+
const sharedKey = nacl.box.before(nodePublicKey, this.#keyPair.secretKey);
|
|
91
|
+
const encrypted = nacl.box.after(plain, nonce, sharedKey);
|
|
92
|
+
return concatBytes([
|
|
93
|
+
Uint8Array.of(NET_PACKET_GET_NODES),
|
|
94
|
+
this.#keyPair.publicKey,
|
|
95
|
+
nonce,
|
|
96
|
+
encrypted
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
#parseSendNodes(data, sendbacks) {
|
|
100
|
+
data = this.#stripCarrierMagic(data);
|
|
101
|
+
const minimumLength = 1 + PUBLIC_KEY_SIZE + NONCE_SIZE + 1 + SENDBACK_SIZE + MAC_SIZE;
|
|
102
|
+
if (data.length < minimumLength || data[0] !== NET_PACKET_SEND_NODES_IPV6) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const remotePublicKey = data.slice(1, 1 + PUBLIC_KEY_SIZE);
|
|
106
|
+
const nonce = data.slice(1 + PUBLIC_KEY_SIZE, 1 + PUBLIC_KEY_SIZE + NONCE_SIZE);
|
|
107
|
+
const encrypted = data.slice(1 + PUBLIC_KEY_SIZE + NONCE_SIZE);
|
|
108
|
+
const sharedKey = nacl.box.before(remotePublicKey, this.#keyPair.secretKey);
|
|
109
|
+
const plain = nacl.box.open.after(encrypted, nonce, sharedKey);
|
|
110
|
+
if (!plain || plain.length < 1 + SENDBACK_SIZE) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const numNodes = plain[0];
|
|
114
|
+
if (numNodes > MAX_SENT_NODES) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const sendback = plain.slice(plain.length - SENDBACK_SIZE);
|
|
118
|
+
const respondingNode = sendbacks.get(Buffer.from(sendback).toString("hex"));
|
|
119
|
+
if (!respondingNode) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const nodeData = plain.slice(1, plain.length - SENDBACK_SIZE);
|
|
123
|
+
const discoveredNodes = this.#parseNodes(nodeData, numNodes);
|
|
124
|
+
if (discoveredNodes.length !== numNodes) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
return { respondingNode, discoveredNodes };
|
|
128
|
+
}
|
|
129
|
+
#stripCarrierMagic(data) {
|
|
130
|
+
if (data.length >= CARRIER_MAGIC.length + 1 &&
|
|
131
|
+
data[0] === CARRIER_MAGIC[0] &&
|
|
132
|
+
data[1] === CARRIER_MAGIC[1] &&
|
|
133
|
+
data[2] === CARRIER_MAGIC[2] &&
|
|
134
|
+
data[3] === CARRIER_MAGIC[3]) {
|
|
135
|
+
return data.slice(CARRIER_MAGIC.length);
|
|
136
|
+
}
|
|
137
|
+
return data;
|
|
138
|
+
}
|
|
139
|
+
#parseNodes(data, count) {
|
|
140
|
+
const nodes = [];
|
|
141
|
+
let offset = 0;
|
|
142
|
+
for (let i = 0; i < count; i++) {
|
|
143
|
+
const family = data[offset];
|
|
144
|
+
offset += 1;
|
|
145
|
+
let host;
|
|
146
|
+
let transport;
|
|
147
|
+
if (family === 2 || family === 130) {
|
|
148
|
+
if (offset + 4 + 2 + PUBLIC_KEY_SIZE > data.length) {
|
|
149
|
+
return nodes;
|
|
150
|
+
}
|
|
151
|
+
host = [...data.slice(offset, offset + 4)].join(".");
|
|
152
|
+
offset += 4;
|
|
153
|
+
transport = family === 2 ? "udp4" : "tcp4";
|
|
154
|
+
}
|
|
155
|
+
else if (family === 10 || family === 138) {
|
|
156
|
+
if (offset + 16 + 2 + PUBLIC_KEY_SIZE > data.length) {
|
|
157
|
+
return nodes;
|
|
158
|
+
}
|
|
159
|
+
const parts = [];
|
|
160
|
+
for (let part = 0; part < 8; part++) {
|
|
161
|
+
parts.push(((data[offset + part * 2] << 8) | data[offset + part * 2 + 1]).toString(16));
|
|
162
|
+
}
|
|
163
|
+
host = parts.join(":");
|
|
164
|
+
offset += 16;
|
|
165
|
+
transport = family === 10 ? "udp6" : "tcp6";
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
return nodes;
|
|
169
|
+
}
|
|
170
|
+
const port = (data[offset] << 8) | data[offset + 1];
|
|
171
|
+
offset += 2;
|
|
172
|
+
const publicKey = data.slice(offset, offset + PUBLIC_KEY_SIZE);
|
|
173
|
+
offset += PUBLIC_KEY_SIZE;
|
|
174
|
+
nodes.push({ host, port, publicKey, transport });
|
|
175
|
+
}
|
|
176
|
+
return nodes;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { LegacyProtocolNotImplementedError } from "../runtime/errors.js";
|
|
2
|
+
export class LegacyDhtClient {
|
|
3
|
+
async lookup(pubkey) {
|
|
4
|
+
if (!pubkey) {
|
|
5
|
+
throw new Error("pubkey is required");
|
|
6
|
+
}
|
|
7
|
+
throw new LegacyProtocolNotImplementedError("Legacy DHT lookup");
|
|
8
|
+
}
|
|
9
|
+
}
|