@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.
Files changed (75) hide show
  1. package/README.md +41 -10
  2. package/core/composite_key.mjs +5 -5
  3. package/crypto/crypto.mjs +3 -3
  4. package/crypto/key.mjs +12 -12
  5. package/dag/storage.mjs +36 -36
  6. package/discovery/advert_peer_hints.mjs +1 -1
  7. package/discovery/adverts.mjs +109 -0
  8. package/discovery/bt/index.mjs +153 -112
  9. package/discovery/bt/probe_child.mjs +2 -1
  10. package/discovery/bt/runtime.mjs +2 -9
  11. package/discovery/index.mjs +267 -62
  12. package/discovery/internal/signal_crypto.mjs +109 -0
  13. package/discovery/lan.mjs +228 -0
  14. package/discovery/nostr.mjs +394 -59
  15. package/federation/chunk_fetch_pending.mjs +10 -13
  16. package/federation/manifest_fetch_pending.mjs +1 -3
  17. package/files/assemble.mjs +14 -14
  18. package/files/assemble_stream.mjs +6 -6
  19. package/files/chunk_responder.mjs +29 -20
  20. package/files/evfs.mjs +29 -28
  21. package/files/manifest_fetch.mjs +16 -3
  22. package/files/public_manifest.mjs +11 -11
  23. package/files/transfer_key_registry.mjs +2 -2
  24. package/index.mjs +86 -11
  25. package/infra/cli.mjs +62 -0
  26. package/infra/debug_log.mjs +56 -0
  27. package/infra/default_node_dir.mjs +22 -0
  28. package/infra/priority.mjs +56 -0
  29. package/infra/service.mjs +140 -0
  30. package/infra/tunables.json +5 -0
  31. package/link/frame.mjs +9 -16
  32. package/link/handshake.mjs +14 -15
  33. package/link/pipe.mjs +8 -8
  34. package/link/providers/ble_gatt.mjs +6 -6
  35. package/link/rtc.mjs +3 -3
  36. package/mailbox/consumer_registry.mjs +2 -2
  37. package/mailbox/deliver_or_store.mjs +5 -5
  38. package/mailbox/wire.mjs +28 -24
  39. package/node/entity_store.mjs +6 -6
  40. package/node/instance.mjs +46 -27
  41. package/node/local_data_revision.mjs +26 -0
  42. package/node/log.mjs +56 -0
  43. package/node/network.mjs +37 -5
  44. package/node/reputation_store.mjs +4 -4
  45. package/node/reputation_sync.mjs +318 -0
  46. package/node/routing_profile.mjs +21 -0
  47. package/node/signaling_config.mjs +30 -11
  48. package/overlay/index.mjs +22 -1
  49. package/package.json +13 -2
  50. package/rooms/scoped_link.mjs +17 -166
  51. package/transport/advert_ingest.mjs +11 -14
  52. package/transport/group_link_set.mjs +149 -99
  53. package/transport/link_registry.mjs +211 -60
  54. package/transport/mesh_keepalive.mjs +217 -0
  55. package/transport/node_scope.mjs +289 -0
  56. package/transport/offer_answer.mjs +45 -48
  57. package/transport/peer_pool.mjs +116 -14
  58. package/transport/remote_user_room.mjs +6 -9
  59. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  60. package/transport/runtime_bootstrap.mjs +170 -108
  61. package/transport/tunables.json +11 -0
  62. package/transport/tunables.mjs +18 -0
  63. package/transport/user_room.mjs +83 -158
  64. package/trust_graph/build.mjs +0 -2
  65. package/trust_graph/cache.mjs +12 -3
  66. package/trust_graph/registry.mjs +6 -6
  67. package/trust_graph/send.mjs +0 -3
  68. package/utils/async_mutex.mjs +4 -4
  69. package/utils/emit_safe.mjs +3 -3
  70. package/utils/json_io.mjs +12 -12
  71. package/utils/map_pool.mjs +5 -5
  72. package/wire/part_ingress.mjs +32 -28
  73. package/wire/part_query.mjs +26 -20
  74. package/discovery/mdns.mjs +0 -197
  75. package/transport/signal_crypto.mjs +0 -104
package/README.md CHANGED
@@ -18,15 +18,43 @@ Requires **Node.js ≥ 20** (ESM + `import ... with { type: 'json' }`).
18
18
  ## Quick start
19
19
 
20
20
  ```javascript
21
- import { startNode, createScopedLinkRoom, ensureUserRoom } from '@steve02081504/fount-p2p'
21
+ import {
22
+ startNode,
23
+ ensureUserRoom,
24
+ attachUserRoomDefaultWires,
25
+ createScopedLinkRoom,
26
+ } from '@steve02081504/fount-p2p'
22
27
 
23
28
  await startNode({ nodeDir: '/path/to/p2p/node' })
24
- await ensureUserRoom()
29
+ await ensureUserRoom() // slot + runtime only
30
+ attachUserRoomDefaultWires({ replicaUsername: 'alice' }) // full business wires
25
31
  ```
26
32
 
27
- Shells talk to the **fount network** (`ensureLinkToNode` / `sendToNodeLink` / rooms). Do not import `link/` or choose WebRTC / BLE / LAN yourself.
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.
28
34
 
29
- Subpath exports mirror source directories, e.g. `@steve02081504/fount-p2p/dag`, `@steve02081504/fount-p2p/transport/link_registry`, `@steve02081504/fount-p2p/transport/signal_crypto`, `@steve02081504/fount-p2p/registries/event_type`.
35
+ Public transport subpaths: `link_registry`, `user_room`, `group_link_set`, `node_scope`, `room_scopes`, `remote_user_room`. Other `transport/*` modules are internal.
36
+
37
+ ## Infra relay (optional)
38
+
39
+ Public-good overlay + mailbox only — does **not** attach `rep_sync` or full user-room wires:
40
+
41
+ ```bash
42
+ npx @steve02081504/fount-p2p
43
+ ```
44
+
45
+ Default `nodeDir`: Windows `%LOCALAPPDATA%/fount-p2p/node`; elsewhere `~/.local/share/fount-p2p/node`.
46
+
47
+ ```javascript
48
+ import { initNode, startNode, startInfra, stopInfra, setInfraPriority } from '@steve02081504/fount-p2p'
49
+
50
+ initNode({ nodeDir })
51
+ await startNode()
52
+ await startInfra({ maxActive: 64 }) // logger defaults to console; pass null to silence
53
+ setInfraPriority({ useLocalReputation: true }) // optional; reads local reputation.json only
54
+ await stopInfra()
55
+ ```
56
+
57
+ Reputation pull/apply is separate: `pullReputationFromNode` → JSON; `setReputationTable` writes. See [docs/infra.md](./docs/infra.md).
30
58
 
31
59
  ## Layout
32
60
 
@@ -35,22 +63,22 @@ Subpath exports mirror source directories, e.g. `@steve02081504/fount-p2p/dag`,
35
63
  | L0 | `core/` | Pure primitives: `hexIds`, `entity_id*`, `canonical_json` |
36
64
  | L1 | `crypto/`, `wire/`, `schemas/` | Cryptography, wire protocol, canonical validation |
37
65
  | L2 | `node/` | Node runtime: `identity`, `entity_store`, `denylist`, `reputation_store` |
38
- | L3 | `discovery/`, `link/`, `transport/`, `rooms/` | Discovery + fount-network registry/rooms (`link/providers` are package-private) |
66
+ | L3 | `discovery/`, `link/`, `transport/`, `rooms/` | Discovery + fount-network registry/rooms (`./link` = provider registration only) |
39
67
  | L4 | `trust_graph/`, `mailbox/`, `dag/`, `federation/`, `files/`, `governance/`, `reputation/` | Federation, store-and-forward, DAG, EVFS, tunables |
68
+ | — | `infra/` | Optional public-good relay (`startInfra` / CLI) |
40
69
  | — | `registries/` | Pluggable registries (event type, part path, room provider, …) |
41
70
 
42
71
  Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscoveryProvider`, …).
43
72
 
44
- **Transport modules** (exported under `./transport/*`):
73
+ **Public transport modules:**
45
74
 
46
75
  | Module | Role |
47
76
  |---|---|
48
77
  | `link_registry.mjs` | fount-network facade: dial fallback, scope/overlay |
49
- | `runtime_bootstrap.mjs` | Progressive `ensureRuntime` (register + background listen / discovery / BT) |
50
- | `offer_answer.mjs` | Discovery-signal glare for offer/answer providers |
51
- | `signal_crypto.mjs` | Rendezvous topics + AES-GCM signal packets |
78
+ | `user_room.mjs` / `group_link_set.mjs` / `node_scope.mjs` | rooms + composable node-scope wires |
79
+ | `room_scopes.mjs` / `remote_user_room.mjs` | scope constants / remote user slot |
52
80
 
53
- `ensureRuntime` returns after registration and scheduling warm-up; it does not await lan_tcp listen, public relays, or Bluetooth. See [docs/runtime.md](./docs/runtime.md) and [docs/transports.md](./docs/transports.md).
81
+ `runtime_bootstrap`, `offer_answer`, `advert_ingest` are **internal** (transport). Signal crypto / rendezvous live under `discovery/internal/signal_crypto.mjs` (used by `nostr.mjs` / `adverts.mjs`; not a package export). `ensureRuntime` returns after registration and scheduling warm-up; it does not await lan_tcp listen, public relays, or Bluetooth. `setSignalingRuntimeConfig` → `reloadDiscoveryRelays`. See [docs/runtime.md](./docs/runtime.md) and [docs/transports.md](./docs/transports.md).
54
82
 
55
83
  Root contains only the facade and package metadata; all modules live in layered subdirectories.
56
84
 
@@ -77,5 +105,8 @@ Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStora
77
105
 
78
106
  ## Docs
79
107
 
108
+ - Mesh keep-alive / bootstrap (N/K, mesh-first): [`docs/mesh.md`](./docs/mesh.md)
80
109
  - Transports and provider fallback: [`docs/transports.md`](./docs/transports.md)
81
110
  - Signaling and WebRTC glare: [`docs/signaling.md`](./docs/signaling.md)
111
+ - Runtime bootstrap / BT probe: [`docs/runtime.md`](./docs/runtime.md)
112
+ - Infra relay / node-scope attaches: [`docs/infra.md`](./docs/infra.md)
@@ -77,15 +77,15 @@ export function mapDeleteByPrefix(map, ...prefixParts) {
77
77
  * 遍历前缀子树;回调收到前缀之后的各段与值。
78
78
  * @template V
79
79
  * @param {Map<string, V>} map 根表
80
- * @param {...(string | ((tail: string[], value: V) => void))} prefixPartsAndFn 前缀段 + 末尾回调
80
+ * @param {...(string | ((tail: string[], value: V) => void))} prefixPartsAndCallback 前缀段 + 末尾回调
81
81
  * @returns {void}
82
82
  */
83
- export function mapForEachUnder(map, ...prefixPartsAndFn) {
84
- const fn = prefixPartsAndFn.pop()
85
- const prefix = compositePrefix(...prefixPartsAndFn)
83
+ export function mapForEachUnder(map, ...prefixPartsAndCallback) {
84
+ const callback = prefixPartsAndCallback.pop()
85
+ const prefix = compositePrefix(...prefixPartsAndCallback)
86
86
  for (const [k, value] of map) {
87
87
  if (!k.startsWith(prefix)) continue
88
88
  const tail = k.slice(prefix.length).split(SEP).filter(Boolean)
89
- fn(tail, value)
89
+ callback(tail, value)
90
90
  }
91
91
  }
package/crypto/crypto.mjs CHANGED
@@ -150,9 +150,9 @@ export function sha256TextHex(text) {
150
150
  /**
151
151
  * Buffer / Uint8Array → 小写 hex 字符串
152
152
  *
153
- * @param {Uint8Array|Buffer} buf 二进制缓冲
153
+ * @param {Uint8Array|Buffer} buffer 二进制缓冲
154
154
  * @returns {string} 小写十六进制文本
155
155
  */
156
- function bufferToHexSimple(buf) {
157
- return Buffer.from(buf).toString('hex')
156
+ function bufferToHexSimple(buffer) {
157
+ return Buffer.from(buffer).toString('hex')
158
158
  }
package/crypto/key.mjs CHANGED
@@ -164,8 +164,8 @@ function edPubToX25519(edPub) {
164
164
  for (let i = 31; i >= 0; i--) y = (y << 8n) | BigInt(yCopy[i])
165
165
  const u = (1n + y) * modInv(1n - y + P25519, P25519) % P25519
166
166
  const result = new Uint8Array(32)
167
- let tmp = u
168
- for (let i = 0; i < 32; i++) { result[i] = Number(tmp & 0xffn); tmp >>= 8n }
167
+ let remaining = u
168
+ for (let i = 0; i < 32; i++) { result[i] = Number(remaining & 0xffn); remaining >>= 8n }
169
169
  return result
170
170
  }
171
171
 
@@ -346,11 +346,11 @@ export function encryptRandomPlaintextWithKey(plaintext, contentKey) {
346
346
  */
347
347
  export function decryptConvergentCiphertext(raw, contentHashHex) {
348
348
  try {
349
- const buf = Buffer.from(raw)
350
- if (buf.length < 28) return null
351
- const iv = buf.subarray(0, 12)
352
- const authTag = buf.subarray(12, 28)
353
- const ciphertext = buf.subarray(28)
349
+ const buffer = Buffer.from(raw)
350
+ if (buffer.length < 28) return null
351
+ const iv = buffer.subarray(0, 12)
352
+ const authTag = buffer.subarray(12, 28)
353
+ const ciphertext = buffer.subarray(28)
354
354
  const contentKey = deriveContentKey(contentHashHex)
355
355
  const decipher = createDecipheriv('aes-256-gcm', contentKey, iv)
356
356
  decipher.setAuthTag(authTag)
@@ -371,11 +371,11 @@ export function decryptConvergentCiphertext(raw, contentHashHex) {
371
371
  */
372
372
  export function decryptRandomCiphertext(raw, contentKey, contentHashHex = '') {
373
373
  try {
374
- const buf = Buffer.from(raw)
375
- if (buf.length < 28) return null
376
- const iv = buf.subarray(0, 12)
377
- const authTag = buf.subarray(12, 28)
378
- const ciphertext = buf.subarray(28)
374
+ const buffer = Buffer.from(raw)
375
+ if (buffer.length < 28) return null
376
+ const iv = buffer.subarray(0, 12)
377
+ const authTag = buffer.subarray(12, 28)
378
+ const ciphertext = buffer.subarray(28)
379
379
  const decipher = createDecipheriv('aes-256-gcm', Buffer.from(contentKey), iv)
380
380
  decipher.setAuthTag(authTag)
381
381
  const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()])
package/dag/storage.mjs CHANGED
@@ -20,40 +20,40 @@ const ATOMIC_RENAME_RETRY_CODES = new Set(['EPERM', 'EBUSY', 'EACCES'])
20
20
  * @param {string} filePath 目标路径
21
21
  * @returns {string} 唯一临时文件路径
22
22
  */
23
- function atomicTmpPath(filePath) {
23
+ function atomicTemporaryPath(filePath) {
24
24
  return `${filePath}.tmp.${process.pid}.${randomUUID()}`
25
25
  }
26
26
 
27
27
  /**
28
- * @param {string} tmp 临时文件路径
28
+ * @param {string} temporaryPath 临时文件路径
29
29
  * @returns {Promise<void>}
30
30
  */
31
- async function cleanupAtomicTmp(tmp) {
32
- try { await unlink(tmp) } catch { /* ok */ }
31
+ async function cleanupAtomicTemporary(temporaryPath) {
32
+ try { await unlink(temporaryPath) } catch { /* ok */ }
33
33
  }
34
34
 
35
35
  /**
36
- * 完成原子写的最终 rename;若目标目录已在 cleanup 中消失,则清理残余 tmp 后静默返回。
37
- * @param {string} tmp 临时文件路径
36
+ * 完成原子写的最终 rename;若目标目录已在 cleanup 中消失,则清理残余临时文件后静默返回。
37
+ * @param {string} temporaryPath 临时文件路径
38
38
  * @param {string} filePath 最终目标路径
39
39
  * @returns {Promise<boolean>} 是否已成功落到目标路径
40
40
  */
41
- export async function finalizeAtomicRename(tmp, filePath) {
41
+ export async function finalizeAtomicRename(temporaryPath, filePath) {
42
42
  /** @type {NodeJS.ErrnoException | undefined} */
43
43
  let lastError
44
44
  for (const delayMs of ATOMIC_RENAME_RETRY_DELAYS_MS) {
45
45
  if (delayMs) await sleep(delayMs)
46
46
  try {
47
- await rename(tmp, filePath)
47
+ await rename(temporaryPath, filePath)
48
48
  return true
49
49
  }
50
- catch (err) {
51
- lastError = err
52
- if (ATOMIC_RENAME_RETRY_CODES.has(err?.code)) continue
50
+ catch (error) {
51
+ lastError = error
52
+ if (ATOMIC_RENAME_RETRY_CODES.has(error?.code)) continue
53
53
  break
54
54
  }
55
55
  }
56
- await cleanupAtomicTmp(tmp)
56
+ await cleanupAtomicTemporary(temporaryPath)
57
57
  if (lastError?.code === 'ENOENT') return false
58
58
  throw lastError
59
59
  }
@@ -132,7 +132,7 @@ export async function* readJsonlStream(filePath, options = {}) {
132
132
  export async function rewriteJsonlKeeping(filePath, keep, options = {}) {
133
133
  const dir = dirname(filePath)
134
134
  await mkdir(dir, { recursive: true })
135
- const tmp = atomicTmpPath(filePath)
135
+ const temporaryPath = atomicTemporaryPath(filePath)
136
136
  /** @type {object[]} */
137
137
  const buffer = []
138
138
  let kept = 0
@@ -143,7 +143,7 @@ export async function rewriteJsonlKeeping(filePath, keep, options = {}) {
143
143
  let block = ''
144
144
  for (const row of buffer)
145
145
  block += `${JSON.stringify(row)}\n`
146
- await appendFile(tmp, block, 'utf8')
146
+ await appendFile(temporaryPath, block, 'utf8')
147
147
  buffer.length = 0
148
148
  }
149
149
  try {
@@ -159,7 +159,7 @@ export async function rewriteJsonlKeeping(filePath, keep, options = {}) {
159
159
  }
160
160
  catch { /* source missing */ }
161
161
  if (kept > 0 || dropped > 0)
162
- await finalizeAtomicRename(tmp, filePath)
162
+ await finalizeAtomicRename(temporaryPath, filePath)
163
163
  else
164
164
  try { await writeFile(filePath, '', 'utf8') }
165
165
  catch { /* ok */ }
@@ -179,9 +179,9 @@ export async function readJsonlTipId(filePath) {
179
179
  const { size } = await fh.stat()
180
180
  if (!size) return null
181
181
  const chunk = Math.min(size, 65_536)
182
- const buf = Buffer.alloc(chunk)
183
- await fh.read(buf, 0, chunk, size - chunk)
184
- const lines = buf.toString('utf8').split('\n').filter(Boolean)
182
+ const buffer = Buffer.alloc(chunk)
183
+ await fh.read(buffer, 0, chunk, size - chunk)
184
+ const lines = buffer.toString('utf8').split('\n').filter(Boolean)
185
185
  const last = lines[lines.length - 1]
186
186
  if (!last) return null
187
187
  const row = JSON.parse(last)
@@ -205,14 +205,14 @@ export async function readJsonlTipId(filePath) {
205
205
  export async function writeJsonl(filePath, records) {
206
206
  const dir = dirname(filePath)
207
207
  await mkdir(dir, { recursive: true })
208
- const tmp = atomicTmpPath(filePath)
208
+ const temporaryPath = atomicTemporaryPath(filePath)
209
209
  /** @returns {Generator<string>} JSONL 行 */
210
210
  function* lines() {
211
211
  for (const rec of records)
212
212
  yield `${JSON.stringify(rec)}\n`
213
213
  }
214
- await pipeline(Readable.from(lines()), createWriteStream(tmp, { encoding: 'utf8' }))
215
- await finalizeAtomicRename(tmp, filePath)
214
+ await pipeline(Readable.from(lines()), createWriteStream(temporaryPath, { encoding: 'utf8' }))
215
+ await finalizeAtomicRename(temporaryPath, filePath)
216
216
  }
217
217
 
218
218
  /**
@@ -253,18 +253,18 @@ export async function appendJsonlSynced(filePath, record) {
253
253
 
254
254
  /**
255
255
  * 写入原子临时文件;若父目录已在 cleanup 竞态中消失(ENOENT)则返回 false。
256
- * @param {string} tmp 临时文件路径
256
+ * @param {string} temporaryPath 临时文件路径
257
257
  * @param {string} data 文件内容
258
258
  * @returns {Promise<boolean>} 是否已写入
259
259
  */
260
- async function writeAtomicTmp(tmp, data) {
260
+ async function writeAtomicTemporary(temporaryPath, data) {
261
261
  try {
262
- await writeFile(tmp, data, 'utf8')
262
+ await writeFile(temporaryPath, data, 'utf8')
263
263
  return true
264
264
  }
265
- catch (err) {
266
- if (err?.code === 'ENOENT') return false
267
- throw err
265
+ catch (error) {
266
+ if (error?.code === 'ENOENT') return false
267
+ throw error
268
268
  }
269
269
  }
270
270
 
@@ -277,9 +277,9 @@ async function writeAtomicTmp(tmp, data) {
277
277
  export async function writeJsonAtomic(filePath, obj) {
278
278
  const dir = dirname(filePath)
279
279
  await mkdir(dir, { recursive: true })
280
- const tmp = atomicTmpPath(filePath)
281
- if (!await writeAtomicTmp(tmp, JSON.stringify(obj, null, '\t'))) return
282
- await finalizeAtomicRename(tmp, filePath)
280
+ const temporaryPath = atomicTemporaryPath(filePath)
281
+ if (!await writeAtomicTemporary(temporaryPath, JSON.stringify(obj, null, '\t'))) return
282
+ await finalizeAtomicRename(temporaryPath, filePath)
283
283
  }
284
284
 
285
285
  /**
@@ -291,16 +291,16 @@ export async function writeJsonAtomic(filePath, obj) {
291
291
  export async function writeJsonAtomicSynced(filePath, obj) {
292
292
  const dir = dirname(filePath)
293
293
  await mkdir(dir, { recursive: true })
294
- const tmp = atomicTmpPath(filePath)
295
- if (!await writeAtomicTmp(tmp, JSON.stringify(obj, null, '\t'))) return
296
- const fh = await open(tmp, 'r+')
294
+ const temporaryPath = atomicTemporaryPath(filePath)
295
+ if (!await writeAtomicTemporary(temporaryPath, JSON.stringify(obj, null, '\t'))) return
296
+ const fileHandle = await open(temporaryPath, 'r+')
297
297
  try {
298
- await fh.sync()
298
+ await fileHandle.sync()
299
299
  }
300
300
  finally {
301
- await fh.close()
301
+ await fileHandle.close()
302
302
  }
303
- if (!await finalizeAtomicRename(tmp, filePath)) return
303
+ if (!await finalizeAtomicRename(temporaryPath, filePath)) return
304
304
  const outFh = await open(filePath, 'r+')
305
305
  try {
306
306
  await outFh.sync()
@@ -5,7 +5,7 @@ import { noteLanPeerHint } from './lan_peer_hints.mjs'
5
5
 
6
6
  /**
7
7
  * 从已验证的 discovery advert + provider meta 写入 LAN/BT peer hints。
8
- * 任意 discovery 路径(node / group / scoped topic)收到 advert 时都应调用。
8
+ * 任意 discovery 路径(node / group / scoped)收到 advert 时都应调用。
9
9
  * @param {string} verifiedNodeHash 验签通过的 nodeHash
10
10
  * @param {{ tcpPort?: unknown } | null | undefined} body advert body
11
11
  * @param {{ address?: unknown, peripheralId?: unknown } | null | undefined} meta discovery provider meta
@@ -0,0 +1,109 @@
1
+ import { normalizeHex64 } from '../core/hexIds.mjs'
2
+ import { buildSignedAdvert, verifySignedAdvert } from '../link/handshake.mjs'
3
+
4
+ import {
5
+ decryptSignalPacket,
6
+ encryptSignalPacket,
7
+ groupRendezvousKey,
8
+ networkRendezvousKey,
9
+ nodeRendezvousKey,
10
+ } from './internal/signal_crypto.mjs'
11
+
12
+ /** @typedef {'node' | 'network' | { roomSecret: string }} AdvertScope */
13
+
14
+ /**
15
+ * 按 scope 派生 rendezvous 键。
16
+ * @param {AdvertScope} scope advert 域
17
+ * @param {string} selfNodeHash 本机 nodeHash
18
+ * @returns {string} rendezvous 键(discovery 内部)
19
+ */
20
+ export function rendezvousKeyForScope(scope, selfNodeHash) {
21
+ if (scope === 'network') return networkRendezvousKey()
22
+ if (scope === 'node') return nodeRendezvousKey(selfNodeHash)
23
+ if (scope?.roomSecret) return groupRendezvousKey(scope.roomSecret)
24
+ throw new Error('p2p: invalid advert scope')
25
+ }
26
+
27
+ /**
28
+ * 为本机身份构建已签名 advert body。
29
+ * @param {AdvertScope} scope advert 域
30
+ * @param {{ nodeHash: string, nodePubKey: string, secretKey: Uint8Array }} localIdentity 本地身份
31
+ * @param {number | null | undefined} [tcpPort] LAN TCP 端口
32
+ * @returns {Promise<object>} 签名 advert body
33
+ */
34
+ export async function buildSignedAdvertForScope(scope, localIdentity, tcpPort) {
35
+ const key = rendezvousKeyForScope(scope, localIdentity.nodeHash)
36
+ return await buildSignedAdvert(key, Date.now(), {
37
+ ...localIdentity,
38
+ ...tcpPort != null ? { tcpPort } : {},
39
+ })
40
+ }
41
+
42
+ /**
43
+ * AES-GCM 封装已签名 advert 包。
44
+ * @param {string} rendezvousKey rendezvous 键
45
+ * @param {object} advertBody 已签名 advert
46
+ * @returns {Uint8Array} 加密 advert 字节
47
+ */
48
+ export function encryptAdvertPacket(rendezvousKey, advertBody) {
49
+ return encryptSignalPacket(rendezvousKey, { type: 'advert', body: advertBody })
50
+ }
51
+
52
+ /**
53
+ * 按 scope 加密已签名 advert。
54
+ * @param {AdvertScope} scope advert 域
55
+ * @param {{ nodeHash: string }} localIdentity 本地身份(仅需 nodeHash)
56
+ * @param {object} advertBody 已签名 advert
57
+ * @returns {Uint8Array} 加密 advert 字节
58
+ */
59
+ export function encryptAdvertForScope(scope, localIdentity, advertBody) {
60
+ return encryptAdvertPacket(rendezvousKeyForScope(scope, localIdentity.nodeHash), advertBody)
61
+ }
62
+
63
+ /**
64
+ * Untrusted ingress:解密并验签 advert;失败返回 null,不抛。不写入可见池 / peer hints。
65
+ * @param {string} rendezvousKey rendezvous 键
66
+ * @param {Uint8Array} bytes 加密 advert
67
+ * @param {object} [meta] 元数据
68
+ * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
69
+ */
70
+ export async function ingestEncryptedAdvert(rendezvousKey, bytes, meta) {
71
+ void meta
72
+ const packet = decryptSignalPacket(rendezvousKey, bytes)
73
+ if (packet?.type !== 'advert' || !packet.body) return null
74
+ const verifiedNodeHash = await verifySignedAdvert(rendezvousKey, packet.body)
75
+ if (!verifiedNodeHash) return null
76
+ return { verifiedNodeHash, body: packet.body }
77
+ }
78
+
79
+ /**
80
+ * Untrusted ingress:验签 network-scope advert;失败返回 null。不写盘 / 不写 hints。
81
+ * @param {Uint8Array} bytes 加密 advert
82
+ * @param {object} [meta] 元数据
83
+ * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
84
+ */
85
+ export async function ingestNetworkAdvert(bytes, meta) {
86
+ return ingestEncryptedAdvert(networkRendezvousKey(), bytes, meta)
87
+ }
88
+
89
+ /**
90
+ * Untrusted ingress:验签 node-scope advert;失败返回 null。不写盘 / 不写 hints。
91
+ * @param {string} nodeHash 目标 nodeHash
92
+ * @param {Uint8Array} bytes 加密 advert
93
+ * @param {object} [meta] 元数据
94
+ * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
95
+ */
96
+ export async function ingestNodeAdvert(nodeHash, bytes, meta) {
97
+ return ingestEncryptedAdvert(nodeRendezvousKey(normalizeHex64(nodeHash)), bytes, meta)
98
+ }
99
+
100
+ /**
101
+ * Untrusted ingress:验签 group-scope advert;失败返回 null。不写盘 / 不写 hints。
102
+ * @param {string} roomSecret 房间密钥
103
+ * @param {Uint8Array} bytes 加密 advert
104
+ * @param {object} [meta] 元数据
105
+ * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
106
+ */
107
+ export async function ingestGroupAdvert(roomSecret, bytes, meta) {
108
+ return ingestEncryptedAdvert(groupRendezvousKey(roomSecret), bytes, meta)
109
+ }