@steve02081504/fount-p2p 0.0.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.
Files changed (154) hide show
  1. package/README.md +62 -0
  2. package/core/bytes_codec.mjs +51 -0
  3. package/core/canonical_json.mjs +21 -0
  4. package/core/composite_key.mjs +91 -0
  5. package/core/constants.mjs +53 -0
  6. package/core/entity_id.mjs +53 -0
  7. package/core/entity_id_parse.mjs +40 -0
  8. package/core/evfs_logical_path.mjs +15 -0
  9. package/core/hexIds.mjs +73 -0
  10. package/core/hlc.mjs +114 -0
  11. package/core/random_id.mjs +9 -0
  12. package/crypto/channel.mjs +101 -0
  13. package/crypto/checkpoint_sign.mjs +67 -0
  14. package/crypto/crypto.mjs +158 -0
  15. package/crypto/key.mjs +431 -0
  16. package/dag/canonicalize_row.mjs +65 -0
  17. package/dag/event_query.mjs +15 -0
  18. package/dag/index.mjs +335 -0
  19. package/dag/storage.mjs +333 -0
  20. package/dag/strip_extensions.mjs +18 -0
  21. package/dag/tunables.json +6 -0
  22. package/discovery/bt.mjs +260 -0
  23. package/discovery/index.mjs +134 -0
  24. package/discovery/mdns.mjs +138 -0
  25. package/discovery/nostr.mjs +273 -0
  26. package/entity/files/acl.mjs +38 -0
  27. package/entity/files/evfs.mjs +189 -0
  28. package/entity/files/evfs_ref.mjs +37 -0
  29. package/entity/files/manifest_acl_registry.mjs +93 -0
  30. package/entity/files/replica_host_cache.mjs +46 -0
  31. package/entity/files/url.mjs +19 -0
  32. package/entity/hosting_registry.mjs +47 -0
  33. package/entity/localized_core.mjs +141 -0
  34. package/entity/logical_entity.mjs +25 -0
  35. package/entity/logical_entity_id_registry.mjs +40 -0
  36. package/entity/node_hash.mjs +15 -0
  37. package/entity/presentation_registry.mjs +44 -0
  38. package/entity/profile.mjs +256 -0
  39. package/entity/replica.mjs +20 -0
  40. package/entity/session_snapshot_registry.mjs +38 -0
  41. package/federation/chunk_fetch_pending.mjs +128 -0
  42. package/federation/chunk_fetch_scheduler.mjs +97 -0
  43. package/federation/dag_order_cache.mjs +124 -0
  44. package/federation/dedupe_slot.mjs +33 -0
  45. package/federation/message_rate_limit.mjs +23 -0
  46. package/federation/operator_key_chain.mjs +200 -0
  47. package/federation/topo_order_memo.mjs +44 -0
  48. package/federation/volatile_streams.mjs +52 -0
  49. package/federation/want_ids.mjs +158 -0
  50. package/files/assemble.mjs +220 -0
  51. package/files/assemble_stream.mjs +129 -0
  52. package/files/chunk_fetch.mjs +113 -0
  53. package/files/chunk_fetch_verify.mjs +23 -0
  54. package/files/chunk_provider_registry.mjs +70 -0
  55. package/files/chunk_responder.mjs +79 -0
  56. package/files/chunk_store.mjs +87 -0
  57. package/files/manifest.mjs +99 -0
  58. package/files/transfer_key.mjs +87 -0
  59. package/files/transfer_key_registry.mjs +108 -0
  60. package/governance/branch.mjs +230 -0
  61. package/governance/join_pow.mjs +165 -0
  62. package/governance/owner_succession_ballot.mjs +57 -0
  63. package/governance/tunables.json +7 -0
  64. package/index.mjs +39 -0
  65. package/link/channel_mux.mjs +222 -0
  66. package/link/frame.mjs +246 -0
  67. package/link/handshake.mjs +175 -0
  68. package/link/link.mjs +617 -0
  69. package/link/rtc.mjs +109 -0
  70. package/link/sdp_fingerprint.mjs +25 -0
  71. package/mailbox/consumer_registry.mjs +71 -0
  72. package/mailbox/deliver_or_store.mjs +190 -0
  73. package/mailbox/importance.mjs +72 -0
  74. package/mailbox/parse.mjs +73 -0
  75. package/mailbox/prune.mjs +106 -0
  76. package/mailbox/rate.mjs +86 -0
  77. package/mailbox/settings.mjs +53 -0
  78. package/mailbox/store.mjs +256 -0
  79. package/mailbox/tunables.json +12 -0
  80. package/mailbox/wire.mjs +40 -0
  81. package/node/denylist.mjs +309 -0
  82. package/node/entity_store.mjs +214 -0
  83. package/node/identity.mjs +99 -0
  84. package/node/instance.mjs +115 -0
  85. package/node/network.mjs +306 -0
  86. package/node/personal_block.mjs +286 -0
  87. package/node/reputation_store.mjs +326 -0
  88. package/node/retention_policy.mjs +82 -0
  89. package/node/signaling_config.mjs +32 -0
  90. package/node/storage.mjs +30 -0
  91. package/node/storage_plugins.mjs +71 -0
  92. package/node/user_paths.mjs +10 -0
  93. package/overlay/index.mjs +191 -0
  94. package/package.json +73 -0
  95. package/permissions/bitmask.mjs +35 -0
  96. package/permissions/evaluator.mjs +90 -0
  97. package/permissions/index.mjs +12 -0
  98. package/permissions/layered.mjs +39 -0
  99. package/registries/action.mjs +94 -0
  100. package/registries/event_type.mjs +75 -0
  101. package/registries/inbound.mjs +90 -0
  102. package/registries/p2p_viewer.mjs +35 -0
  103. package/registries/part_path.mjs +35 -0
  104. package/registries/room_provider.mjs +47 -0
  105. package/reputation/engine.mjs +458 -0
  106. package/reputation/math.mjs +73 -0
  107. package/reputation/pick_score.mjs +11 -0
  108. package/reputation/relay_dedupe.mjs +22 -0
  109. package/reputation/tunables.json +36 -0
  110. package/rooms/scoped_link.mjs +185 -0
  111. package/schemas/discovery.mjs +110 -0
  112. package/schemas/federation_pull.mjs +102 -0
  113. package/schemas/mailbox.mjs +37 -0
  114. package/schemas/remote_event.mjs +17 -0
  115. package/timeline/append_core.mjs +54 -0
  116. package/timeline/materialize_runner.mjs +26 -0
  117. package/timeline/prune.mjs +39 -0
  118. package/timeline/retention.mjs +54 -0
  119. package/timeline/retention_runner.mjs +35 -0
  120. package/timeline/verify_remote.mjs +25 -0
  121. package/transport/group_link_set.mjs +324 -0
  122. package/transport/ice_servers.mjs +72 -0
  123. package/transport/link_registry.mjs +749 -0
  124. package/transport/peer_identity_maps.mjs +94 -0
  125. package/transport/peer_pool.mjs +355 -0
  126. package/transport/remote_user_room.mjs +109 -0
  127. package/transport/room_scopes.mjs +2 -0
  128. package/transport/room_wire_action.mjs +19 -0
  129. package/transport/rtc_connection_budget.mjs +180 -0
  130. package/transport/rtc_mdns_filter.mjs +118 -0
  131. package/transport/stale_peer_log.mjs +50 -0
  132. package/transport/user_room.mjs +263 -0
  133. package/trust_graph/build.mjs +99 -0
  134. package/trust_graph/cache.mjs +44 -0
  135. package/trust_graph/engine.mjs +184 -0
  136. package/trust_graph/registry.mjs +38 -0
  137. package/trust_graph/resolve.mjs +105 -0
  138. package/trust_graph/send.mjs +88 -0
  139. package/trust_graph/tunables.json +13 -0
  140. package/utils/async_mutex.mjs +69 -0
  141. package/utils/debug_log.mjs +16 -0
  142. package/utils/duration.mjs +28 -0
  143. package/utils/json_io.mjs +57 -0
  144. package/utils/lru.mjs +22 -0
  145. package/utils/map_pool.mjs +27 -0
  146. package/wire/group_part.mjs +43 -0
  147. package/wire/ingress.mjs +58 -0
  148. package/wire/part_common.mjs +55 -0
  149. package/wire/part_fanout.mjs +71 -0
  150. package/wire/part_ingress.mjs +172 -0
  151. package/wire/part_invoke.mjs +81 -0
  152. package/wire/rate_bucket.mjs +49 -0
  153. package/wire/volatile_signature.mjs +76 -0
  154. package/wire/wait.mjs +127 -0
@@ -0,0 +1,134 @@
1
+ /** @typedef {{ id: string, priority: number, caps: { canDiscover?: boolean, canSignal?: boolean, canRelay?: boolean }, advertise?: (topic: string, bytes: Uint8Array) => (() => void) | Promise<() => void>, subscribe?: (topic: string, onAdvert: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void>, sendSignal?: (topic: string, to: string, bytes: Uint8Array) => void | Promise<void>, onSignal?: (topic: string, onSignal: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void> }} DiscoveryProvider */
2
+
3
+ /** @type {Map<string, DiscoveryProvider>} */
4
+ const providers = new Map()
5
+
6
+ /**
7
+ * 注册 discovery provider。
8
+ * @param {DiscoveryProvider} provider 发现提供者
9
+ * @returns {() => void} 注销函数
10
+ */
11
+ export function registerDiscoveryProvider(provider) {
12
+ if (!provider?.id) throw new Error('p2p: discovery provider requires id')
13
+ providers.set(String(provider.id), provider)
14
+ return () => unregisterDiscoveryProvider(provider.id)
15
+ }
16
+
17
+ /**
18
+ * 注销 discovery provider。
19
+ * @param {string} id 提供者 id
20
+ * @returns {void}
21
+ */
22
+ export function unregisterDiscoveryProvider(id) {
23
+ providers.delete(String(id))
24
+ }
25
+
26
+ /**
27
+ * 列出已注册的 discovery provider(按 priority 排序)。
28
+ * @returns {DiscoveryProvider[]} 提供者列表
29
+ */
30
+ export function listDiscoveryProviders() {
31
+ return [...providers.values()].sort((left, right) => Number(left.priority || 0) - Number(right.priority || 0))
32
+ }
33
+
34
+ /**
35
+ * 通过所有可用 provider 广播 topic advert。
36
+ * @param {string} topic advert 主题
37
+ * @param {Uint8Array} bytes advert 载荷
38
+ * @returns {Promise<() => void>} 统一取消函数
39
+ */
40
+ export async function advertiseTopic(topic, bytes) {
41
+ const cleanups = []
42
+ for (const provider of listDiscoveryProviders()) {
43
+ if (!provider.caps?.canDiscover || typeof provider.advertise !== 'function') continue
44
+ let cleanup = null
45
+ try {
46
+ cleanup = await provider.advertise(topic, bytes)
47
+ }
48
+ catch (error) {
49
+ console.warn(`p2p: discovery advertise failed for ${provider.id}`, error)
50
+ continue
51
+ }
52
+ if (typeof cleanup === 'function') cleanups.push(cleanup)
53
+ }
54
+ return () => {
55
+ for (const cleanup of cleanups)
56
+ try { cleanup() } catch { /* ignore */ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 通过所有可用 provider 订阅 topic advert。
62
+ * @param {string} topic advert 主题
63
+ * @param {(bytes: Uint8Array, meta?: object) => void} onAdvert advert 回调
64
+ * @returns {Promise<() => void>} 统一取消函数
65
+ */
66
+ export async function subscribeTopic(topic, onAdvert) {
67
+ const cleanups = []
68
+ for (const provider of listDiscoveryProviders()) {
69
+ if (!provider.caps?.canDiscover || typeof provider.subscribe !== 'function') continue
70
+ let cleanup = null
71
+ try {
72
+ cleanup = await provider.subscribe(topic, onAdvert)
73
+ }
74
+ catch (error) {
75
+ console.warn(`p2p: discovery subscribe failed for ${provider.id}`, error)
76
+ continue
77
+ }
78
+ if (typeof cleanup === 'function') cleanups.push(cleanup)
79
+ }
80
+ return () => {
81
+ for (const cleanup of cleanups)
82
+ try { cleanup() } catch { /* ignore */ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 通过 discovery provider 发送信令。
88
+ * @param {string} topic 信令 topic
89
+ * @param {string} to 目标节点标识
90
+ * @param {Uint8Array} bytes 信令载荷
91
+ * @returns {Promise<void>}
92
+ */
93
+ export async function sendSignal(topic, to, bytes) {
94
+ const capable = listDiscoveryProviders().filter(provider => provider.caps?.canSignal && typeof provider.sendSignal === 'function')
95
+ if (!capable.length) throw new Error('p2p: no discovery provider can signal')
96
+ let sent = false
97
+ let lastError = null
98
+ for (const provider of capable)
99
+ try {
100
+ await Promise.resolve(provider.sendSignal(topic, to, bytes))
101
+ sent = true
102
+ }
103
+ catch (error) {
104
+ lastError = error
105
+ console.warn(`p2p: discovery signal failed for ${provider.id}`, error)
106
+ }
107
+ if (!sent) throw lastError || new Error('p2p: no discovery provider delivered signal')
108
+ }
109
+
110
+ /**
111
+ * 通过所有可用 provider 监听信令。
112
+ * @param {string} topic 信令 topic
113
+ * @param {(bytes: Uint8Array, meta?: object) => void} onSignal 信令回调
114
+ * @returns {Promise<() => void>} 统一取消函数
115
+ */
116
+ export async function listenSignals(topic, onSignal) {
117
+ const cleanups = []
118
+ for (const provider of listDiscoveryProviders()) {
119
+ if (!provider.caps?.canSignal || typeof provider.onSignal !== 'function') continue
120
+ let cleanup = null
121
+ try {
122
+ cleanup = await provider.onSignal(topic, onSignal)
123
+ }
124
+ catch (error) {
125
+ console.warn(`p2p: discovery signal listener failed for ${provider.id}`, error)
126
+ continue
127
+ }
128
+ if (typeof cleanup === 'function') cleanups.push(cleanup)
129
+ }
130
+ return () => {
131
+ for (const cleanup of cleanups)
132
+ try { cleanup() } catch { /* ignore */ }
133
+ }
134
+ }
@@ -0,0 +1,138 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import dgram from 'node:dgram'
3
+
4
+ const DEFAULT_PORT = 53531
5
+ const DEFAULT_GROUP = '239.255.42.99'
6
+
7
+ /**
8
+ * 轻量 multicast 发现插件:不做完整 DNS-SD,只复用 mDNS 的 LAN multicast 发现思路。
9
+ *
10
+ * @param {{ port?: number, group?: string }} [opts] 组播端口与组地址
11
+ * @returns {import('./index.mjs').DiscoveryProvider} mDNS 发现提供者
12
+ */
13
+ export function createMdnsDiscoveryProvider(opts = {}) {
14
+ const port = Number(opts.port) || DEFAULT_PORT
15
+ const group = String(opts.group || DEFAULT_GROUP)
16
+ const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true })
17
+ let bound = false
18
+ let bindPromise = null
19
+ /** @type {Map<string, Set<Function>>} */
20
+ const advertListeners = new Map()
21
+ /** @type {Map<string, Set<Function>>} */
22
+ const signalListeners = new Map()
23
+
24
+ /**
25
+ * 绑定 UDP socket 并注册组播消息处理器。
26
+ * @returns {Promise<void>}
27
+ */
28
+ async function ensureBound() {
29
+ if (bound) return
30
+ if (!bindPromise)
31
+ bindPromise = (async () => {
32
+ await new Promise((resolve, reject) => {
33
+ socket.once('error', reject)
34
+ socket.bind(port, '0.0.0.0', () => {
35
+ socket.off('error', reject)
36
+ socket.addMembership(group)
37
+ socket.setMulticastTTL(1)
38
+ resolve()
39
+ })
40
+ })
41
+ socket.on('message', raw => {
42
+ let packet
43
+ try { packet = JSON.parse(String(raw)) } catch { return }
44
+ const listeners = packet.type === 'advert'
45
+ ? advertListeners.get(String(packet.topic || ''))
46
+ : signalListeners.get(String(packet.topic || ''))
47
+ if (!listeners?.size) return
48
+ const bytes = Uint8Array.from(Buffer.from(String(packet.data || ''), 'base64'))
49
+ for (const listener of listeners)
50
+ listener(bytes, { provider: 'mdns' })
51
+ })
52
+ bound = true
53
+ })()
54
+ .finally(() => {
55
+ if (!bound) bindPromise = null
56
+ })
57
+ await bindPromise
58
+ }
59
+
60
+ /**
61
+ * 向组播组发送 advert 或 signal 包。
62
+ * @param {'advert' | 'signal'} type 包类型
63
+ * @param {string} topic topic 名称
64
+ * @param {Uint8Array} bytes 载荷字节
65
+ * @returns {Promise<void>}
66
+ */
67
+ async function multicast(type, topic, bytes) {
68
+ await ensureBound()
69
+ const packet = Buffer.from(JSON.stringify({
70
+ type,
71
+ topic,
72
+ data: Buffer.from(bytes).toString('base64'),
73
+ }))
74
+ await new Promise((resolve, reject) => {
75
+ socket.send(packet, port, group, error => error ? reject(error) : resolve())
76
+ })
77
+ }
78
+
79
+ /**
80
+ * 向 topic bucket 注册监听器。
81
+ * @param {Map<string, Set<Function>>} bucket topic → 监听器集合
82
+ * @param {string} topic 订阅 topic
83
+ * @param {Function} listener 回调函数
84
+ * @returns {() => void} 取消订阅函数
85
+ */
86
+ function addListener(bucket, topic, listener) {
87
+ if (!bucket.has(topic)) bucket.set(topic, new Set())
88
+ bucket.get(topic).add(listener)
89
+ return () => bucket.get(topic)?.delete(listener)
90
+ }
91
+
92
+ return {
93
+ id: 'mdns',
94
+ priority: 10,
95
+ caps: { canDiscover: true, canSignal: true, canRelay: false },
96
+ /**
97
+ * 周期性组播广播 advert。
98
+ * @param {string} topic advert 主题
99
+ * @param {Uint8Array} bytes advert 载荷
100
+ * @returns {Promise<() => void>} 取消广播函数
101
+ */
102
+ async advertise(topic, bytes) {
103
+ await multicast('advert', topic, bytes)
104
+ const timer = setInterval(() => { void multicast('advert', topic, bytes).catch(() => { }) }, 30_000)
105
+ return () => clearInterval(timer)
106
+ },
107
+ /**
108
+ * 订阅组播 advert。
109
+ * @param {string} topic advert 主题
110
+ * @param {Function} onAdvert advert 回调
111
+ * @returns {Promise<() => void>} 取消订阅函数
112
+ */
113
+ async subscribe(topic, onAdvert) {
114
+ await ensureBound()
115
+ return addListener(advertListeners, topic, onAdvert)
116
+ },
117
+ /**
118
+ * 组播发送信令(忽略单播目标)。
119
+ * @param {string} topic 信令 topic
120
+ * @param {string} _to 目标标识(未使用)
121
+ * @param {Uint8Array} bytes 信令载荷
122
+ * @returns {Promise<void>}
123
+ */
124
+ async sendSignal(topic, _to, bytes) {
125
+ await multicast('signal', topic, bytes)
126
+ },
127
+ /**
128
+ * 订阅组播信令。
129
+ * @param {string} topic 信令 topic
130
+ * @param {Function} onSignal 信令回调
131
+ * @returns {Promise<() => void>} 取消订阅函数
132
+ */
133
+ async onSignal(topic, onSignal) {
134
+ await ensureBound()
135
+ return addListener(signalListeners, topic, onSignal)
136
+ },
137
+ }
138
+ }
@@ -0,0 +1,273 @@
1
+ import { randomBytes } from 'node:crypto'
2
+
3
+ import { schnorr } from '@noble/curves/secp256k1.js'
4
+
5
+ import { sha256Hex } from '../crypto/crypto.mjs'
6
+
7
+ /** 默认 Nostr 中继 URL 列表。 */
8
+ export const DEFAULT_RELAY_URLS = [
9
+ 'wss://relay.damus.io',
10
+ 'wss://nos.lol',
11
+ 'wss://relay.nostr.band',
12
+ ]
13
+
14
+ /** Nostr advert 事件 kind。 */
15
+ export const NOSTR_ADVERT_KIND = 27235
16
+ /** Nostr signal 事件 kind。 */
17
+ export const NOSTR_SIGNAL_KIND = 27236
18
+
19
+ /**
20
+ * 合并默认与用户配置的中继 URL(去重)。
21
+ * @param {string[] | undefined | null} userRelayUrls 用户自定义中继列表
22
+ * @returns {string[]} 合并后的中继 URL 列表
23
+ */
24
+ export function mergeSignalingRelayUrls(userRelayUrls) {
25
+ const seen = new Set()
26
+ /** @type {string[]} */
27
+ const merged = []
28
+ for (const url of [...DEFAULT_RELAY_URLS, ...Array.isArray(userRelayUrls) ? userRelayUrls : []]) {
29
+ const trimmed = String(url || '').trim()
30
+ if (!trimmed || seen.has(trimmed)) continue
31
+ seen.add(trimmed)
32
+ merged.push(trimmed)
33
+ }
34
+ return merged.length ? merged : [...DEFAULT_RELAY_URLS]
35
+ }
36
+
37
+ /**
38
+ * 字节数组转十六进制字符串。
39
+ * @param {Uint8Array} bytes 输入字节
40
+ * @returns {string} 小写 hex 字符串
41
+ */
42
+ function bytesToHex(bytes) {
43
+ return [...bytes].map(byte => byte.toString(16).padStart(2, '0')).join('')
44
+ }
45
+
46
+ /**
47
+ * 十六进制字符串转字节数组。
48
+ * @param {string} hex 输入 hex 字符串
49
+ * @returns {Uint8Array} 解码后的字节
50
+ */
51
+ function hexToBytes(hex) {
52
+ const normalized = String(hex || '').trim().toLowerCase()
53
+ const out = new Uint8Array(Math.floor(normalized.length / 2))
54
+ for (let index = 0; index < out.length; index++)
55
+ out[index] = parseInt(normalized.slice(index * 2, index * 2 + 2), 16)
56
+ return out
57
+ }
58
+
59
+ /**
60
+ * 字节数组转 base64 字符串。
61
+ * @param {Uint8Array} bytes 输入字节
62
+ * @returns {string} base64 编码
63
+ */
64
+ function bytesToBase64(bytes) {
65
+ return btoa(String.fromCharCode(...bytes))
66
+ }
67
+
68
+ /**
69
+ * base64 字符串转字节数组。
70
+ * @param {string} base64 输入 base64 字符串
71
+ * @returns {Uint8Array} 解码后的字节
72
+ */
73
+ function base64ToBytes(base64) {
74
+ return Uint8Array.from(atob(base64).split('').map(ch => ch.charCodeAt(0)))
75
+ }
76
+
77
+ /**
78
+ * 签名 Nostr 事件。
79
+ * @param {number} kind 事件 kind
80
+ * @param {string[][]} tags 事件标签
81
+ * @param {string} content 事件内容
82
+ * @param {Uint8Array} secretKey Schnorr 私钥
83
+ * @returns {Promise<object>} 已签名的 Nostr 事件对象
84
+ */
85
+ async function signNostrEvent(kind, tags, content, secretKey) {
86
+ const pubkey = bytesToHex(schnorr.getPublicKey(secretKey))
87
+ const created_at = Math.floor(Date.now() / 1000)
88
+ const serialized = JSON.stringify([0, pubkey, created_at, kind, tags, content])
89
+ const id = sha256Hex(serialized)
90
+ const sig = bytesToHex(await schnorr.sign(hexToBytes(id), secretKey))
91
+ return { id, pubkey, created_at, kind, tags, content, sig }
92
+ }
93
+
94
+ /**
95
+ * 连接 Nostr 中继 WebSocket。
96
+ * @param {string} relayUrl 中继 URL
97
+ * @returns {Promise<WebSocket>} 已打开的 WebSocket
98
+ */
99
+ function connectRelay(relayUrl) {
100
+ return new Promise((resolve, reject) => {
101
+ const ws = new WebSocket(relayUrl)
102
+ const timer = setTimeout(() => {
103
+ try { ws.close() } catch { /* ignore */ }
104
+ reject(new Error(`nostr: connect timeout for ${relayUrl}`))
105
+ }, 10_000)
106
+ ws.addEventListener('open', () => {
107
+ clearTimeout(timer)
108
+ resolve(ws)
109
+ }, { once: true })
110
+ ws.addEventListener('error', () => {
111
+ clearTimeout(timer)
112
+ reject(new Error(`nostr: websocket error for ${relayUrl}`))
113
+ }, { once: true })
114
+ })
115
+ }
116
+
117
+ /**
118
+ * 向多个中继发布 Nostr 事件。
119
+ * @param {string[]} relayUrls 中继 URL 列表
120
+ * @param {object} event 待发布事件
121
+ * @returns {Promise<void>}
122
+ */
123
+ async function publishEvent(relayUrls, event) {
124
+ let published = false
125
+ let lastError = null
126
+ await Promise.allSettled(relayUrls.map(async relayUrl => {
127
+ const ws = await connectRelay(relayUrl)
128
+ try {
129
+ ws.send(JSON.stringify(['EVENT', event]))
130
+ published = true
131
+ }
132
+ catch (error) {
133
+ lastError = error
134
+ throw error
135
+ }
136
+ finally {
137
+ setTimeout(() => { try { ws.close() } catch { /* ignore */ } }, 250)
138
+ }
139
+ }))
140
+ if (!published) throw lastError || new Error('nostr: no relay accepted publish')
141
+ }
142
+
143
+ /**
144
+ * 创建 Nostr discovery provider。
145
+ * @param {{ relayUrls?: string[] }} [opts] 可选中继 URL 覆盖
146
+ * @returns {import('./index.mjs').DiscoveryProvider} Nostr 发现提供者
147
+ */
148
+ export function createNostrDiscoveryProvider(opts = {}) {
149
+ const relayUrls = mergeSignalingRelayUrls(opts.relayUrls)
150
+ const secretKey = randomBytes(32)
151
+ return {
152
+ id: 'nostr',
153
+ priority: 100,
154
+ caps: { canDiscover: true, canSignal: true, canRelay: false },
155
+ /**
156
+ * 周期性向中继发布 advert 事件。
157
+ * @param {string} topic advert 主题
158
+ * @param {Uint8Array} bytes advert 载荷
159
+ * @returns {Promise<() => void>} 取消广播函数
160
+ */
161
+ async advertise(topic, bytes) {
162
+ let closed = false
163
+ /**
164
+ * 向中继发布当前 advert。
165
+ * @returns {Promise<void>}
166
+ */
167
+ const publish = async () => {
168
+ if (closed) return
169
+ const event = await signNostrEvent(
170
+ NOSTR_ADVERT_KIND,
171
+ [['t', topic], ['x', 'advert']],
172
+ bytesToBase64(bytes),
173
+ secretKey,
174
+ )
175
+ await publishEvent(relayUrls, event)
176
+ }
177
+ await publish()
178
+ const timer = setInterval(() => { void publish().catch(() => { }) }, 5 * 60_000)
179
+ return () => {
180
+ closed = true
181
+ clearInterval(timer)
182
+ }
183
+ },
184
+ /**
185
+ * 订阅中继上的 advert 事件。
186
+ * @param {string} topic advert 主题
187
+ * @param {Function} onAdvert advert 回调
188
+ * @returns {Promise<() => void>} 取消订阅函数
189
+ */
190
+ async subscribe(topic, onAdvert) {
191
+ let closed = false
192
+ const sockets = []
193
+ const subscriptionId = randomBytes(8).toString('hex')
194
+ let connected = 0
195
+ for (const relayUrl of relayUrls) {
196
+ let ws
197
+ try { ws = await connectRelay(relayUrl) }
198
+ catch { continue }
199
+ connected++
200
+ sockets.push(ws)
201
+ ws.addEventListener('message', event => {
202
+ if (closed) return
203
+ let parsed
204
+ try { parsed = JSON.parse(String(event.data)) } catch { return }
205
+ if (!Array.isArray(parsed) || parsed[0] !== 'EVENT') return
206
+ const nostrEvent = parsed[2]
207
+ if (nostrEvent?.kind !== NOSTR_ADVERT_KIND) return
208
+ try { onAdvert(base64ToBytes(String(nostrEvent.content || '')), { relayUrl, event: nostrEvent }) }
209
+ catch { /* ignore */ }
210
+ })
211
+ 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')
214
+ return () => {
215
+ closed = true
216
+ for (const ws of sockets)
217
+ try { ws.close() } catch { /* ignore */ }
218
+ }
219
+ },
220
+ /**
221
+ * 向中继发布 signal 事件。
222
+ * @param {string} topic 信令 topic
223
+ * @param {string} to 目标节点标识
224
+ * @param {Uint8Array} bytes 信令载荷
225
+ * @returns {Promise<void>}
226
+ */
227
+ async sendSignal(topic, to, bytes) {
228
+ const event = await signNostrEvent(
229
+ NOSTR_SIGNAL_KIND,
230
+ [['t', topic], ['x', 'signal'], ['p', String(to || '')]],
231
+ bytesToBase64(bytes),
232
+ secretKey,
233
+ )
234
+ await publishEvent(relayUrls, event)
235
+ },
236
+ /**
237
+ * 订阅中继上的 signal 事件。
238
+ * @param {string} topic 信令 topic
239
+ * @param {Function} onSignal 信令回调
240
+ * @returns {Promise<() => void>} 取消订阅函数
241
+ */
242
+ async onSignal(topic, onSignal) {
243
+ let closed = false
244
+ const sockets = []
245
+ const subscriptionId = randomBytes(8).toString('hex')
246
+ let connected = 0
247
+ for (const relayUrl of relayUrls) {
248
+ let ws
249
+ try { ws = await connectRelay(relayUrl) }
250
+ catch { continue }
251
+ connected++
252
+ sockets.push(ws)
253
+ ws.addEventListener('message', event => {
254
+ if (closed) return
255
+ let parsed
256
+ try { parsed = JSON.parse(String(event.data)) } catch { return }
257
+ if (!Array.isArray(parsed) || parsed[0] !== 'EVENT') return
258
+ const nostrEvent = parsed[2]
259
+ if (nostrEvent?.kind !== NOSTR_SIGNAL_KIND) return
260
+ try { onSignal(base64ToBytes(String(nostrEvent.content || '')), { relayUrl, event: nostrEvent }) }
261
+ catch { /* ignore */ }
262
+ })
263
+ 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')
266
+ return () => {
267
+ closed = true
268
+ for (const ws of sockets)
269
+ try { ws.close() } catch { /* ignore */ }
270
+ }
271
+ },
272
+ }
273
+ }
@@ -0,0 +1,38 @@
1
+ import { isLogicalEntityHash } from '../logical_entity.mjs'
2
+ import { isWritableLocalEntity } from '../replica.mjs'
3
+
4
+ import { checkManifestAcl, resolveManifestAclType } from './manifest_acl_registry.mjs'
5
+
6
+ /**
7
+ * @param {string} replicaUsername 请求 replica
8
+ * @param {string} ownerEntityHash 文件 owner
9
+ * @param {import('../../files/manifest.mjs').FileManifest} manifest 清单
10
+ * @returns {Promise<boolean>} 是否允许读
11
+ */
12
+ export async function canReadManifest(replicaUsername, ownerEntityHash, manifest) {
13
+ const type = resolveManifestAclType(manifest, ownerEntityHash)
14
+ if (type)
15
+ return checkManifestAcl(type, { replicaUsername, ownerEntityHash, manifest })
16
+
17
+ if (isLogicalEntityHash(ownerEntityHash))
18
+ return false
19
+
20
+ return true
21
+ }
22
+
23
+ /**
24
+ * @param {string} replicaUsername 请求 replica
25
+ * @param {string} ownerEntityHash 所有者
26
+ * @param {string} logicalPath 路径
27
+ * @returns {Promise<boolean>} 是否允许写
28
+ */
29
+ export async function canWriteManifestPath(replicaUsername, ownerEntityHash, logicalPath) {
30
+ const type = resolveManifestAclType(null, ownerEntityHash)
31
+ if (type)
32
+ return checkManifestAcl(type, { replicaUsername, ownerEntityHash, manifest: /** @type {any} */ {} }, logicalPath)
33
+
34
+ if (isLogicalEntityHash(ownerEntityHash))
35
+ return false
36
+
37
+ return isWritableLocalEntity(ownerEntityHash)
38
+ }