@steve02081504/fount-p2p 0.0.12 → 0.0.13
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 +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +394 -59
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/frame.mjs +9 -16
- package/link/handshake.mjs +14 -15
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
package/files/assemble.mjs
CHANGED
|
@@ -147,18 +147,18 @@ export async function encryptPlaintextToMultiPartsAsync(plaintext, ceMode = 'con
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
* @param {object}
|
|
151
|
-
* @param {string}
|
|
152
|
-
* @param {string}
|
|
153
|
-
* @param {Buffer | Uint8Array}
|
|
154
|
-
* @param {string} [
|
|
155
|
-
* @param {string} [
|
|
156
|
-
* @param {CeMode} [
|
|
157
|
-
* @param {import('./manifest.mjs').TransferKeyDescriptor} [
|
|
158
|
-
* @param {object} [
|
|
150
|
+
* @param {object} parameters 参数
|
|
151
|
+
* @param {string} parameters.ownerEntityHash 128 hex
|
|
152
|
+
* @param {string} parameters.logicalPath EVFS 路径
|
|
153
|
+
* @param {Buffer | Uint8Array} parameters.plaintext 明文
|
|
154
|
+
* @param {string} [parameters.name] 文件名
|
|
155
|
+
* @param {string} [parameters.mimeType] MIME
|
|
156
|
+
* @param {CeMode} [parameters.ceMode] 加密模式
|
|
157
|
+
* @param {import('./manifest.mjs').TransferKeyDescriptor} [parameters.transferKeyDescriptor] 传递密钥
|
|
158
|
+
* @param {object} [parameters.meta] 元数据
|
|
159
159
|
* @returns {FileManifest} manifest(未写盘)
|
|
160
160
|
*/
|
|
161
|
-
export function buildFileManifest(
|
|
161
|
+
export function buildFileManifest(parameters) {
|
|
162
162
|
const {
|
|
163
163
|
ownerEntityHash,
|
|
164
164
|
logicalPath,
|
|
@@ -168,7 +168,7 @@ export function buildFileManifest(params) {
|
|
|
168
168
|
ceMode = 'convergent',
|
|
169
169
|
transferKeyDescriptor,
|
|
170
170
|
meta,
|
|
171
|
-
} =
|
|
171
|
+
} = parameters
|
|
172
172
|
const enc = encryptPlaintextToParts(plaintext, ceMode)
|
|
173
173
|
const manifest = normalizeFileManifest({
|
|
174
174
|
ownerEntityHash: ownerEntityHash.toLowerCase(),
|
|
@@ -188,11 +188,11 @@ export function buildFileManifest(params) {
|
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* 由已加密分块构建 manifest(vault / file-master-key-wrap 等需自定义 transferKeyDescriptor)。
|
|
191
|
-
* @param {object}
|
|
191
|
+
* @param {object} parameters 与 buildFileManifest 相同字段(不含 plaintext 重加密)
|
|
192
192
|
* @param {{ contentHash: string, parts: Array<{ hash: string, size: number, raw?: Buffer, contentHash?: string }> }} enc 加密结果
|
|
193
193
|
* @returns {FileManifest} manifest
|
|
194
194
|
*/
|
|
195
|
-
export function buildFileManifestFromEnc(
|
|
195
|
+
export function buildFileManifestFromEnc(parameters, enc) {
|
|
196
196
|
const {
|
|
197
197
|
ownerEntityHash,
|
|
198
198
|
logicalPath,
|
|
@@ -202,7 +202,7 @@ export function buildFileManifestFromEnc(params, enc) {
|
|
|
202
202
|
ceMode = 'convergent',
|
|
203
203
|
transferKeyDescriptor,
|
|
204
204
|
meta,
|
|
205
|
-
} =
|
|
205
|
+
} = parameters
|
|
206
206
|
const manifest = normalizeFileManifest({
|
|
207
207
|
ownerEntityHash: ownerEntityHash.toLowerCase(),
|
|
208
208
|
logicalPath: logicalPath.replace(/^\/+/, ''),
|
|
@@ -44,11 +44,11 @@ export async function encryptReadableToParts(readable, ceMode = 'convergent', on
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
for await (const chunk of readable) {
|
|
47
|
-
const
|
|
48
|
-
total +=
|
|
47
|
+
const buffer = Buffer.from(chunk)
|
|
48
|
+
total += buffer.length
|
|
49
49
|
if (total > maxBytes)
|
|
50
50
|
throw new Error('plaintext exceeds max upload size')
|
|
51
|
-
pending = Buffer.concat([pending,
|
|
51
|
+
pending = Buffer.concat([pending, buffer])
|
|
52
52
|
while (pending.length >= FEDERATION_CHUNK_MAX_BYTES) {
|
|
53
53
|
const slice = pending.subarray(0, FEDERATION_CHUNK_MAX_BYTES)
|
|
54
54
|
pending = pending.subarray(FEDERATION_CHUNK_MAX_BYTES)
|
|
@@ -91,11 +91,11 @@ function readStreamChunk(stream) {
|
|
|
91
91
|
resolve(null)
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
|
-
* @param {Error}
|
|
94
|
+
* @param {Error} error 错误
|
|
95
95
|
*/
|
|
96
|
-
const onError =
|
|
96
|
+
const onError = error => {
|
|
97
97
|
cleanup()
|
|
98
|
-
reject(
|
|
98
|
+
reject(error)
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
101
101
|
*
|
|
@@ -44,27 +44,36 @@ export async function handleFedManifestDataIngress(data) {
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* node scope user-room wire:注册 fed_chunk_* + fed_manifest_*。
|
|
47
|
-
* @param {string}
|
|
48
|
-
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire action 表
|
|
49
|
-
* @returns {void}
|
|
47
|
+
* @param {string | (() => string)} usernameOrGetter 副本用户名或 live getter
|
|
48
|
+
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => (() => void) | void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire action 表
|
|
49
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
50
50
|
*/
|
|
51
|
-
export function attachNodeScopeFedChunkResponder(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
51
|
+
export function attachNodeScopeFedChunkResponder(usernameOrGetter, wire) {
|
|
52
|
+
const resolveUsername = typeof usernameOrGetter === 'function'
|
|
53
|
+
? usernameOrGetter
|
|
54
|
+
: () => usernameOrGetter
|
|
55
|
+
const offs = [
|
|
56
|
+
wire.on('fed_chunk_get', (data, peerId) => {
|
|
57
|
+
void handleFedChunkGetIngress(resolveUsername(), data, peerId, (resp, pid) => {
|
|
58
|
+
try { wire.send('fed_chunk_data', resp, pid) }
|
|
59
|
+
catch { /* disconnected */ }
|
|
60
|
+
})
|
|
61
|
+
}),
|
|
62
|
+
wire.on('fed_chunk_data', handleFedChunkDataIngress),
|
|
63
|
+
wire.on('fed_manifest_get', (data, peerId) => {
|
|
64
|
+
void handleFedManifestGetIngress(resolveUsername(), data, peerId, (resp, pid) => {
|
|
65
|
+
try { wire.send('fed_manifest_data', resp, pid) }
|
|
66
|
+
catch { /* disconnected */ }
|
|
67
|
+
})
|
|
68
|
+
}),
|
|
69
|
+
wire.on('fed_manifest_data', data => {
|
|
70
|
+
void handleFedManifestDataIngress(data)
|
|
71
|
+
}),
|
|
72
|
+
]
|
|
73
|
+
return () => {
|
|
74
|
+
for (const off of offs)
|
|
75
|
+
try { off?.() } catch { /* ignore */ }
|
|
76
|
+
}
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
/**
|
package/files/evfs.mjs
CHANGED
|
@@ -128,18 +128,18 @@ export async function readManifestPlaintextStream(replicaUsername, manifest, opt
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* @param {object}
|
|
132
|
-
* @param {string}
|
|
133
|
-
* @param {string}
|
|
134
|
-
* @param {Buffer | Uint8Array}
|
|
135
|
-
* @param {string} [
|
|
136
|
-
* @param {string} [
|
|
137
|
-
* @param {import('./manifest.mjs').CeMode} [
|
|
138
|
-
* @param {import('./manifest.mjs').TransferKeyDescriptor} [
|
|
139
|
-
* @param {object} [
|
|
131
|
+
* @param {object} parameters 参数
|
|
132
|
+
* @param {string} parameters.ownerEntityHash owner
|
|
133
|
+
* @param {string} parameters.logicalPath 路径
|
|
134
|
+
* @param {Buffer | Uint8Array} parameters.plaintext 明文
|
|
135
|
+
* @param {string} [parameters.name] 文件名
|
|
136
|
+
* @param {string} [parameters.mimeType] MIME
|
|
137
|
+
* @param {import('./manifest.mjs').CeMode} [parameters.ceMode] 模式
|
|
138
|
+
* @param {import('./manifest.mjs').TransferKeyDescriptor} [parameters.transferKeyDescriptor] 传递密钥
|
|
139
|
+
* @param {object} [parameters.meta] meta
|
|
140
140
|
* @returns {Promise<import('./manifest.mjs').FileManifest>} 写入后的 manifest
|
|
141
141
|
*/
|
|
142
|
-
export async function putFileManifest(
|
|
142
|
+
export async function putFileManifest(parameters) {
|
|
143
143
|
const {
|
|
144
144
|
ownerEntityHash,
|
|
145
145
|
logicalPath,
|
|
@@ -149,15 +149,15 @@ export async function putFileManifest(params) {
|
|
|
149
149
|
ceMode = 'convergent',
|
|
150
150
|
transferKeyDescriptor,
|
|
151
151
|
meta,
|
|
152
|
-
} =
|
|
153
|
-
const
|
|
154
|
-
const enc =
|
|
155
|
-
? await encryptPlaintextToMultiPartsAsync(
|
|
156
|
-
: encryptPlaintextToParts(
|
|
152
|
+
} = parameters
|
|
153
|
+
const plaintextBuffer = Buffer.from(plaintext)
|
|
154
|
+
const enc = plaintextBuffer.length > FEDERATION_CHUNK_MAX_BYTES
|
|
155
|
+
? await encryptPlaintextToMultiPartsAsync(plaintextBuffer, ceMode)
|
|
156
|
+
: encryptPlaintextToParts(plaintextBuffer, ceMode)
|
|
157
157
|
const manifest = buildFileManifestFromEnc({
|
|
158
158
|
ownerEntityHash,
|
|
159
159
|
logicalPath,
|
|
160
|
-
plaintext:
|
|
160
|
+
plaintext: plaintextBuffer,
|
|
161
161
|
name,
|
|
162
162
|
mimeType,
|
|
163
163
|
ceMode,
|
|
@@ -171,19 +171,19 @@ export async function putFileManifest(params) {
|
|
|
171
171
|
|
|
172
172
|
/**
|
|
173
173
|
* 流式写入文件(请求流 -> 加密分块 -> chunk store)。
|
|
174
|
-
* @param {object}
|
|
175
|
-
* @param {string}
|
|
176
|
-
* @param {string}
|
|
177
|
-
* @param {import('node:stream').Readable}
|
|
178
|
-
* @param {number}
|
|
179
|
-
* @param {string} [
|
|
180
|
-
* @param {string} [
|
|
181
|
-
* @param {import('./manifest.mjs').CeMode} [
|
|
182
|
-
* @param {import('./manifest.mjs').TransferKeyDescriptor} [
|
|
183
|
-
* @param {object} [
|
|
174
|
+
* @param {object} parameters 参数
|
|
175
|
+
* @param {string} parameters.ownerEntityHash owner
|
|
176
|
+
* @param {string} parameters.logicalPath 路径
|
|
177
|
+
* @param {import('node:stream').Readable} parameters.readable 明文流
|
|
178
|
+
* @param {number} parameters.plainSize 明文字节数
|
|
179
|
+
* @param {string} [parameters.name] 文件名
|
|
180
|
+
* @param {string} [parameters.mimeType] MIME
|
|
181
|
+
* @param {import('./manifest.mjs').CeMode} [parameters.ceMode] 模式
|
|
182
|
+
* @param {import('./manifest.mjs').TransferKeyDescriptor} [parameters.transferKeyDescriptor] 传递密钥
|
|
183
|
+
* @param {object} [parameters.meta] meta
|
|
184
184
|
* @returns {Promise<import('./manifest.mjs').FileManifest>} 写入后的 manifest
|
|
185
185
|
*/
|
|
186
|
-
export async function putFileManifestFromStream(
|
|
186
|
+
export async function putFileManifestFromStream(parameters) {
|
|
187
187
|
const {
|
|
188
188
|
ownerEntityHash,
|
|
189
189
|
logicalPath,
|
|
@@ -194,7 +194,7 @@ export async function putFileManifestFromStream(params) {
|
|
|
194
194
|
ceMode = 'convergent',
|
|
195
195
|
transferKeyDescriptor,
|
|
196
196
|
meta,
|
|
197
|
-
} =
|
|
197
|
+
} = parameters
|
|
198
198
|
const enc = await encryptReadableToParts(readable, ceMode, async part =>
|
|
199
199
|
putChunk(part.hash, part.raw), plainSize)
|
|
200
200
|
const manifest = normalizeFileManifest({
|
|
@@ -228,6 +228,7 @@ export async function readPublicFile(replicaUsername, entityHash, logicalPath, o
|
|
|
228
228
|
username: options.username || replicaUsername,
|
|
229
229
|
ownerEntityHash: entityHash,
|
|
230
230
|
logicalPath,
|
|
231
|
+
cache: true,
|
|
231
232
|
})
|
|
232
233
|
if (!manifest) return null
|
|
233
234
|
return readManifestPlaintext(replicaUsername, manifest, options)
|
package/files/manifest_fetch.mjs
CHANGED
|
@@ -16,8 +16,9 @@ import { normalizeFileManifest } from './manifest.mjs'
|
|
|
16
16
|
import { shouldPreferIncomingPublicManifest } from './public_manifest.mjs'
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
19
|
+
* 拉取公开 manifest;默认不写盘。`cache: true` 或 `cachePublicManifest` 才缓存。
|
|
20
|
+
* @param {{ username: string, ownerEntityHash: string, logicalPath: string, cache?: boolean }} context - 拉取上下文
|
|
21
|
+
* @returns {Promise<import('./manifest.mjs').FileManifest | null>} 验签后的 manifest,失败为 null
|
|
21
22
|
*/
|
|
22
23
|
export async function fetchPublicManifest(context) {
|
|
23
24
|
const ownerEntityHash = String(context.ownerEntityHash || '').trim().toLowerCase()
|
|
@@ -48,10 +49,22 @@ export async function fetchPublicManifest(context) {
|
|
|
48
49
|
const result = await done
|
|
49
50
|
if (!result) return null
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
if (context.cache === true)
|
|
53
|
+
await cachePublicManifest(ownerEntityHash, logicalPath, result)
|
|
52
54
|
return result
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* 将已验签公开 manifest 写入本地缓存(显式 apply;`fetchPublicManifest` 默认不调用)。
|
|
59
|
+
* @param {string} ownerEntityHash owner
|
|
60
|
+
* @param {string} logicalPath 路径
|
|
61
|
+
* @param {import('./manifest.mjs').FileManifest} incoming 已验签入站清单
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
export async function cachePublicManifest(ownerEntityHash, logicalPath, incoming) {
|
|
65
|
+
await maybeCacheIncomingPublicManifest(ownerEntityHash, logicalPath, incoming)
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
/**
|
|
56
69
|
* @param {string} ownerEntityHash owner
|
|
57
70
|
* @param {string} logicalPath 路径
|
|
@@ -116,18 +116,18 @@ export function shouldPreferIncomingPublicManifest(localManifest, incoming) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* @param {object}
|
|
120
|
-
* @param {string}
|
|
121
|
-
* @param {string}
|
|
122
|
-
* @param {Buffer | Uint8Array}
|
|
123
|
-
* @param {string} [
|
|
124
|
-
* @param {string} [
|
|
125
|
-
* @param {Uint8Array | Buffer}
|
|
126
|
-
* @param {string}
|
|
127
|
-
* @param {number} [
|
|
119
|
+
* @param {object} parameters 参数
|
|
120
|
+
* @param {string} parameters.ownerEntityHash owner
|
|
121
|
+
* @param {string} parameters.logicalPath 路径
|
|
122
|
+
* @param {Buffer | Uint8Array} parameters.plaintext 明文
|
|
123
|
+
* @param {string} [parameters.name] 文件名
|
|
124
|
+
* @param {string} [parameters.mimeType] MIME
|
|
125
|
+
* @param {Uint8Array | Buffer} parameters.entitySecretKey recovery 私钥种子
|
|
126
|
+
* @param {string} parameters.entityPubKeyHex recovery 公钥 hex
|
|
127
|
+
* @param {number} [parameters.publishedAt] 发布时间(默认 Date.now)
|
|
128
128
|
* @returns {Promise<import('./manifest.mjs').FileManifest>} 已签名并落盘的公开清单
|
|
129
129
|
*/
|
|
130
|
-
export async function publishPublicFile(
|
|
130
|
+
export async function publishPublicFile(parameters) {
|
|
131
131
|
const {
|
|
132
132
|
ownerEntityHash,
|
|
133
133
|
logicalPath,
|
|
@@ -137,7 +137,7 @@ export async function publishPublicFile(params) {
|
|
|
137
137
|
entitySecretKey,
|
|
138
138
|
entityPubKeyHex,
|
|
139
139
|
publishedAt = Date.now(),
|
|
140
|
-
} =
|
|
140
|
+
} = parameters
|
|
141
141
|
const base = await putFileManifest({
|
|
142
142
|
ownerEntityHash,
|
|
143
143
|
logicalPath,
|
|
@@ -90,8 +90,8 @@ export async function readDagManifestPlaintext(replicaUsername, manifest) {
|
|
|
90
90
|
const reader = dagPlaintextReadersByOwner.get(ownerId)
|
|
91
91
|
if (reader)
|
|
92
92
|
try {
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
93
|
+
const buffer = await reader(replicaUsername, manifest)
|
|
94
|
+
if (buffer?.length) return buffer
|
|
95
95
|
}
|
|
96
96
|
catch { /* fall through */ }
|
|
97
97
|
return null
|
package/index.mjs
CHANGED
|
@@ -1,40 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Federation P2P 门面:fount 网络引导与房间/发现入口。
|
|
3
|
-
* 上层只面对 nodeHash + envelope,不选择 WebRTC/BLE 等传输。
|
|
4
|
-
* 重型子系统请从子路径导入(如 `./dag`);勿导入未导出的 `link/`。
|
|
5
3
|
*/
|
|
6
4
|
import { registerDiscoveryProvider } from './discovery/index.mjs'
|
|
5
|
+
import {
|
|
6
|
+
isInfraRunning,
|
|
7
|
+
setInfraPriority,
|
|
8
|
+
startInfra,
|
|
9
|
+
stopInfra,
|
|
10
|
+
} from './infra/service.mjs'
|
|
11
|
+
import { registerLinkProvider } from './link/providers/index.mjs'
|
|
7
12
|
import { ensureNodeDefaults, getNodeHash } from './node/identity.mjs'
|
|
8
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
getNodeDir,
|
|
15
|
+
initNode,
|
|
16
|
+
isNodeInitialized,
|
|
17
|
+
setNodeLogger,
|
|
18
|
+
setSignalingRuntimeConfig,
|
|
19
|
+
} from './node/instance.mjs'
|
|
20
|
+
import { setConnectivityDebug } from './node/log.mjs'
|
|
21
|
+
import {
|
|
22
|
+
attachReputationSyncWire,
|
|
23
|
+
getReputationExportAllowlist,
|
|
24
|
+
getReputationLocks,
|
|
25
|
+
getReputationTable,
|
|
26
|
+
getTrustSyncDonors,
|
|
27
|
+
lockReputationMax,
|
|
28
|
+
pullReputationFromNode,
|
|
29
|
+
setReputationExportAllowlist,
|
|
30
|
+
setReputationTable,
|
|
31
|
+
setTrustSyncDonors,
|
|
32
|
+
unlockReputationMax,
|
|
33
|
+
} from './node/reputation_sync.mjs'
|
|
34
|
+
import { getRoutingProfile, setRoutingProfile } from './node/routing_profile.mjs'
|
|
9
35
|
import { createScopedLinkRoom } from './rooms/scoped_link.mjs'
|
|
10
36
|
import { createGroupLinkSet } from './transport/group_link_set.mjs'
|
|
11
|
-
import {
|
|
12
|
-
|
|
37
|
+
import {
|
|
38
|
+
configureLinkRegistry,
|
|
39
|
+
ensureLinkToNode,
|
|
40
|
+
ensureOverlayRouter,
|
|
41
|
+
getLinkRegistry,
|
|
42
|
+
reloadDiscoveryRelays,
|
|
43
|
+
sendToNodeLink,
|
|
44
|
+
} from './transport/link_registry.mjs'
|
|
45
|
+
import {
|
|
46
|
+
attachUserRoomDefaultWires,
|
|
47
|
+
ensureNodeScope,
|
|
48
|
+
ensureUserRoom,
|
|
49
|
+
getUserRoomSlot,
|
|
50
|
+
} from './transport/user_room.mjs'
|
|
13
51
|
|
|
14
52
|
/**
|
|
15
|
-
*
|
|
53
|
+
* 包门面:节点、infra、mesh/registry、信誉同步、node-scope 等公开导出。
|
|
16
54
|
*/
|
|
17
55
|
export {
|
|
56
|
+
attachReputationSyncWire,
|
|
57
|
+
attachUserRoomDefaultWires,
|
|
58
|
+
configureLinkRegistry,
|
|
18
59
|
createGroupLinkSet,
|
|
19
60
|
createScopedLinkRoom,
|
|
61
|
+
ensureLinkToNode,
|
|
20
62
|
ensureNodeDefaults,
|
|
63
|
+
ensureNodeScope,
|
|
64
|
+
ensureOverlayRouter,
|
|
21
65
|
ensureUserRoom,
|
|
22
66
|
getLinkRegistry,
|
|
23
67
|
getNodeDir,
|
|
24
68
|
getNodeHash,
|
|
69
|
+
getReputationExportAllowlist,
|
|
70
|
+
getReputationLocks,
|
|
71
|
+
getReputationTable,
|
|
72
|
+
getRoutingProfile,
|
|
73
|
+
getTrustSyncDonors,
|
|
74
|
+
getUserRoomSlot,
|
|
25
75
|
initNode,
|
|
76
|
+
isInfraRunning,
|
|
26
77
|
isNodeInitialized,
|
|
78
|
+
lockReputationMax,
|
|
79
|
+
pullReputationFromNode,
|
|
27
80
|
registerDiscoveryProvider,
|
|
81
|
+
registerLinkProvider,
|
|
82
|
+
reloadDiscoveryRelays,
|
|
83
|
+
sendToNodeLink,
|
|
84
|
+
setConnectivityDebug,
|
|
85
|
+
setInfraPriority,
|
|
86
|
+
setNodeLogger,
|
|
87
|
+
setReputationExportAllowlist,
|
|
88
|
+
setReputationTable,
|
|
89
|
+
setRoutingProfile,
|
|
90
|
+
setSignalingRuntimeConfig,
|
|
91
|
+
setTrustSyncDonors,
|
|
92
|
+
startInfra,
|
|
93
|
+
stopInfra,
|
|
94
|
+
unlockReputationMax,
|
|
28
95
|
}
|
|
29
96
|
|
|
30
97
|
/**
|
|
31
|
-
*
|
|
32
|
-
* @param {{ nodeDir: string, entityStore?: import('./node/entity_store.mjs').EntityStore, logger?: object, signaling?: import('./node/signaling_config.mjs').SignalingRuntimeConfig }} options 节点配置
|
|
98
|
+
* @param {{ nodeDir?: string, entityStore?: import('./node/entity_store.mjs').EntityStore, logger?: object | null, signaling?: import('./node/signaling_config.mjs').SignalingRuntimeConfig }} [options] - 首次 init 时的节点选项
|
|
33
99
|
* @returns {Promise<void>}
|
|
34
100
|
*/
|
|
35
|
-
export async function startNode(options) {
|
|
36
|
-
if (!isNodeInitialized())
|
|
37
|
-
|
|
101
|
+
export async function startNode(options = {}) {
|
|
102
|
+
if (!isNodeInitialized()) {
|
|
103
|
+
const { nodeDir, entityStore, logger, signaling, ...rest } = options
|
|
104
|
+
if (Object.keys(rest).length)
|
|
105
|
+
throw new Error('p2p: startNode unknown options')
|
|
106
|
+
initNode({ nodeDir, entityStore })
|
|
107
|
+
if (logger !== undefined) setNodeLogger(logger)
|
|
108
|
+
if (signaling !== undefined) setSignalingRuntimeConfig(signaling)
|
|
109
|
+
}
|
|
110
|
+
else if (options?.nodeDir || options?.entityStore || options?.logger !== undefined || options?.signaling)
|
|
111
|
+
throw new Error('p2p: startNode options ignored after initNode — use setNodeLogger / setSignalingRuntimeConfig')
|
|
112
|
+
|
|
38
113
|
ensureNodeDefaults()
|
|
39
114
|
await getLinkRegistry().ensureRuntime()
|
|
40
115
|
}
|
package/infra/cli.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { on_shutdown } from 'on-shutdown'
|
|
4
|
+
|
|
5
|
+
import { ensureNodeDefaults, getNodeHash } from '../node/identity.mjs'
|
|
6
|
+
import { initNode, setNodeLogger } from '../node/instance.mjs'
|
|
7
|
+
import { setConnectivityDebug } from '../node/log.mjs'
|
|
8
|
+
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
9
|
+
|
|
10
|
+
import { resolveNodeDir } from './default_node_dir.mjs'
|
|
11
|
+
import { setInfraPriority, startInfra, stopInfra } from './service.mjs'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string[]} argv - 命令行参数(不含 node 路径)
|
|
15
|
+
* @returns {{ nodeDir?: string, quiet?: boolean, useLocalReputation?: boolean, help?: boolean }} 解析结果
|
|
16
|
+
*/
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
/** @type {ReturnType<typeof parseArgs>} */
|
|
19
|
+
const out = {}
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const arg = argv[i]
|
|
22
|
+
if (arg === '--help' || arg === '-h') out.help = true
|
|
23
|
+
else if (arg === '--quiet') out.quiet = true
|
|
24
|
+
else if (arg === '--use-local-reputation') out.useLocalReputation = true
|
|
25
|
+
else if (arg === '--node-dir') out.nodeDir = argv[++i]
|
|
26
|
+
}
|
|
27
|
+
return out
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 打印 CLI 用法。 */
|
|
31
|
+
function printHelp() {
|
|
32
|
+
console.log(`Usage: fount-p2p [--node-dir PATH] [--use-local-reputation] [--quiet]
|
|
33
|
+
|
|
34
|
+
Public-good infra relay node (overlay + mailbox).
|
|
35
|
+
Connectivity debug logs on by default (Nostr/LAN/mesh/dial); --quiet silences.
|
|
36
|
+
Non-CLI shells: setConnectivityDebug(true).
|
|
37
|
+
Default data dir: Windows %LOCALAPPDATA%/fount-p2p/node, else ~/.local/share/fount-p2p/node`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const args = parseArgs(process.argv.slice(2))
|
|
41
|
+
if (args.help) {
|
|
42
|
+
printHelp()
|
|
43
|
+
process.exit(0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const nodeDir = resolveNodeDir(args.nodeDir)
|
|
47
|
+
initNode({ nodeDir })
|
|
48
|
+
if (args.quiet) {
|
|
49
|
+
setNodeLogger(null)
|
|
50
|
+
setConnectivityDebug(false)
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
setConnectivityDebug(true)
|
|
54
|
+
ensureNodeDefaults()
|
|
55
|
+
const nodeHash = getNodeHash()
|
|
56
|
+
await getLinkRegistry().ensureRuntime()
|
|
57
|
+
if (args.useLocalReputation) setInfraPriority({ useLocalReputation: true })
|
|
58
|
+
await startInfra({ logger: args.quiet ? null : console })
|
|
59
|
+
|
|
60
|
+
console.log(`p2p infra running (nodeDir=${nodeDir} nodeHash=${nodeHash})`)
|
|
61
|
+
|
|
62
|
+
on_shutdown(stopInfra)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
2
|
+
|
|
3
|
+
/** @type {Array<() => void>} */
|
|
4
|
+
const cleanups = []
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 挂载 registry link/overlay/node scope 调试日志。
|
|
8
|
+
* @param {{ info?: Function, warn?: Function, error?: Function, log?: Function } | null} logger - 日志输出目标,null 表示静默
|
|
9
|
+
* @returns {() => void} 取消 debug 监听的 dispose
|
|
10
|
+
*/
|
|
11
|
+
export function attachInfraDebugLog(logger) {
|
|
12
|
+
detachInfraDebugLog()
|
|
13
|
+
if (!logger) return () => { }
|
|
14
|
+
const registry = getLinkRegistry()
|
|
15
|
+
/**
|
|
16
|
+
* @param {'info' | 'warn' | 'error' | 'log'} level - 日志级别
|
|
17
|
+
* @param {string} message - 日志消息
|
|
18
|
+
* @param {object} [extra] - 附加字段
|
|
19
|
+
*/
|
|
20
|
+
const log = (level, message, extra) => {
|
|
21
|
+
logger?.[level]?.(message, extra)
|
|
22
|
+
}
|
|
23
|
+
cleanups.push(registry.onLinkUp((nodeHash, link) => {
|
|
24
|
+
log('info', 'p2p:infra link up', {
|
|
25
|
+
nodeHash,
|
|
26
|
+
providerId: link?.providerId,
|
|
27
|
+
level: link?.level,
|
|
28
|
+
})
|
|
29
|
+
}))
|
|
30
|
+
cleanups.push(registry.onLinkDown((nodeHash, reason) => {
|
|
31
|
+
log('info', 'p2p:infra link down', { nodeHash, reason })
|
|
32
|
+
}))
|
|
33
|
+
cleanups.push(registry.subscribeScope('overlay', (from, envelope) => {
|
|
34
|
+
log('info', 'p2p:infra overlay', {
|
|
35
|
+
from,
|
|
36
|
+
action: envelope?.action,
|
|
37
|
+
path: envelope?.payload?.path,
|
|
38
|
+
})
|
|
39
|
+
}))
|
|
40
|
+
cleanups.push(registry.subscribeScope('node', (from, envelope) => {
|
|
41
|
+
log('info', 'p2p:infra node', {
|
|
42
|
+
from,
|
|
43
|
+
action: envelope?.action,
|
|
44
|
+
})
|
|
45
|
+
}))
|
|
46
|
+
return detachInfraDebugLog
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 卸掉 infra debug 监听。
|
|
51
|
+
* @returns {void}
|
|
52
|
+
*/
|
|
53
|
+
export function detachInfraDebugLog() {
|
|
54
|
+
for (const cleanup of cleanups.splice(0))
|
|
55
|
+
try { cleanup() } catch { /* ignore */ }
|
|
56
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @returns {string} 平台默认 node 数据目录
|
|
6
|
+
*/
|
|
7
|
+
export function defaultNodeDir() {
|
|
8
|
+
if (process.platform === 'win32') {
|
|
9
|
+
const base = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
|
|
10
|
+
return path.join(base, 'fount-p2p', 'node')
|
|
11
|
+
}
|
|
12
|
+
return path.join(os.homedir(), '.local', 'share', 'fount-p2p', 'node')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string | undefined} override - CLI 或调用方覆盖
|
|
17
|
+
* @returns {string} 解析后的绝对 node 目录
|
|
18
|
+
*/
|
|
19
|
+
export function resolveNodeDir(override) {
|
|
20
|
+
const trimmed = String(override || '').trim()
|
|
21
|
+
return trimmed ? path.resolve(trimmed) : defaultNodeDir()
|
|
22
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
|
+
import { getReputationTable } from '../node/reputation_sync.mjs'
|
|
3
|
+
import { pickNodeScoreFromReputation } from '../reputation/pick_score.mjs'
|
|
4
|
+
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** @type {{ useLocalReputation: boolean }} */
|
|
8
|
+
let priorityConfig = { useLocalReputation: false }
|
|
9
|
+
|
|
10
|
+
const PRIORITY_BOOST = 1000
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 配置 infra 路由加权(是否用本地 reputation)。
|
|
14
|
+
* @param {{ useLocalReputation?: boolean }} config - 是否用本地 reputation 加权路由
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
export function setInfraPriority(config = {}) {
|
|
18
|
+
priorityConfig = {
|
|
19
|
+
useLocalReputation: Boolean(config.useLocalReputation),
|
|
20
|
+
}
|
|
21
|
+
applyPriorityToRegistry()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @returns {{ useLocalReputation: boolean }} 当前 priority 配置副本
|
|
26
|
+
*/
|
|
27
|
+
export function getInfraPriority() {
|
|
28
|
+
return { ...priorityConfig }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
export function applyPriorityToRegistry() {
|
|
35
|
+
const registry = getLinkRegistry()
|
|
36
|
+
if (!priorityConfig.useLocalReputation) {
|
|
37
|
+
registry.setPriorityWeightFunction(null)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
registry.setPriorityWeightFunction(nodeHash => {
|
|
41
|
+
const score = pickNodeScoreFromReputation(
|
|
42
|
+
getReputationTable(),
|
|
43
|
+
normalizeHex64(nodeHash) || nodeHash,
|
|
44
|
+
)
|
|
45
|
+
return Math.floor(score * PRIORITY_BOOST)
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* stopInfra:卸 weight,并重置 priority 配置,避免再次 startInfra 幽灵恢复加权。
|
|
51
|
+
* @returns {void}
|
|
52
|
+
*/
|
|
53
|
+
export function clearInfraPriorityFromRegistry() {
|
|
54
|
+
priorityConfig = { useLocalReputation: false }
|
|
55
|
+
getLinkRegistry().setPriorityWeightFunction(null)
|
|
56
|
+
}
|