@steve02081504/fount-p2p 0.0.15 → 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
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
|
|
|
@@ -86,7 +88,7 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
|
|
|
86
88
|
| `user_room.mjs` / `group_link_set.mjs` / `node_scope.mjs` | rooms + composable node-scope wires |
|
|
87
89
|
| `room_scopes.mjs` / `remote_user_room.mjs` | scope constants / remote user slot |
|
|
88
90
|
|
|
89
|
-
`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).
|
|
90
92
|
|
|
91
93
|
Root contains only the facade and package metadata; all modules live in layered subdirectories.
|
|
92
94
|
|
|
@@ -99,7 +101,7 @@ npm run test:live # link / LAN / glare smoke
|
|
|
99
101
|
npm run test:sim # tunables co-evolution sim (dev only, not published; --social-tunables to write back)
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
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).
|
|
103
105
|
|
|
104
106
|
Maintainer notes for agents / contributors: [AGENTS.md](./AGENTS.md). Sim harness fidelity: [sim/AGENTS.md](./sim/AGENTS.md).
|
|
105
107
|
|
|
@@ -107,7 +109,7 @@ Maintainer notes for agents / contributors: [AGENTS.md](./AGENTS.md). Sim harnes
|
|
|
107
109
|
|
|
108
110
|
- `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth (optionalDependencies). Hardware probe is subprocess-only; see [docs/runtime.md](./docs/runtime.md).
|
|
109
111
|
- `node-datachannel` — WebRTC DataChannels (dependency).
|
|
110
|
-
- `ws` — Nostr discovery/signaling WebSockets (dependency).
|
|
112
|
+
- `ws` — Nostr discovery / signaling / last-resort link WebSockets (dependency).
|
|
111
113
|
|
|
112
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.
|
|
113
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/link/providers/index.mjs
CHANGED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { randomBytes } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
import { base64ToBytes, bytesToBase64 } from '../../core/bytes_codec.mjs'
|
|
5
|
+
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
|
+
import { getDiscoveryProvider, sendNodeSignalPacket } from '../../discovery/index.mjs'
|
|
7
|
+
import { mergeSignalingRelayUrls } from '../../discovery/nostr.mjs'
|
|
8
|
+
import { getNodeTransportSettings } from '../../node/identity.mjs'
|
|
9
|
+
import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
|
|
10
|
+
import { ms } from '../../utils/duration.mjs'
|
|
11
|
+
import { createLruMap } from '../../utils/lru.mjs'
|
|
12
|
+
import { asLinkHandle } from '../pipe.mjs'
|
|
13
|
+
|
|
14
|
+
import { LINK_LEVEL_NOSTR } from './levels.mjs'
|
|
15
|
+
import { createLinkIdBoundPipe } from './link_id_pipe.mjs'
|
|
16
|
+
|
|
17
|
+
/** 单包 payload(UTF-8 / base64)上限,避免撞 relay content 限制。 */
|
|
18
|
+
const MAX_LINK_PAYLOAD_CHARS = 12 * 1024
|
|
19
|
+
/** open 到达前为同一 linkId 暂存的 c/b 包上限。 */
|
|
20
|
+
const PENDING_PACKETS_MAX = 32
|
|
21
|
+
/** Nostr 链握手超时(relay RTT 更慢)。 */
|
|
22
|
+
const NOSTR_HANDSHAKE_TIMEOUT_MS = ms('30s')
|
|
23
|
+
/** Nostr 链心跳间隔。 */
|
|
24
|
+
const NOSTR_HEARTBEAT_MS = ms('60s')
|
|
25
|
+
/** Nostr 链空闲超时。 */
|
|
26
|
+
const NOSTR_IDLE_TIMEOUT_MS = ms('3m')
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @returns {string[]} 当前可用中继 URL
|
|
30
|
+
*/
|
|
31
|
+
function resolveDefaultRelayUrls() {
|
|
32
|
+
return getSignalingRuntimeConfig().relayOverride
|
|
33
|
+
?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} remoteNodeHash 对端
|
|
38
|
+
* @param {object} packet link 包
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
async function publishLinkPacket(remoteNodeHash, packet) {
|
|
42
|
+
await sendNodeSignalPacket(remoteNodeHash, packet)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 创建 Nostr 末位数据链路 provider(level = -∞)。
|
|
47
|
+
* @param {{ getRelayUrls?: () => string[] }} [options] 中继解析(测试可注入)
|
|
48
|
+
* @returns {import('./index.mjs').LinkProvider & { deliverPacket: (packet: object) => void }} provider
|
|
49
|
+
*/
|
|
50
|
+
export function createNostrLinkProvider(options = {}) {
|
|
51
|
+
const resolveRelayUrls = typeof options.getRelayUrls === 'function'
|
|
52
|
+
? options.getRelayUrls
|
|
53
|
+
: resolveDefaultRelayUrls
|
|
54
|
+
|
|
55
|
+
/** @type {((link: import('./index.mjs').LinkHandle) => void) | null} */
|
|
56
|
+
let onInbound = null
|
|
57
|
+
/** @type {object | null} */
|
|
58
|
+
let localIdentity = null
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {{
|
|
61
|
+
* pipe: ReturnType<typeof createLinkIdBoundPipe>,
|
|
62
|
+
* remoteNodeHash: string,
|
|
63
|
+
* initiator: boolean,
|
|
64
|
+
* }} NostrLinkSession
|
|
65
|
+
*/
|
|
66
|
+
/** @type {Map<string, NostrLinkSession>} */
|
|
67
|
+
const sessions = new Map()
|
|
68
|
+
/** @type {Map<string, object[]> & { touch: (key: string, value: object[]) => void }} */
|
|
69
|
+
const pendingByLinkId = createLruMap(64)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @returns {boolean} discovery nostr 已注册且有中继
|
|
73
|
+
*/
|
|
74
|
+
function isAvailable() {
|
|
75
|
+
if (!getDiscoveryProvider('nostr')) return false
|
|
76
|
+
return resolveRelayUrls().length > 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {NostrLinkSession} session 会话
|
|
81
|
+
* @param {object} packet link 包
|
|
82
|
+
* @returns {void}
|
|
83
|
+
*/
|
|
84
|
+
function applySessionPacket(session, packet) {
|
|
85
|
+
const op = String(packet.op || '')
|
|
86
|
+
if (op === 'close') {
|
|
87
|
+
void session.pipe.close('remote-close')
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
if (op === 'c') {
|
|
91
|
+
if (typeof packet.payload !== 'string') return
|
|
92
|
+
session.pipe.handleInbound(packet.payload)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (op === 'b') {
|
|
96
|
+
if (typeof packet.payload !== 'string') return
|
|
97
|
+
try {
|
|
98
|
+
session.pipe.handleInbound(Buffer.from(base64ToBytes(packet.payload)))
|
|
99
|
+
}
|
|
100
|
+
catch { /* drop malformed */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} linkId 链路 id
|
|
106
|
+
* @param {object} packet 暂存包
|
|
107
|
+
* @returns {void}
|
|
108
|
+
*/
|
|
109
|
+
function bufferPending(linkId, packet) {
|
|
110
|
+
let list = pendingByLinkId.get(linkId)
|
|
111
|
+
if (!list) {
|
|
112
|
+
list = []
|
|
113
|
+
pendingByLinkId.touch(linkId, list)
|
|
114
|
+
}
|
|
115
|
+
if (list.length >= PENDING_PACKETS_MAX) list.shift()
|
|
116
|
+
list.push(packet)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} remoteNodeHash 对端
|
|
121
|
+
* @param {string} linkId 链路 id
|
|
122
|
+
* @param {string} op open|c|b|close
|
|
123
|
+
* @param {string} [payload] 可选载荷
|
|
124
|
+
* @returns {Promise<void>}
|
|
125
|
+
*/
|
|
126
|
+
async function sendOp(remoteNodeHash, linkId, op, payload) {
|
|
127
|
+
if (payload != null && payload.length > MAX_LINK_PAYLOAD_CHARS)
|
|
128
|
+
throw new Error('p2p: nostr link payload too large')
|
|
129
|
+
const packet = {
|
|
130
|
+
type: 'link',
|
|
131
|
+
op,
|
|
132
|
+
from: localIdentity?.nodeHash || '',
|
|
133
|
+
linkId,
|
|
134
|
+
}
|
|
135
|
+
if (payload != null) packet.payload = payload
|
|
136
|
+
await publishLinkPacket(remoteNodeHash, packet)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {object} opts 会话选项
|
|
141
|
+
* @returns {ReturnType<typeof createLinkIdBoundPipe>} pipe
|
|
142
|
+
*/
|
|
143
|
+
function openPipe(opts) {
|
|
144
|
+
const { linkId, remoteNodeHash, initiator } = opts
|
|
145
|
+
const pipe = createLinkIdBoundPipe({
|
|
146
|
+
providerId: 'nostr',
|
|
147
|
+
level: LINK_LEVEL_NOSTR,
|
|
148
|
+
initiator: !!initiator,
|
|
149
|
+
linkId,
|
|
150
|
+
nodeHash: remoteNodeHash,
|
|
151
|
+
localIdentity: opts.localIdentity || localIdentity,
|
|
152
|
+
handshakeTimeoutMs: NOSTR_HANDSHAKE_TIMEOUT_MS,
|
|
153
|
+
heartbeatMs: NOSTR_HEARTBEAT_MS,
|
|
154
|
+
idleTimeoutMs: NOSTR_IDLE_TIMEOUT_MS,
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} text control JSON
|
|
157
|
+
* @returns {Promise<void>}
|
|
158
|
+
*/
|
|
159
|
+
async sendControlText(text) {
|
|
160
|
+
await sendOp(remoteNodeHash, linkId, 'c', text)
|
|
161
|
+
},
|
|
162
|
+
/**
|
|
163
|
+
* @param {string} _action action
|
|
164
|
+
* @param {Uint8Array} frame 帧
|
|
165
|
+
* @returns {Promise<void>}
|
|
166
|
+
*/
|
|
167
|
+
async sendFrame(_action, frame) {
|
|
168
|
+
await sendOp(remoteNodeHash, linkId, 'b', bytesToBase64(frame))
|
|
169
|
+
},
|
|
170
|
+
/**
|
|
171
|
+
* @returns {Promise<void>}
|
|
172
|
+
*/
|
|
173
|
+
async closeTransport() {
|
|
174
|
+
sessions.delete(linkId)
|
|
175
|
+
try {
|
|
176
|
+
await sendOp(remoteNodeHash, linkId, 'close')
|
|
177
|
+
}
|
|
178
|
+
catch { /* ignore */ }
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
sessions.set(linkId, { pipe, remoteNodeHash, initiator: !!initiator })
|
|
182
|
+
pipe.onDown(() => { sessions.delete(linkId) })
|
|
183
|
+
return pipe
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 入站已解密的 link 包(由信令 demux 调用)。
|
|
188
|
+
* @param {object} packet link 包
|
|
189
|
+
* @returns {void}
|
|
190
|
+
*/
|
|
191
|
+
function deliverPacket(packet) {
|
|
192
|
+
if (packet?.type !== 'link') return
|
|
193
|
+
const linkId = normalizeHex64(packet.linkId)
|
|
194
|
+
const from = normalizeHex64(packet.from)
|
|
195
|
+
if (!linkId || !from) return
|
|
196
|
+
if (localIdentity?.nodeHash && from === localIdentity.nodeHash) return
|
|
197
|
+
|
|
198
|
+
const op = String(packet.op || '')
|
|
199
|
+
if (op === 'open') {
|
|
200
|
+
if (sessions.has(linkId)) return
|
|
201
|
+
if (!onInbound || !localIdentity) return
|
|
202
|
+
const pipe = openPipe({
|
|
203
|
+
linkId,
|
|
204
|
+
remoteNodeHash: from,
|
|
205
|
+
initiator: false,
|
|
206
|
+
localIdentity,
|
|
207
|
+
})
|
|
208
|
+
onInbound(asLinkHandle(pipe))
|
|
209
|
+
void pipe.startHandshake().catch(() => {
|
|
210
|
+
sessions.delete(linkId)
|
|
211
|
+
void pipe.close('accept-failed')
|
|
212
|
+
})
|
|
213
|
+
const pending = pendingByLinkId.get(linkId) || []
|
|
214
|
+
pendingByLinkId.delete(linkId)
|
|
215
|
+
const session = sessions.get(linkId)
|
|
216
|
+
if (session)
|
|
217
|
+
for (const queued of pending)
|
|
218
|
+
applySessionPacket(session, queued)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const session = sessions.get(linkId)
|
|
223
|
+
if (!session) {
|
|
224
|
+
if (op === 'c' || op === 'b' || op === 'close')
|
|
225
|
+
bufferPending(linkId, packet)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
applySessionPacket(session, packet)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
id: 'nostr',
|
|
233
|
+
level: LINK_LEVEL_NOSTR,
|
|
234
|
+
caps: { needsOfferAnswer: false, needsDiscoverySignal: false, probe: 'sync' },
|
|
235
|
+
isAvailable,
|
|
236
|
+
/**
|
|
237
|
+
* @returns {boolean} 有中继即可
|
|
238
|
+
*/
|
|
239
|
+
canReach() {
|
|
240
|
+
return isAvailable()
|
|
241
|
+
},
|
|
242
|
+
deliverPacket,
|
|
243
|
+
/**
|
|
244
|
+
* @param {object} dialOptions dial 选项
|
|
245
|
+
* @returns {Promise<import('./index.mjs').LinkHandle | null>} link
|
|
246
|
+
*/
|
|
247
|
+
async dial(dialOptions) {
|
|
248
|
+
if (!isAvailable()) return null
|
|
249
|
+
const remoteNodeHash = normalizeHex64(dialOptions.nodeHash)
|
|
250
|
+
if (!remoteNodeHash) return null
|
|
251
|
+
localIdentity = dialOptions.localIdentity || localIdentity
|
|
252
|
+
if (!localIdentity?.nodeHash) throw new Error('p2p: nostr dial requires localIdentity')
|
|
253
|
+
const linkId = randomBytes(32).toString('hex')
|
|
254
|
+
const pipe = openPipe({
|
|
255
|
+
linkId,
|
|
256
|
+
remoteNodeHash,
|
|
257
|
+
initiator: true,
|
|
258
|
+
localIdentity,
|
|
259
|
+
})
|
|
260
|
+
await sendOp(remoteNodeHash, linkId, 'open')
|
|
261
|
+
await pipe.startHandshake()
|
|
262
|
+
return asLinkHandle(pipe)
|
|
263
|
+
},
|
|
264
|
+
/**
|
|
265
|
+
* @param {{ onInbound: (link: import('./index.mjs').LinkHandle) => void, localIdentity: object }} handlers 回调
|
|
266
|
+
* @returns {() => void} 停止 listening
|
|
267
|
+
*/
|
|
268
|
+
ensureListening(handlers) {
|
|
269
|
+
onInbound = handlers.onInbound
|
|
270
|
+
localIdentity = handlers.localIdentity
|
|
271
|
+
return () => {
|
|
272
|
+
onInbound = null
|
|
273
|
+
for (const session of sessions.values())
|
|
274
|
+
void session.pipe.close('listen-stop')
|
|
275
|
+
sessions.clear()
|
|
276
|
+
pendingByLinkId.clear()
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
}
|
package/package.json
CHANGED
|
@@ -150,11 +150,10 @@ export function createOfferAnswerDial(deps) {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
|
-
* @param {
|
|
153
|
+
* @param {object} packet 已解密的 signal 包
|
|
154
154
|
* @returns {Promise<void>}
|
|
155
155
|
*/
|
|
156
|
-
async function
|
|
157
|
-
const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
|
|
156
|
+
async function handleSignalPacket(packet) {
|
|
158
157
|
if (packet?.type !== 'signal') return
|
|
159
158
|
const remoteNodeHash = normalizeHex64(packet.from)
|
|
160
159
|
const connId = String(packet.connId || '')
|
|
@@ -175,6 +174,21 @@ export function createOfferAnswerDial(deps) {
|
|
|
175
174
|
session.deliver(packet.body)
|
|
176
175
|
}
|
|
177
176
|
|
|
177
|
+
/**
|
|
178
|
+
* @param {Uint8Array} bytes 加密信令
|
|
179
|
+
* @returns {Promise<void>}
|
|
180
|
+
*/
|
|
181
|
+
async function handleIncomingSignal(bytes) {
|
|
182
|
+
const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
|
|
183
|
+
if (!packet) return
|
|
184
|
+
if (packet.type === 'link') {
|
|
185
|
+
const provider = listLinkProviders().find(entry => entry.id === 'nostr')
|
|
186
|
+
provider?.deliverPacket?.(packet)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
await handleSignalPacket(packet)
|
|
190
|
+
}
|
|
191
|
+
|
|
178
192
|
/**
|
|
179
193
|
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
180
194
|
* @param {string} remoteNodeHash 远端 nodeHash
|
|
@@ -187,5 +201,5 @@ export function createOfferAnswerDial(deps) {
|
|
|
187
201
|
return await buildConnLink({ provider, remoteNodeHash, connId, session, initiator: true })
|
|
188
202
|
}
|
|
189
203
|
|
|
190
|
-
return { handleIncomingSignal, dialOfferAnswer }
|
|
204
|
+
return { handleIncomingSignal, handleSignalPacket, dialOfferAnswer }
|
|
191
205
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
unregisterLinkProvider,
|
|
19
19
|
} from '../link/providers/index.mjs'
|
|
20
20
|
import { createLanTcpLinkProvider } from '../link/providers/lan_tcp.mjs'
|
|
21
|
+
import { createNostrLinkProvider } from '../link/providers/nostr.mjs'
|
|
21
22
|
import { createWebRtcLinkProvider } from '../link/providers/webrtc.mjs'
|
|
22
23
|
import { getNodeTransportSettings } from '../node/identity.mjs'
|
|
23
24
|
import { getSignalingRuntimeConfig, onNodeChange } from '../node/instance.mjs'
|
|
@@ -157,6 +158,8 @@ export function createRuntimeBootstrap(deps) {
|
|
|
157
158
|
const ids = new Set(listLinkProviders().map(provider => provider.id))
|
|
158
159
|
if (!ids.has('webrtc'))
|
|
159
160
|
registerLinkProvider(createWebRtcLinkProvider())
|
|
161
|
+
if (!ids.has('nostr'))
|
|
162
|
+
registerLinkProvider(createNostrLinkProvider({ getRelayUrls: resolveNostrRelayUrls }))
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
/**
|