@heart-it/p2p-hello 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/README.md +50 -0
- package/index.js +98 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# p2p-hello — heartIT lab #1
|
|
2
|
+
|
|
3
|
+
Companion lab for the **P2P from Scratch** series on [heartit.tech](https://heartit.tech).
|
|
4
|
+
|
|
5
|
+
Two strangers run one command with the same passphrase. Their machines find
|
|
6
|
+
each other through the Hyperswarm DHT, hole-punch a direct UDP path, open a
|
|
7
|
+
Noise-XX-encrypted stream, and say hello. No server, no account; kill it and
|
|
8
|
+
nothing remains.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
npx @heart-it/p2p-hello swordfish
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Run it in two terminals — better, on two networks (laptop + phone hotspot),
|
|
15
|
+
because same-LAN peers connect directly without needing the punch.
|
|
16
|
+
|
|
17
|
+
## What you'll see
|
|
18
|
+
|
|
19
|
+
- the 32-byte **topic** (a salted hash of your passphrase — the DHT sees the
|
|
20
|
+
hash, never the phrase)
|
|
21
|
+
- the peer's **Noise key** (their ephemeral identity for this session)
|
|
22
|
+
- the **UDP path** — their actual `host:port`, meaning the connection was
|
|
23
|
+
hole-punched, not relayed
|
|
24
|
+
- an end-to-end **encrypted hello** (secret-stream won't give you less)
|
|
25
|
+
|
|
26
|
+
Each line maps to a part of the series: DHT announce/lookup, reflexive
|
|
27
|
+
address discovery, the punch, and the encrypted transport on top.
|
|
28
|
+
|
|
29
|
+
## Publishing checklist (maintainer)
|
|
30
|
+
|
|
31
|
+
**npm (current distribution):**
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
npm publish --access public
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Pear (when `pear install` stabilizes):** the `pear` block in package.json is
|
|
38
|
+
already staged for it. Mind two things:
|
|
39
|
+
|
|
40
|
+
1. `pear touch` mints the pear:// link once; then `pear stage pear://<key> .`
|
|
41
|
+
and `pear release pear://<key>`; keep it available with
|
|
42
|
+
`pear seed pear://<key>` on an always-on peer.
|
|
43
|
+
2. `pear stage` has **no default ignores** since v2.4 — the `pear.stage.ignore`
|
|
44
|
+
list in package.json is load-bearing. Anything staged into the drive is
|
|
45
|
+
public and content-addressed forever; verify with `pear info` before the
|
|
46
|
+
first release. (`pear run` is deprecated/removed — end users arrive via
|
|
47
|
+
`pear install` once it ships stable.)
|
|
48
|
+
|
|
49
|
+
The lab is intentionally ~80 lines. Readers should be able to hold the whole
|
|
50
|
+
thing in their head — that's the point.
|
package/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* p2p-hello — heartIT lab #1 (companion to the "P2P from Scratch" series)
|
|
4
|
+
*
|
|
5
|
+
* Two strangers run this with the same passphrase. Their machines find each
|
|
6
|
+
* other through the Hyperswarm DHT, hole-punch a direct UDP path, open an
|
|
7
|
+
* encrypted stream (Noise XX via secret-stream — you never asked for
|
|
8
|
+
* encryption; the stack refuses to give you less), and say hello.
|
|
9
|
+
*
|
|
10
|
+
* No server. No account. Kill it and nothing remains.
|
|
11
|
+
*
|
|
12
|
+
* npx @heart-it/p2p-hello <passphrase>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict'
|
|
16
|
+
|
|
17
|
+
/* Explicit require so the imports map in package.json can swap in
|
|
18
|
+
* bare-process under Bare/Pear — same file runs on both runtimes. */
|
|
19
|
+
const process = require('process')
|
|
20
|
+
const Hyperswarm = require('hyperswarm')
|
|
21
|
+
const sodium = require('sodium-universal')
|
|
22
|
+
const b4a = require('b4a')
|
|
23
|
+
|
|
24
|
+
const passphrase = process.argv[2]
|
|
25
|
+
|
|
26
|
+
if (!passphrase) {
|
|
27
|
+
console.error('usage: p2p-hello <passphrase>')
|
|
28
|
+
console.error(' run it in two terminals (or send a friend) with the same passphrase')
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* The swarm topic is a 32-byte hash of the passphrase, salted with the lab
|
|
33
|
+
* name so "swordfish" here never collides with "swordfish" in another app.
|
|
34
|
+
* Only people who know the phrase can derive the topic — the DHT sees the
|
|
35
|
+
* hash, never the phrase. */
|
|
36
|
+
const topic = b4a.alloc(32)
|
|
37
|
+
sodium.crypto_generichash(topic, b4a.from('heartit/p2p-hello::' + passphrase))
|
|
38
|
+
|
|
39
|
+
const swarm = new Hyperswarm()
|
|
40
|
+
let peers = 0
|
|
41
|
+
|
|
42
|
+
console.log('→ topic ' + b4a.toString(topic, 'hex'))
|
|
43
|
+
console.log('→ announcing on the DHT and looking for peers… (ctrl+c to leave)')
|
|
44
|
+
|
|
45
|
+
swarm.on('connection', function (conn, info) {
|
|
46
|
+
/* Every socket needs an error handler — peers vanish mid-flight and
|
|
47
|
+
* that is normal weather in a P2P system, not an exception. */
|
|
48
|
+
conn.on('error', function () {})
|
|
49
|
+
|
|
50
|
+
peers++
|
|
51
|
+
const remoteKey = b4a.toString(conn.remotePublicKey, 'hex')
|
|
52
|
+
const raw = conn.rawStream
|
|
53
|
+
|
|
54
|
+
console.log('')
|
|
55
|
+
console.log('✓ peer connected')
|
|
56
|
+
console.log(' noise key ' + remoteKey.slice(0, 16) + '… (their ephemeral identity)')
|
|
57
|
+
if (raw && raw.remoteHost) {
|
|
58
|
+
/* Private-range host = same network (direct, no punch needed);
|
|
59
|
+
* public host = the DHT-assisted hole-punch did its job. */
|
|
60
|
+
const local = /^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|127\.)/.test(raw.remoteHost)
|
|
61
|
+
console.log(' udp path ' + raw.remoteHost + ':' + raw.remotePort +
|
|
62
|
+
(local
|
|
63
|
+
? ' (same network — direct, no punch needed)'
|
|
64
|
+
: ' (hole-punched — their actual public socket, no relay)'))
|
|
65
|
+
}
|
|
66
|
+
console.log(' encryption Noise XX handshake complete — everything below is end-to-end encrypted')
|
|
67
|
+
console.log('')
|
|
68
|
+
|
|
69
|
+
conn.write('hello from ' + b4a.toString(swarm.keyPair.publicKey, 'hex').slice(0, 8) + '… 👋')
|
|
70
|
+
|
|
71
|
+
conn.on('data', function (data) {
|
|
72
|
+
console.log(' they say: ' + b4a.toString(data))
|
|
73
|
+
console.log('')
|
|
74
|
+
console.log('That was a direct, encrypted, serverless exchange between two machines')
|
|
75
|
+
console.log('that had never heard of each other. The series explains every hop.')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
conn.on('close', function () {
|
|
79
|
+
peers--
|
|
80
|
+
console.log('✗ peer left' + (peers === 0 ? ' — waiting for others…' : ''))
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/* server: true → announce ourselves; client: true → look for others.
|
|
85
|
+
* Both on, so any two copies of this lab can find each other. */
|
|
86
|
+
swarm.join(topic, { server: true, client: true })
|
|
87
|
+
|
|
88
|
+
swarm.flush().then(function () {
|
|
89
|
+
console.log('→ fully announced. If nobody appears, you are first — leave this open')
|
|
90
|
+
console.log(' and run the same command on another machine or network.')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
process.once('SIGINT', function () {
|
|
94
|
+
console.log('\n→ leaving the swarm…')
|
|
95
|
+
swarm.destroy().then(function () {
|
|
96
|
+
process.exit(0)
|
|
97
|
+
})
|
|
98
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heart-it/p2p-hello",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "heartIT lab #1 — two strangers, one passphrase, a hole-punched encrypted connection. Companion to the P2P from Scratch series.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"p2p-hello": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"b4a": "^1.6.0",
|
|
14
|
+
"bare-process": "^4.0.0",
|
|
15
|
+
"hyperswarm": "^4.17.0",
|
|
16
|
+
"sodium-universal": "^5.0.0"
|
|
17
|
+
},
|
|
18
|
+
"imports": {
|
|
19
|
+
"process": {
|
|
20
|
+
"bare": "bare-process",
|
|
21
|
+
"default": "process"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"pear": {
|
|
25
|
+
"name": "p2p-hello",
|
|
26
|
+
"stage": {
|
|
27
|
+
"entrypoints": ["./index.js"],
|
|
28
|
+
"ignore": ["/.git", "/.github", "/node_modules", "/test", "/.env", "/.DS_Store", "/README.md"]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/heart-IT/p2p-from-scratch-labs",
|
|
34
|
+
"directory": "p2p-hello"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"keywords": ["p2p", "hyperswarm", "holepunch", "hole-punching", "lab"]
|
|
38
|
+
}
|