@steve02081504/fount-p2p 0.0.14 → 0.0.16
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 +14 -4
- package/crypto/channel.mjs +7 -7
- package/deno.json +4 -1
- package/discovery/adverts.mjs +1 -2
- package/discovery/bt/index.mjs +4 -1
- package/discovery/lan.mjs +12 -4
- package/discovery/nostr.mjs +307 -133
- package/discovery/peer_clue.mjs +23 -0
- package/link/handshake.mjs +3 -3
- package/link/providers/index.mjs +1 -0
- package/link/providers/levels.mjs +2 -0
- package/link/providers/nostr.mjs +280 -0
- package/link/providers/webrtc.mjs +13 -1
- package/link/rtc.mjs +14 -5
- package/package.json +1 -1
- package/transport/link_registry.mjs +37 -1
- package/transport/offer_answer.mjs +18 -4
- package/transport/runtime_bootstrap.mjs +7 -1
package/README.md
CHANGED
|
@@ -30,7 +30,9 @@ await ensureUserRoom() // slot + runtime only
|
|
|
30
30
|
attachUserRoomDefaultWires({ replicaUsername: 'alice' }) // full business wires
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Shells talk to the **fount network** (`ensureLinkToNode` / `sendToNodeLink` / rooms). Do not import `link/providers/*` or choose WebRTC / BLE / LAN yourself. Provider registration: `registerLinkProvider` from `@steve02081504/fount-p2p/link` or the facade.
|
|
33
|
+
Shells talk to the **fount network** (`ensureLinkToNode` / `sendToNodeLink` / rooms). Do not import `link/providers/*` or choose WebRTC / BLE / LAN / Nostr yourself. Provider registration: `registerLinkProvider` from `@steve02081504/fount-p2p/link` or the facade.
|
|
34
|
+
|
|
35
|
+
Dial order is descending link **`level`**: `lan_tcp` → `webrtc` → `ble_gatt` → `nostr` (−∞ last resort). Discovery **`priority`** only orders handshake / presence media. Details: [docs/transports.md](./docs/transports.md).
|
|
34
36
|
|
|
35
37
|
Public transport subpaths: `link_registry`, `user_room`, `group_link_set`, `node_scope`, `room_scopes`, `remote_user_room`. Other `transport/*` modules are internal.
|
|
36
38
|
|
|
@@ -44,6 +46,14 @@ npx @steve02081504/fount-p2p
|
|
|
44
46
|
|
|
45
47
|
Default `nodeDir`: Windows `%LOCALAPPDATA%/fount-p2p/node`; elsewhere `~/.local/share/fount-p2p/node`.
|
|
46
48
|
|
|
49
|
+
Deno (optional; WebRTC needs explicit native scripts — **not** blanket `--allow-scripts`):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
deno run -A --minimum-dependency-age=0 --node-modules-dir=auto --allow-scripts=npm:node-datachannel npm:@steve02081504/fount-p2p
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See [docs/runtime.md](./docs/runtime.md).
|
|
56
|
+
|
|
47
57
|
```javascript
|
|
48
58
|
import { initNode, startNode, startInfra, stopInfra, setInfraPriority } from '@steve02081504/fount-p2p'
|
|
49
59
|
|
|
@@ -78,7 +88,7 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
|
|
|
78
88
|
| `user_room.mjs` / `group_link_set.mjs` / `node_scope.mjs` | rooms + composable node-scope wires |
|
|
79
89
|
| `room_scopes.mjs` / `remote_user_room.mjs` | scope constants / remote user slot |
|
|
80
90
|
|
|
81
|
-
`runtime_bootstrap`, `offer_answer`, `advert_ingest` are **internal** (transport). Signal crypto / rendezvous live under `discovery/internal/signal_crypto.mjs` (used by `nostr.mjs` / `adverts.mjs`; not a package export). `ensureRuntime` returns after registration and scheduling warm-up; it does not await lan_tcp listen, public relays, or Bluetooth. `setSignalingRuntimeConfig` → `reloadDiscoveryRelays`. See [docs/runtime.md](./docs/runtime.md) and [docs/transports.md](./docs/transports.md).
|
|
91
|
+
`runtime_bootstrap`, `offer_answer`, `advert_ingest` are **internal** (transport). Signal crypto / rendezvous live under `discovery/internal/signal_crypto.mjs` (used by discovery `nostr.mjs` / `adverts.mjs`; not a package export). The Nostr **link** provider (`link/providers/nostr.mjs`) reuses the same discovery signal path (`type: 'link'`) as a last-resort duplex pipe. `ensureRuntime` returns after registration and scheduling warm-up; it does not await lan_tcp listen, public relays, or Bluetooth. `setSignalingRuntimeConfig` → `reloadDiscoveryRelays`. See [docs/runtime.md](./docs/runtime.md) and [docs/transports.md](./docs/transports.md).
|
|
82
92
|
|
|
83
93
|
Root contains only the facade and package metadata; all modules live in layered subdirectories.
|
|
84
94
|
|
|
@@ -91,7 +101,7 @@ npm run test:live # link / LAN / glare smoke
|
|
|
91
101
|
npm run test:sim # tunables co-evolution sim (dev only, not published; --social-tunables to write back)
|
|
92
102
|
```
|
|
93
103
|
|
|
94
|
-
During development: `node scripts/check-imports.mjs` validates relative imports
|
|
104
|
+
During development: `node scripts/check-imports.mjs` validates relative imports; `node scripts/find-unused-exports.mjs` scans dead exports (`--fount <path>` optional).
|
|
95
105
|
|
|
96
106
|
Maintainer notes for agents / contributors: [AGENTS.md](./AGENTS.md). Sim harness fidelity: [sim/AGENTS.md](./sim/AGENTS.md).
|
|
97
107
|
|
|
@@ -99,7 +109,7 @@ Maintainer notes for agents / contributors: [AGENTS.md](./AGENTS.md). Sim harnes
|
|
|
99
109
|
|
|
100
110
|
- `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth (optionalDependencies). Hardware probe is subprocess-only; see [docs/runtime.md](./docs/runtime.md).
|
|
101
111
|
- `node-datachannel` — WebRTC DataChannels (dependency).
|
|
102
|
-
- `ws` — Nostr discovery/signaling WebSockets (dependency).
|
|
112
|
+
- `ws` — Nostr discovery / signaling / last-resort link WebSockets (dependency).
|
|
103
113
|
|
|
104
114
|
Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStoragePlugin` and injected; see `node/storage_plugins.mjs` for the local reference implementation.
|
|
105
115
|
|
package/crypto/channel.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Domain-key 信封:X25519 ECIES 包装与 AES-GCM 消息载荷(wire scheme
|
|
2
|
+
* Domain-key 信封:X25519 ECIES 包装与 AES-GCM 消息载荷(wire scheme:channel-key)。
|
|
3
3
|
* 解密 payload 不可脱离外层 DAG Ed25519 签名上下文单独传递或信任。
|
|
4
4
|
*/
|
|
5
5
|
import { Buffer } from 'node:buffer'
|
|
@@ -7,8 +7,8 @@ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:cr
|
|
|
7
7
|
|
|
8
8
|
import { unwrapKeyEcies, wrapKeyEcies } from './key.mjs'
|
|
9
9
|
|
|
10
|
-
/** @type {'
|
|
11
|
-
export const
|
|
10
|
+
/** @type {'channel-key'} 频道消息 content 加密 scheme */
|
|
11
|
+
export const CHANNEL_KEY_SCHEME = 'channel-key'
|
|
12
12
|
|
|
13
13
|
/** @typedef {{ ephemPub: string, iv: string, ciphertext: string, authTag: string }} EciesWrapBlob */
|
|
14
14
|
|
|
@@ -49,7 +49,7 @@ function messageAesKey(channelKeyHex, channelId, generation) {
|
|
|
49
49
|
return Buffer.from(hkdfSync(
|
|
50
50
|
'sha256',
|
|
51
51
|
Buffer.from(channelKeyHex, 'hex'),
|
|
52
|
-
|
|
52
|
+
`${CHANNEL_KEY_SCHEME}:${String(channelId)}:${String(generation)}`,
|
|
53
53
|
'',
|
|
54
54
|
32,
|
|
55
55
|
))
|
|
@@ -60,7 +60,7 @@ function messageAesKey(channelKeyHex, channelId, generation) {
|
|
|
60
60
|
* @param {string} channelKeyHex K_ch
|
|
61
61
|
* @param {string} channelId 频道 ID
|
|
62
62
|
* @param {number} generation 密钥代际
|
|
63
|
-
* @returns {{ scheme: typeof
|
|
63
|
+
* @returns {{ scheme: typeof CHANNEL_KEY_SCHEME, channelId: string, generation: number, payload: string }} 频道密钥信封
|
|
64
64
|
*/
|
|
65
65
|
export function encryptWithChannelKey(plaintext, channelKeyHex, channelId, generation) {
|
|
66
66
|
const key = messageAesKey(channelKeyHex, channelId, generation)
|
|
@@ -70,7 +70,7 @@ export function encryptWithChannelKey(plaintext, channelKeyHex, channelId, gener
|
|
|
70
70
|
const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
|
|
71
71
|
const authTag = cipher.getAuthTag()
|
|
72
72
|
return {
|
|
73
|
-
scheme:
|
|
73
|
+
scheme: CHANNEL_KEY_SCHEME,
|
|
74
74
|
channelId: String(channelId),
|
|
75
75
|
generation: Number(generation) || 0,
|
|
76
76
|
payload: `${iv.toString('base64')}.${ciphertext.toString('base64')}.${authTag.toString('base64')}`,
|
|
@@ -84,7 +84,7 @@ export function encryptWithChannelKey(plaintext, channelKeyHex, channelId, gener
|
|
|
84
84
|
* @returns {string | null} 明文 UTF-8
|
|
85
85
|
*/
|
|
86
86
|
export function decryptWithChannelKey(envelope, channelKeyHex, channelId) {
|
|
87
|
-
if (envelope?.scheme !==
|
|
87
|
+
if (envelope?.scheme !== CHANNEL_KEY_SCHEME || !envelope.payload) return null
|
|
88
88
|
try {
|
|
89
89
|
const parts = envelope.payload.split('.')
|
|
90
90
|
if (parts.length !== 3) return null
|
package/deno.json
CHANGED
package/discovery/adverts.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
2
|
import { buildSignedAdvert, verifySignedAdvert } from '../link/handshake.mjs'
|
|
3
3
|
|
|
4
|
-
import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
|
|
5
|
-
|
|
6
4
|
import {
|
|
7
5
|
decryptSignalPacket,
|
|
8
6
|
encryptSignalPacket,
|
|
@@ -10,6 +8,7 @@ import {
|
|
|
10
8
|
networkRendezvousKey,
|
|
11
9
|
nodeRendezvousKey,
|
|
12
10
|
} from './internal/signal_crypto.mjs'
|
|
11
|
+
import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
|
|
13
12
|
|
|
14
13
|
/** @typedef {'node' | 'network' | { roomSecret: string }} AdvertScope */
|
|
15
14
|
|
package/discovery/bt/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { isHex64, normalizeHex64 } from '../../core/hexIds.mjs'
|
|
|
4
4
|
import { nodeDebug, shortHash } from '../../node/log.mjs'
|
|
5
5
|
import { noteAdvertPeerHints } from '../advert_peer_hints.mjs'
|
|
6
6
|
import { ingestNetworkAdvert } from '../adverts.mjs'
|
|
7
|
+
import { noteDiscoveryPeerClue } from '../peer_clue.mjs'
|
|
7
8
|
|
|
8
9
|
import { getBtPeerHint } from './peer_hints.mjs'
|
|
9
10
|
import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
|
|
@@ -106,11 +107,13 @@ export async function acceptBtScannedPresence(bytes, meta) {
|
|
|
106
107
|
const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
|
|
107
108
|
noteBtVisibleNode(ingested.verifiedNodeHash)
|
|
108
109
|
noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
|
|
109
|
-
if (firstSeen)
|
|
110
|
+
if (firstSeen) {
|
|
111
|
+
noteDiscoveryPeerClue(ingested.verifiedNodeHash)
|
|
110
112
|
nodeDebug('p2p:bt peer visible', {
|
|
111
113
|
peer: shortHash(ingested.verifiedNodeHash),
|
|
112
114
|
peripheralId,
|
|
113
115
|
})
|
|
116
|
+
}
|
|
114
117
|
return ingested
|
|
115
118
|
}
|
|
116
119
|
|
package/discovery/lan.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { noteAdvertPeerHints } from './advert_peer_hints.mjs'
|
|
|
9
9
|
import { ingestNetworkAdvert } from './adverts.mjs'
|
|
10
10
|
import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
|
|
11
11
|
import { getLanPeerHint } from './lan_peer_hints.mjs'
|
|
12
|
+
import { noteDiscoveryPeerClue } from './peer_clue.mjs'
|
|
12
13
|
|
|
13
14
|
const DEFAULT_PORT = 53531
|
|
14
15
|
const DEFAULT_GROUP = '239.255.42.99'
|
|
@@ -66,6 +67,7 @@ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
|
|
|
66
67
|
noteLanVisibleNode(ingested.verifiedNodeHash)
|
|
67
68
|
noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
|
|
68
69
|
if (firstSeen) {
|
|
70
|
+
noteDiscoveryPeerClue(ingested.verifiedNodeHash)
|
|
69
71
|
const host = String(meta.address || '').trim()
|
|
70
72
|
nodeDebug('p2p:lan peer visible', {
|
|
71
73
|
peer: shortHash(ingested.verifiedNodeHash),
|
|
@@ -78,7 +80,7 @@ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
|
|
|
78
80
|
|
|
79
81
|
/**
|
|
80
82
|
* LAN UDP presence:段内 beacon,非 topic 订阅模型。
|
|
81
|
-
* @param {{ port?: number, group?: string }} [options] 配置
|
|
83
|
+
* @param {{ port?: number, group?: string, localNodeHash?: string }} [options] 配置
|
|
82
84
|
* @returns {import('./index.mjs').DiscoveryProvider} LAN 发现提供者
|
|
83
85
|
*/
|
|
84
86
|
export function createLanDiscoveryProvider(options = {}) {
|
|
@@ -91,12 +93,14 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
91
93
|
let refs = 0
|
|
92
94
|
/** @type {ReturnType<typeof setInterval> | null} */
|
|
93
95
|
let beaconTimer = null
|
|
96
|
+
const seededSelf = normalizeHex64(options.localNodeHash)
|
|
94
97
|
/** @type {string | null} */
|
|
95
|
-
let selfNodeHash = null
|
|
98
|
+
let selfNodeHash = isHex64(seededSelf) ? seededSelf : null
|
|
96
99
|
/** @type {Set<string>} */
|
|
97
100
|
const joinedAddresses = new Set()
|
|
98
101
|
|
|
99
102
|
/**
|
|
103
|
+
* 在每个本机 IPv4 接口上加入组播组;全部失败则回退到默认接口。
|
|
100
104
|
* @param {import('node:dgram').Socket} sock UDP socket
|
|
101
105
|
* @returns {void}
|
|
102
106
|
*/
|
|
@@ -112,6 +116,11 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
112
116
|
if (!joinedAddresses.size)
|
|
113
117
|
try { sock.addMembership(group) } catch { /* ignore */ }
|
|
114
118
|
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 懒创建复用的 udp4 socket(reuseAddr,便于同机多实例绑同一 port)。
|
|
122
|
+
* @returns {import('node:dgram').Socket} 组播 presence socket
|
|
123
|
+
*/
|
|
115
124
|
function getSocket() {
|
|
116
125
|
return socket ||= dgram.createSocket({ type: 'udp4', reuseAddr: true })
|
|
117
126
|
}
|
|
@@ -190,7 +199,7 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
190
199
|
let sent = 0
|
|
191
200
|
/** @type {unknown} */
|
|
192
201
|
let lastError = null
|
|
193
|
-
for (const addr of addrs)
|
|
202
|
+
for (const addr of addrs)
|
|
194
203
|
try {
|
|
195
204
|
await sendOnce(addr)
|
|
196
205
|
sent++
|
|
@@ -198,7 +207,6 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
198
207
|
catch (error) {
|
|
199
208
|
lastError = error
|
|
200
209
|
}
|
|
201
|
-
}
|
|
202
210
|
if (!sent) throw lastError || new Error('p2p: lan multicast send failed')
|
|
203
211
|
}
|
|
204
212
|
|