@steve02081504/fount-p2p 0.0.10 → 0.0.11
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 +25 -7
- package/discovery/bt/probe_child.mjs +20 -0
- package/discovery/bt/runtime.mjs +58 -14
- package/discovery/index.mjs +45 -48
- package/discovery/mdns.mjs +66 -17
- package/discovery/nostr.mjs +149 -67
- package/link/providers/ble_gatt.mjs +1 -1
- package/link/providers/index.mjs +7 -9
- package/link/providers/lan_tcp.mjs +1 -1
- package/link/providers/webrtc.mjs +1 -1
- package/package.json +3 -2
- package/rooms/scoped_link.mjs +3 -3
- package/transport/group_link_set.mjs +4 -4
- package/transport/ice_servers.mjs +10 -1
- package/transport/link_registry.mjs +76 -426
- package/transport/offer_answer.mjs +190 -0
- package/transport/runtime_bootstrap.mjs +363 -0
- package/transport/signal_crypto.mjs +82 -0
package/discovery/nostr.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
2
|
|
|
3
3
|
import { schnorr } from '@noble/curves/secp256k1.js'
|
|
4
|
+
import WebSocket from 'ws'
|
|
4
5
|
|
|
5
6
|
import { sha256Hex } from '../crypto/crypto.mjs'
|
|
6
7
|
|
|
@@ -11,26 +12,69 @@ export const DEFAULT_RELAY_URLS = [
|
|
|
11
12
|
'wss://relay.nostr.band',
|
|
12
13
|
]
|
|
13
14
|
|
|
15
|
+
/** 单中继 WebSocket 首连超时(短超时 + 并行,避免串行 10s×N)。 */
|
|
16
|
+
export const NOSTR_CONNECT_TIMEOUT_MS = 2_000
|
|
17
|
+
/** 先 close,超时未 CLOSED 再 terminate(给对端优雅关闭的窗口)。 */
|
|
18
|
+
export const NOSTR_CLOSE_GRACE_MS = 1_000
|
|
19
|
+
|
|
14
20
|
/** Nostr advert 事件 kind。 */
|
|
15
21
|
export const NOSTR_ADVERT_KIND = 27235
|
|
16
22
|
/** Nostr signal 事件 kind。 */
|
|
17
23
|
export const NOSTR_SIGNAL_KIND = 27236
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* @
|
|
26
|
+
* 关掉 WebSocket:已连上则先 close,grace 内未 CLOSED 再 terminate。
|
|
27
|
+
* 连接中直接 terminate(无握手可优雅收尾)。
|
|
28
|
+
* @param {import('ws').WebSocket} ws 连接
|
|
29
|
+
* @returns {void}
|
|
23
30
|
*/
|
|
24
|
-
|
|
31
|
+
function dropWebSocket(ws) {
|
|
32
|
+
if (ws.readyState === WebSocket.CLOSED) return
|
|
33
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
34
|
+
try { ws.terminate() } catch { /* ignore */ }
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
if (ws.readyState !== WebSocket.CLOSED)
|
|
39
|
+
try { ws.terminate() } catch { /* ignore */ }
|
|
40
|
+
}, NOSTR_CLOSE_GRACE_MS)
|
|
41
|
+
// Grace is polite to the peer; must not pin the process after shutdown.
|
|
42
|
+
timer.unref()
|
|
43
|
+
ws.once('close', () => clearTimeout(timer))
|
|
44
|
+
try {
|
|
45
|
+
ws.close()
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
clearTimeout(timer)
|
|
49
|
+
try { ws.terminate() } catch { /* ignore */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 去重并清洗中继 URL 列表。
|
|
55
|
+
* @param {string[] | undefined | null} urls 原始列表
|
|
56
|
+
* @returns {string[]} 清洗后的列表
|
|
57
|
+
*/
|
|
58
|
+
function dedupeRelayUrls(urls) {
|
|
25
59
|
const seen = new Set()
|
|
26
60
|
/** @type {string[]} */
|
|
27
|
-
const
|
|
28
|
-
for (const url of
|
|
61
|
+
const out = []
|
|
62
|
+
for (const url of Array.isArray(urls) ? urls : []) {
|
|
29
63
|
const trimmed = String(url || '').trim()
|
|
30
64
|
if (!trimmed || seen.has(trimmed)) continue
|
|
31
65
|
seen.add(trimmed)
|
|
32
|
-
|
|
66
|
+
out.push(trimmed)
|
|
33
67
|
}
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 合并默认与用户配置的中继 URL(去重)。
|
|
73
|
+
* @param {string[] | undefined | null} userRelayUrls 用户自定义中继列表
|
|
74
|
+
* @returns {string[]} 合并后的中继 URL 列表
|
|
75
|
+
*/
|
|
76
|
+
export function mergeSignalingRelayUrls(userRelayUrls) {
|
|
77
|
+
const merged = dedupeRelayUrls([...DEFAULT_RELAY_URLS, ...Array.isArray(userRelayUrls) ? userRelayUrls : []])
|
|
34
78
|
return merged.length ? merged : [...DEFAULT_RELAY_URLS]
|
|
35
79
|
}
|
|
36
80
|
|
|
@@ -92,40 +136,67 @@ async function signNostrEvent(kind, tags, content, secretKey) {
|
|
|
92
136
|
}
|
|
93
137
|
|
|
94
138
|
/**
|
|
95
|
-
* 连接 Nostr 中继 WebSocket
|
|
139
|
+
* 连接 Nostr 中继 WebSocket(`ws` 包,支持 terminate)。
|
|
96
140
|
* @param {string} relayUrl 中继 URL
|
|
97
|
-
* @
|
|
141
|
+
* @param {number} [timeoutMs] 超时毫秒
|
|
142
|
+
* @param {AbortSignal} [signal] 取消信号
|
|
143
|
+
* @returns {Promise<import('ws').WebSocket>} 已打开的 WebSocket
|
|
98
144
|
*/
|
|
99
|
-
function connectRelay(relayUrl) {
|
|
145
|
+
function connectRelay(relayUrl, timeoutMs = NOSTR_CONNECT_TIMEOUT_MS, signal) {
|
|
100
146
|
return new Promise((resolve, reject) => {
|
|
147
|
+
if (signal?.aborted) {
|
|
148
|
+
reject(new Error('nostr: aborted'))
|
|
149
|
+
return
|
|
150
|
+
}
|
|
101
151
|
const ws = new WebSocket(relayUrl)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
152
|
+
let settled = false
|
|
153
|
+
/**
|
|
154
|
+
* @param {Error} error 失败原因
|
|
155
|
+
* @returns {void}
|
|
156
|
+
*/
|
|
157
|
+
const fail = error => {
|
|
158
|
+
if (settled) return
|
|
159
|
+
settled = true
|
|
107
160
|
clearTimeout(timer)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
161
|
+
signal?.removeEventListener('abort', onAbort)
|
|
162
|
+
dropWebSocket(ws)
|
|
163
|
+
reject(error)
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* @returns {void}
|
|
167
|
+
*/
|
|
168
|
+
const onAbort = () => fail(new Error('nostr: aborted'))
|
|
169
|
+
const timer = setTimeout(() => fail(new Error(`nostr: connect timeout for ${relayUrl}`)), timeoutMs)
|
|
170
|
+
timer.unref()
|
|
171
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
172
|
+
ws.once('open', () => {
|
|
173
|
+
if (settled) return
|
|
174
|
+
settled = true
|
|
111
175
|
clearTimeout(timer)
|
|
112
|
-
|
|
113
|
-
|
|
176
|
+
signal?.removeEventListener('abort', onAbort)
|
|
177
|
+
resolve(ws)
|
|
178
|
+
})
|
|
179
|
+
ws.once('error', () => {
|
|
180
|
+
fail(new Error(`nostr: websocket error for ${relayUrl}`))
|
|
181
|
+
})
|
|
114
182
|
})
|
|
115
183
|
}
|
|
116
184
|
|
|
117
185
|
/**
|
|
118
|
-
*
|
|
186
|
+
* 向多个中继并行发布 Nostr 事件(任一成功即可)。
|
|
119
187
|
* @param {string[]} relayUrls 中继 URL 列表
|
|
120
188
|
* @param {object} event 待发布事件
|
|
189
|
+
* @param {AbortSignal} [signal] 取消信号
|
|
121
190
|
* @returns {Promise<void>}
|
|
122
191
|
*/
|
|
123
|
-
async function publishEvent(relayUrls, event) {
|
|
192
|
+
async function publishEvent(relayUrls, event, signal) {
|
|
124
193
|
let published = false
|
|
125
194
|
let lastError = null
|
|
126
195
|
await Promise.allSettled(relayUrls.map(async relayUrl => {
|
|
127
|
-
|
|
196
|
+
if (signal?.aborted) throw new Error('nostr: aborted')
|
|
197
|
+
const ws = await connectRelay(relayUrl, NOSTR_CONNECT_TIMEOUT_MS, signal)
|
|
128
198
|
try {
|
|
199
|
+
if (signal?.aborted) throw new Error('nostr: aborted')
|
|
129
200
|
ws.send(JSON.stringify(['EVENT', event]))
|
|
130
201
|
published = true
|
|
131
202
|
}
|
|
@@ -134,74 +205,94 @@ async function publishEvent(relayUrls, event) {
|
|
|
134
205
|
throw error
|
|
135
206
|
}
|
|
136
207
|
finally {
|
|
137
|
-
|
|
208
|
+
dropWebSocket(ws)
|
|
138
209
|
}
|
|
139
210
|
}))
|
|
140
211
|
if (!published) throw lastError || new Error('nostr: no relay accepted publish')
|
|
141
212
|
}
|
|
142
213
|
|
|
214
|
+
/**
|
|
215
|
+
* 并行渐进连接中继:立刻返回,连上后回调;abort 后 stop 接入并 drop 已开 socket。
|
|
216
|
+
* @param {string[]} relayUrls 中继列表
|
|
217
|
+
* @param {(ws: import('ws').WebSocket, relayUrl: string) => void} onOpen 连上回调
|
|
218
|
+
* @param {AbortSignal} signal 取消信号
|
|
219
|
+
* @param {import('ws').WebSocket[]} sockets 已打开 socket 收集
|
|
220
|
+
* @returns {void}
|
|
221
|
+
*/
|
|
222
|
+
function connectRelaysProgressive(relayUrls, onOpen, signal, sockets) {
|
|
223
|
+
for (const relayUrl of relayUrls)
|
|
224
|
+
void connectRelay(relayUrl, NOSTR_CONNECT_TIMEOUT_MS, signal).then(ws => {
|
|
225
|
+
if (signal.aborted) {
|
|
226
|
+
dropWebSocket(ws)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
sockets.push(ws)
|
|
230
|
+
onOpen(ws, relayUrl)
|
|
231
|
+
}).catch(() => { /* 单路失败正常降级 */ })
|
|
232
|
+
}
|
|
233
|
+
|
|
143
234
|
/**
|
|
144
235
|
* 创建 Nostr discovery provider。
|
|
145
|
-
*
|
|
236
|
+
* subscribe/onSignal/advertise 首调用不阻塞等公网中继;连上后渐进生效。
|
|
237
|
+
* `options.relayUrls` 为最终列表(调用方 `mergeSignalingRelayUrls`,或直接传入 `relayOverride`)。
|
|
238
|
+
* 省略 / `undefined` → 默认公网中继;显式 `[]` 表示无中继(不再回填默认)。
|
|
239
|
+
* @param {{ relayUrls?: string[] | null }} [options] 可选最终中继 URL 列表
|
|
146
240
|
* @returns {import('./index.mjs').DiscoveryProvider} Nostr 发现提供者
|
|
147
241
|
*/
|
|
148
242
|
export function createNostrDiscoveryProvider(options = {}) {
|
|
149
|
-
const relayUrls =
|
|
243
|
+
const relayUrls = options.relayUrls != null
|
|
244
|
+
? dedupeRelayUrls(options.relayUrls)
|
|
245
|
+
: [...DEFAULT_RELAY_URLS]
|
|
150
246
|
const secretKey = randomBytes(32)
|
|
151
247
|
return {
|
|
152
248
|
id: 'nostr',
|
|
153
249
|
priority: 100,
|
|
154
250
|
caps: { canDiscover: true, canSignal: true, canRelay: false },
|
|
155
251
|
/**
|
|
156
|
-
* 周期性向中继发布 advert
|
|
252
|
+
* 周期性向中继发布 advert 事件(首发后台,不阻塞 ensureRuntime)。
|
|
157
253
|
* @param {string} topic advert 主题
|
|
158
254
|
* @param {Uint8Array} bytes advert 载荷
|
|
159
255
|
* @returns {Promise<() => void>} 取消广播函数
|
|
160
256
|
*/
|
|
161
257
|
async advertise(topic, bytes) {
|
|
162
|
-
|
|
258
|
+
const ac = new AbortController()
|
|
163
259
|
/**
|
|
164
260
|
* 向中继发布当前 advert。
|
|
165
261
|
* @returns {Promise<void>}
|
|
166
262
|
*/
|
|
167
263
|
const publish = async () => {
|
|
168
|
-
if (
|
|
264
|
+
if (ac.signal.aborted) return
|
|
169
265
|
const event = await signNostrEvent(
|
|
170
266
|
NOSTR_ADVERT_KIND,
|
|
171
267
|
[['t', topic], ['x', 'advert']],
|
|
172
268
|
bytesToBase64(bytes),
|
|
173
269
|
secretKey,
|
|
174
270
|
)
|
|
175
|
-
await publishEvent(relayUrls, event)
|
|
271
|
+
await publishEvent(relayUrls, event, ac.signal)
|
|
176
272
|
}
|
|
177
|
-
|
|
273
|
+
void publish().catch(() => { })
|
|
178
274
|
const timer = setInterval(() => { void publish().catch(() => { }) }, 5 * 60_000)
|
|
179
275
|
return () => {
|
|
180
|
-
|
|
276
|
+
ac.abort()
|
|
181
277
|
clearInterval(timer)
|
|
182
278
|
}
|
|
183
279
|
},
|
|
184
280
|
/**
|
|
185
|
-
* 订阅中继上的 advert
|
|
281
|
+
* 订阅中继上的 advert 事件(并行渐进连中继,立即返回 cleanup)。
|
|
186
282
|
* @param {string} topic advert 主题
|
|
187
283
|
* @param {Function} onAdvert advert 回调
|
|
188
284
|
* @returns {Promise<() => void>} 取消订阅函数
|
|
189
285
|
*/
|
|
190
286
|
async subscribe(topic, onAdvert) {
|
|
191
|
-
|
|
287
|
+
const ac = new AbortController()
|
|
288
|
+
/** @type {import('ws').WebSocket[]} */
|
|
192
289
|
const sockets = []
|
|
193
290
|
const subscriptionId = randomBytes(8).toString('hex')
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
try { ws = await connectRelay(relayUrl) }
|
|
198
|
-
catch { continue }
|
|
199
|
-
connected++
|
|
200
|
-
sockets.push(ws)
|
|
201
|
-
ws.addEventListener('message', event => {
|
|
202
|
-
if (closed) return
|
|
291
|
+
connectRelaysProgressive(relayUrls, (ws, relayUrl) => {
|
|
292
|
+
ws.on('message', data => {
|
|
293
|
+
if (ac.signal.aborted) return
|
|
203
294
|
let parsed
|
|
204
|
-
try { parsed = JSON.parse(String(
|
|
295
|
+
try { parsed = JSON.parse(String(data)) } catch { return }
|
|
205
296
|
if (!Array.isArray(parsed) || parsed[0] !== 'EVENT') return
|
|
206
297
|
const nostrEvent = parsed[2]
|
|
207
298
|
if (nostrEvent?.kind !== NOSTR_ADVERT_KIND) return
|
|
@@ -209,16 +300,14 @@ export function createNostrDiscoveryProvider(options = {}) {
|
|
|
209
300
|
catch { /* ignore */ }
|
|
210
301
|
})
|
|
211
302
|
ws.send(JSON.stringify(['REQ', subscriptionId, { kinds: [NOSTR_ADVERT_KIND], '#t': [topic], '#x': ['advert'] }]))
|
|
212
|
-
}
|
|
213
|
-
if (!connected) throw new Error('nostr: no relay available for advert subscribe')
|
|
303
|
+
}, ac.signal, sockets)
|
|
214
304
|
return () => {
|
|
215
|
-
|
|
216
|
-
for (const ws of sockets)
|
|
217
|
-
try { ws.close() } catch { /* ignore */ }
|
|
305
|
+
ac.abort()
|
|
306
|
+
for (const ws of sockets) dropWebSocket(ws)
|
|
218
307
|
}
|
|
219
308
|
},
|
|
220
309
|
/**
|
|
221
|
-
* 向中继发布 signal
|
|
310
|
+
* 向中继发布 signal 事件(按需发送,仍等待至少一路成功)。
|
|
222
311
|
* @param {string} topic 信令 topic
|
|
223
312
|
* @param {string} to 目标节点标识
|
|
224
313
|
* @param {Uint8Array} bytes 信令载荷
|
|
@@ -234,26 +323,21 @@ export function createNostrDiscoveryProvider(options = {}) {
|
|
|
234
323
|
await publishEvent(relayUrls, event)
|
|
235
324
|
},
|
|
236
325
|
/**
|
|
237
|
-
* 订阅中继上的 signal
|
|
326
|
+
* 订阅中继上的 signal 事件(并行渐进连中继,立即返回 cleanup)。
|
|
238
327
|
* @param {string} topic 信令 topic
|
|
239
328
|
* @param {Function} onSignal 信令回调
|
|
240
329
|
* @returns {Promise<() => void>} 取消订阅函数
|
|
241
330
|
*/
|
|
242
331
|
async onSignal(topic, onSignal) {
|
|
243
|
-
|
|
332
|
+
const ac = new AbortController()
|
|
333
|
+
/** @type {import('ws').WebSocket[]} */
|
|
244
334
|
const sockets = []
|
|
245
335
|
const subscriptionId = randomBytes(8).toString('hex')
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
try { ws = await connectRelay(relayUrl) }
|
|
250
|
-
catch { continue }
|
|
251
|
-
connected++
|
|
252
|
-
sockets.push(ws)
|
|
253
|
-
ws.addEventListener('message', event => {
|
|
254
|
-
if (closed) return
|
|
336
|
+
connectRelaysProgressive(relayUrls, (ws, relayUrl) => {
|
|
337
|
+
ws.on('message', data => {
|
|
338
|
+
if (ac.signal.aborted) return
|
|
255
339
|
let parsed
|
|
256
|
-
try { parsed = JSON.parse(String(
|
|
340
|
+
try { parsed = JSON.parse(String(data)) } catch { return }
|
|
257
341
|
if (!Array.isArray(parsed) || parsed[0] !== 'EVENT') return
|
|
258
342
|
const nostrEvent = parsed[2]
|
|
259
343
|
if (nostrEvent?.kind !== NOSTR_SIGNAL_KIND) return
|
|
@@ -261,12 +345,10 @@ export function createNostrDiscoveryProvider(options = {}) {
|
|
|
261
345
|
catch { /* ignore */ }
|
|
262
346
|
})
|
|
263
347
|
ws.send(JSON.stringify(['REQ', subscriptionId, { kinds: [NOSTR_SIGNAL_KIND], '#t': [topic], '#x': ['signal'] }]))
|
|
264
|
-
}
|
|
265
|
-
if (!connected) throw new Error('nostr: no relay available for signal subscribe')
|
|
348
|
+
}, ac.signal, sockets)
|
|
266
349
|
return () => {
|
|
267
|
-
|
|
268
|
-
for (const ws of sockets)
|
|
269
|
-
try { ws.close() } catch { /* ignore */ }
|
|
350
|
+
ac.abort()
|
|
351
|
+
for (const ws of sockets) dropWebSocket(ws)
|
|
270
352
|
}
|
|
271
353
|
},
|
|
272
354
|
}
|
|
@@ -290,7 +290,7 @@ export function createBleGattLinkProvider() {
|
|
|
290
290
|
return {
|
|
291
291
|
id: instanceId,
|
|
292
292
|
level: LINK_LEVEL_BLE_GATT,
|
|
293
|
-
caps: { needsOfferAnswer: false, needsDiscoverySignal: false },
|
|
293
|
+
caps: { needsOfferAnswer: false, needsDiscoverySignal: false, probe: 'native' },
|
|
294
294
|
isAvailable: canUseBleGattLink,
|
|
295
295
|
/**
|
|
296
296
|
* @param {{ nodeHash: string }} remote 远端
|
package/link/providers/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @typedef {{
|
|
16
16
|
* id: string,
|
|
17
17
|
* level: number,
|
|
18
|
-
* caps?: { needsOfferAnswer?: boolean, needsDiscoverySignal?: boolean },
|
|
18
|
+
* caps?: { needsOfferAnswer?: boolean, needsDiscoverySignal?: boolean, probe?: 'sync' | 'native' },
|
|
19
19
|
* isAvailable: () => boolean | Promise<boolean>,
|
|
20
20
|
* canReach?: (remote: { nodeHash: string, hints?: object }) => boolean | Promise<boolean>,
|
|
21
21
|
* dial: (options: object) => Promise<LinkHandle | null>,
|
|
@@ -57,7 +57,7 @@ export function listLinkProviders() {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* 清空全部 link provider
|
|
60
|
+
* 清空全部 link provider。
|
|
61
61
|
* @returns {void}
|
|
62
62
|
*/
|
|
63
63
|
export function clearLinkProviders() {
|
|
@@ -69,17 +69,15 @@ export function clearLinkProviders() {
|
|
|
69
69
|
* @returns {Promise<LinkProvider[]>} 按 level 降序的可用列表
|
|
70
70
|
*/
|
|
71
71
|
export async function listAvailableLinkProviders() {
|
|
72
|
-
const
|
|
73
|
-
for (const provider of listLinkProviders())
|
|
72
|
+
const results = await Promise.all(listLinkProviders().map(async provider => {
|
|
74
73
|
try {
|
|
75
|
-
|
|
76
|
-
available.push(provider)
|
|
74
|
+
return await Promise.resolve(provider.isAvailable()) ? provider : null
|
|
77
75
|
}
|
|
78
76
|
catch {
|
|
79
|
-
|
|
77
|
+
return null
|
|
80
78
|
}
|
|
81
|
-
|
|
82
|
-
return
|
|
79
|
+
}))
|
|
80
|
+
return results.filter(Boolean)
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
/** 重导出 level 常量。 */
|
|
@@ -283,7 +283,7 @@ export function createLanTcpLinkProvider() {
|
|
|
283
283
|
return {
|
|
284
284
|
id: instanceId,
|
|
285
285
|
level: LINK_LEVEL_LAN_TCP,
|
|
286
|
-
caps: { needsOfferAnswer: false, needsDiscoverySignal: false },
|
|
286
|
+
caps: { needsOfferAnswer: false, needsDiscoverySignal: false, probe: 'sync' },
|
|
287
287
|
/**
|
|
288
288
|
* @returns {boolean} 始终可用
|
|
289
289
|
*/
|
|
@@ -365,7 +365,7 @@ export function createWebRtcLinkProvider(options = {}) {
|
|
|
365
365
|
return {
|
|
366
366
|
id: 'webrtc',
|
|
367
367
|
level: LINK_LEVEL_WEBRTC,
|
|
368
|
-
caps: { needsOfferAnswer: true, needsDiscoverySignal: true },
|
|
368
|
+
caps: { needsOfferAnswer: true, needsDiscoverySignal: true, probe: 'native' },
|
|
369
369
|
isAvailable: canUseWebRtcLink,
|
|
370
370
|
/**
|
|
371
371
|
* @param {object} dialOptions dial 参数(含 signal / iceServers 等)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@noble/curves": "latest",
|
|
64
|
-
"node-datachannel": "latest"
|
|
64
|
+
"node-datachannel": "latest",
|
|
65
|
+
"ws": "latest"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
68
|
"@steve02081504/exec": "latest"
|
package/rooms/scoped_link.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
2
2
|
import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
|
|
3
3
|
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
4
|
+
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
4
5
|
import {
|
|
5
|
-
groupRendezvousTopic,
|
|
6
6
|
decryptSignalPacket,
|
|
7
7
|
encryptSignalPacket,
|
|
8
|
-
|
|
9
|
-
} from '../transport/
|
|
8
|
+
groupRendezvousTopic,
|
|
9
|
+
} from '../transport/signal_crypto.mjs'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 在指定 scope 与 roomSecret 下创建 link 层房间(discovery + registry 转发)。
|
|
@@ -4,13 +4,13 @@ import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
|
4
4
|
import { loadPeerPoolView } from '../node/network.mjs'
|
|
5
5
|
import { loadReputation } from '../node/reputation_store.mjs'
|
|
6
6
|
|
|
7
|
+
import { getLinkRegistry } from './link_registry.mjs'
|
|
8
|
+
import { resolveFederationPoolLimits, selectLinkTargetsFromMembers } from './peer_pool.mjs'
|
|
7
9
|
import {
|
|
8
|
-
groupRendezvousTopic,
|
|
9
10
|
decryptSignalPacket,
|
|
10
11
|
encryptSignalPacket,
|
|
11
|
-
|
|
12
|
-
} from './
|
|
13
|
-
import { resolveFederationPoolLimits, selectLinkTargetsFromMembers } from './peer_pool.mjs'
|
|
12
|
+
groupRendezvousTopic,
|
|
13
|
+
} from './signal_crypto.mjs'
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const ICE_URL_RE = /^(stun|turn|turns):/iu
|
|
6
|
-
const MAX_ICE_SERVERS =
|
|
6
|
+
const MAX_ICE_SERVERS = 12
|
|
7
7
|
|
|
8
8
|
/** @type {{ urls: string, username?: string, credential?: string }[]} */
|
|
9
9
|
export const DEFAULT_ICE_SERVERS = [
|
|
10
|
+
// Global first — ICE tries in order; regional CN servers are fallback when global STUN is blocked.
|
|
10
11
|
{ urls: 'stun:stun.l.google.com:19302' },
|
|
12
|
+
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
13
|
+
{ urls: 'stun:global.stun.twilio.com:3478' },
|
|
14
|
+
{ urls: 'stun:stun.nextcloud.com:3478' },
|
|
15
|
+
{ urls: 'stun:stun.voip.blackberry.com:3478' },
|
|
16
|
+
{ urls: 'stun:stun.freeswitch.org:3478' },
|
|
17
|
+
{ urls: 'stun:stun.chat.bilibili.com:3478' },
|
|
18
|
+
{ urls: 'stun:stun.hitv.com:3478' },
|
|
19
|
+
{ urls: 'stun:stun.miwifi.com:3478' },
|
|
11
20
|
]
|
|
12
21
|
|
|
13
22
|
/**
|