@steve02081504/fount-p2p 0.0.26 → 0.0.28

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 (100) hide show
  1. package/README.md +3 -2
  2. package/core/bytes_codec.mjs +5 -6
  3. package/core/composite_key.mjs +0 -79
  4. package/core/entity_id_parse.mjs +2 -3
  5. package/core/evfs_logical_path.mjs +2 -3
  6. package/core/hexIds.mjs +5 -5
  7. package/core/logical_entity.mjs +3 -4
  8. package/crypto/channel.mjs +2 -25
  9. package/crypto/checkpoint_sign.mjs +5 -4
  10. package/crypto/crypto.mjs +0 -8
  11. package/crypto/key.mjs +9 -10
  12. package/dag/canonicalize_row.mjs +1 -1
  13. package/discovery/advert_peer_hints.mjs +1 -1
  14. package/discovery/adverts.mjs +7 -12
  15. package/discovery/bt/index.mjs +6 -11
  16. package/discovery/bt/peer_hints.mjs +2 -3
  17. package/discovery/bt/runtime.mjs +1 -1
  18. package/discovery/index.mjs +14 -14
  19. package/discovery/lan.mjs +3 -3
  20. package/discovery/lan_interfaces.mjs +2 -2
  21. package/discovery/lan_peer_hints.mjs +1 -1
  22. package/discovery/nostr.mjs +19 -24
  23. package/federation/entity_key_chain.mjs +5 -24
  24. package/federation/message_rate_limit.mjs +3 -2
  25. package/federation/part_query/runtime.mjs +9 -20
  26. package/files/assemble.mjs +3 -3
  27. package/files/assemble_stream.mjs +1 -1
  28. package/files/chunk/fetch.mjs +1 -2
  29. package/files/chunk/responder.mjs +4 -41
  30. package/files/chunk/store.mjs +2 -3
  31. package/files/chunk/verify.mjs +2 -3
  32. package/files/evfs.mjs +1 -1
  33. package/files/evfs_ref.mjs +3 -4
  34. package/files/fetch_fanout.mjs +3 -3
  35. package/files/manifest/fetch.mjs +2 -3
  36. package/files/manifest/normalize.mjs +9 -9
  37. package/files/manifest/pending.mjs +1 -4
  38. package/files/manifest/public.mjs +8 -8
  39. package/files/transfer_key.mjs +3 -3
  40. package/files/transfer_key_registry.mjs +6 -6
  41. package/governance/branch.mjs +2 -1
  42. package/governance/join_pow.mjs +7 -5
  43. package/governance/owner_succession_ballot.mjs +6 -6
  44. package/index.mjs +8 -2
  45. package/infra/default_node_dir.mjs +2 -2
  46. package/infra/priority.mjs +2 -2
  47. package/link/frame.mjs +2 -3
  48. package/link/handshake.mjs +6 -6
  49. package/link/pipe.mjs +69 -5
  50. package/link/providers/ble_gatt.mjs +1 -9
  51. package/link/providers/index.mjs +2 -2
  52. package/link/providers/nostr.mjs +1 -3
  53. package/link/providers/webrtc.mjs +2 -2
  54. package/link/rtc/ice_local_hostname.mjs +6 -5
  55. package/link/rtc/w3c_bridge.mjs +1 -1
  56. package/link/sdp_fingerprint.mjs +1 -1
  57. package/mailbox/consumer_registry.mjs +2 -2
  58. package/mailbox/deliver_or_store.mjs +4 -4
  59. package/mailbox/parse.mjs +1 -1
  60. package/mailbox/prune.mjs +2 -2
  61. package/mailbox/rate.mjs +3 -10
  62. package/mailbox/store.mjs +7 -7
  63. package/node/denylist.mjs +38 -38
  64. package/node/entity_store.mjs +5 -5
  65. package/node/identity.mjs +7 -8
  66. package/node/instance.mjs +1 -1
  67. package/node/log.mjs +1 -1
  68. package/node/network.mjs +14 -16
  69. package/node/personal_block.mjs +22 -20
  70. package/node/reputation_store.mjs +11 -19
  71. package/node/reputation_sync.mjs +0 -7
  72. package/node/signaling_config.mjs +1 -1
  73. package/node/storage_plugins.mjs +2 -2
  74. package/overlay/index.mjs +1 -1
  75. package/package.json +1 -1
  76. package/registries/event_type.mjs +2 -2
  77. package/registries/room_provider.mjs +2 -2
  78. package/reputation/engine.mjs +17 -15
  79. package/schemas/discovery.mjs +5 -5
  80. package/schemas/federation_pull.mjs +16 -24
  81. package/schemas/part_query.mjs +4 -4
  82. package/timeline/verify_remote.mjs +4 -4
  83. package/transport/group_link_set.mjs +3 -3
  84. package/transport/ice_servers.mjs +2 -2
  85. package/transport/link_registry.mjs +51 -20
  86. package/transport/mesh_keepalive.mjs +1 -1
  87. package/transport/node_scope/wire.mjs +1 -1
  88. package/transport/peer_health.mjs +127 -0
  89. package/transport/remote_user_room.mjs +13 -15
  90. package/transport/rtc_connection_budget.mjs +6 -6
  91. package/transport/runtime_bootstrap.mjs +12 -12
  92. package/trust_graph/cache.mjs +2 -3
  93. package/trust_graph/engine.mjs +1 -1
  94. package/trust_graph/registry.mjs +2 -2
  95. package/trust_graph/send.mjs +1 -1
  96. package/utils/async_mutex.mjs +1 -1
  97. package/wire/ingress.mjs +1 -1
  98. package/wire/part/ingress.mjs +6 -6
  99. package/wire/part/query.mjs +3 -3
  100. package/wire/volatile_signature.mjs +4 -9
package/README.md CHANGED
@@ -69,7 +69,7 @@ Reputation pull/apply is separate: `pullReputationFromNode` → JSON; `setReputa
69
69
  ## Layout
70
70
 
71
71
  | Layer | Directory | Role |
72
- |---|---|---|
72
+ | --- | --- | --- |
73
73
  | L0 | `core/` | Pure primitives: `hexIds`, `entity_id*`, `canonical_json` |
74
74
  | L1 | `crypto/`, `wire/`, `schemas/` | Cryptography, wire protocol, canonical validation |
75
75
  | L2 | `node/` | Node runtime: `identity`, `entity_store`, `denylist`, `reputation_store` |
@@ -83,7 +83,7 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
83
83
  **Public transport modules:**
84
84
 
85
85
  | Module | Role |
86
- |---|---|
86
+ | --- | --- |
87
87
  | `link_registry.mjs` | fount-network facade: dial fallback, scope/overlay |
88
88
  | `user_room.mjs` / `group_link_set.mjs` / `node_scope/wire` + `node_scope/features` | rooms + composable node-scope wires (`registerNodeScopeWireHook`, attaches) |
89
89
  | `room_scopes.mjs` / `remote_user_room.mjs` | scope constants / remote user slot |
@@ -123,3 +123,4 @@ Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStora
123
123
  - Infra relay / node-scope attaches: [`docs/infra.md`](./docs/infra.md)
124
124
  - Wire part / fanout attaches: [`docs/wire.md`](./docs/wire.md)
125
125
  - EVFS public manifests / fetch: [`docs/evfs.md`](./docs/evfs.md)
126
+ - Subjective reputation: [`docs/reputation.md`](./docs/reputation.md)
@@ -29,16 +29,15 @@ export function bytesToHex(bytes) {
29
29
  }
30
30
 
31
31
  /**
32
- * @param {string} hex hex 文本(可含空白;大小写不敏感)
32
+ * @param {string} hex hex 文本(偶数字符;大小写均可)
33
33
  * @returns {Uint8Array} 解码后的字节
34
34
  */
35
35
  export function hexToBytes(hex) {
36
- const text = hex.trim().toLowerCase()
37
- if (text.length % 2) throw new Error('p2p: hex length must be even')
38
- const out = new Uint8Array(text.length / 2)
36
+ if (hex.length % 2) throw new Error('p2p: hex length must be even')
37
+ const out = new Uint8Array(hex.length / 2)
39
38
  for (let index = 0; index < out.length; index++) {
40
- const high = hexNibble(text.charCodeAt(index * 2))
41
- const low = hexNibble(text.charCodeAt(index * 2 + 1))
39
+ const high = hexNibble(hex.charCodeAt(index * 2))
40
+ const low = hexNibble(hex.charCodeAt(index * 2 + 1))
42
41
  if (high < 0 || low < 0) throw new Error('p2p: invalid hex')
43
42
  out[index] = (high << 4) | low
44
43
  }
@@ -10,82 +10,3 @@ export function compositeKey(...parts) {
10
10
  if (!parts.length) throw new Error('compositeKey: at least one part required')
11
11
  return parts.join(SEP)
12
12
  }
13
-
14
- /**
15
- * @param {...string} prefixParts 前缀段(可为空,表示整表)
16
- * @returns {string} 带尾部分隔符的前缀(用于子树匹配)
17
- */
18
- function compositePrefix(...prefixParts) {
19
- return prefixParts.length ? `${compositeKey(...prefixParts)}${SEP}` : ''
20
- }
21
-
22
- /**
23
- * @template V
24
- * @param {Map<string, V>} map 根表
25
- * @param {...string} parts 键段
26
- * @returns {V | undefined} 命中值
27
- */
28
- export function mapGet(map, ...parts) {
29
- return map.get(compositeKey(...parts))
30
- }
31
-
32
- /**
33
- * @template V
34
- * @param {Map<string, V>} map 根表
35
- * @param {...string} partsAndValue 键段 + 末尾值
36
- * @returns {void}
37
- */
38
- export function mapSet(map, ...partsAndValue) {
39
- const value = partsAndValue.pop()
40
- map.set(compositeKey(...partsAndValue), value)
41
- }
42
-
43
- /**
44
- * @template V
45
- * @param {Map<string, V>} map 根表
46
- * @param {...string} parts 键段
47
- * @returns {boolean} 是否存在
48
- */
49
- export function mapHas(map, ...parts) {
50
- return map.has(compositeKey(...parts))
51
- }
52
-
53
- /**
54
- * @template V
55
- * @param {Map<string, V>} map 根表
56
- * @param {...string} parts 键段
57
- * @returns {void}
58
- */
59
- export function mapDelete(map, ...parts) {
60
- map.delete(compositeKey(...parts))
61
- }
62
-
63
- /**
64
- * 删除以 `prefixParts` 为前缀的所有条目(含更深层级)。
65
- * @template V
66
- * @param {Map<string, V>} map 根表
67
- * @param {...string} prefixParts 前缀段(空则清空整表)
68
- * @returns {void}
69
- */
70
- export function mapDeleteByPrefix(map, ...prefixParts) {
71
- const prefix = compositePrefix(...prefixParts)
72
- for (const k of [...map.keys()])
73
- if (!prefix || k.startsWith(prefix)) map.delete(k)
74
- }
75
-
76
- /**
77
- * 遍历前缀子树;回调收到前缀之后的各段与值。
78
- * @template V
79
- * @param {Map<string, V>} map 根表
80
- * @param {...(string | ((tail: string[], value: V) => void))} prefixPartsAndCallback 前缀段 + 末尾回调
81
- * @returns {void}
82
- */
83
- export function mapForEachUnder(map, ...prefixPartsAndCallback) {
84
- const callback = prefixPartsAndCallback.pop()
85
- const prefix = compositePrefix(...prefixPartsAndCallback)
86
- for (const [k, value] of map) {
87
- if (!k.startsWith(prefix)) continue
88
- const tail = k.slice(prefix.length).split(SEP).filter(Boolean)
89
- callback(tail, value)
90
- }
91
- }
@@ -8,8 +8,7 @@ export const ENTITY_HASH_RE = /^[\da-f]{128}$/u
8
8
  * @returns {boolean} 是否为合法 128 位 hex
9
9
  */
10
10
  export function isEntityHash128(value) {
11
- const raw = String(value ?? '').trim().toLowerCase().replace(/^0x/iu, '')
12
- return ENTITY_HASH_RE.test(raw)
11
+ return ENTITY_HASH_RE.test(String(value ?? '').replace(/^0x/iu, ''))
13
12
  }
14
13
 
15
14
  /**
@@ -17,7 +16,7 @@ export function isEntityHash128(value) {
17
16
  * @returns {{ entityHash: string, nodeHash: string, subjectHash: string } | null} 解析结果;非法时 null
18
17
  */
19
18
  export function parseEntityHash(entityHash) {
20
- const raw = String(entityHash ?? '').trim().toLowerCase().replace(/^0x/iu, '')
19
+ const raw = String(entityHash ?? '').replace(/^0x/iu, '')
21
20
  if (!ENTITY_HASH_RE.test(raw)) return null
22
21
  return {
23
22
  entityHash: raw,
@@ -3,10 +3,9 @@
3
3
  * @returns {string} 规范化后的相对路径
4
4
  */
5
5
  export function assertSafeEvfsLogicalPath(logicalPath) {
6
- const raw = String(logicalPath || '').trim()
7
- if (!raw || raw.includes('\0'))
6
+ if (!logicalPath || logicalPath.includes('\0'))
8
7
  throw new Error('invalid EVFS path')
9
- const segments = raw.split(/[/\\]+/).map(s => s.trim()).filter(Boolean)
8
+ const segments = logicalPath.split(/[/\\]+/).filter(Boolean)
10
9
  if (!segments.length) throw new Error('invalid EVFS path')
11
10
  for (const segment of segments)
12
11
  if (segment === '.' || segment === '..')
package/core/hexIds.mjs CHANGED
@@ -12,15 +12,15 @@ export const LOCAL_CHUNK_FILE_RE = /^local:[^/]+\/chunks\/([\da-f]{64})\.bin$/u
12
12
 
13
13
  /**
14
14
  * @param {unknown} value 原始字符串
15
- * @returns {string} trim + 去 0x + 小写
15
+ * @returns {string} 去可选 0x 前缀后的字符串(不修大小写/空白)
16
16
  */
17
17
  export function normalizeHex64(value) {
18
- return String(value ?? '').trim().toLowerCase().replace(/^0x/iu, '')
18
+ return String(value ?? '').replace(/^0x/iu, '')
19
19
  }
20
20
 
21
21
  /**
22
22
  * @param {unknown} value 待校验值
23
- * @returns {boolean} 是否为 64 hex
23
+ * @returns {boolean} 是否为 64 位小写 hex
24
24
  */
25
25
  export function isHex64(value) {
26
26
  return HEX_ID_64.test(normalizeHex64(value))
@@ -39,7 +39,7 @@ export function compareHex64Asc(a, b) {
39
39
  }
40
40
 
41
41
  /**
42
- * 外部入站专用:规范化并断言 64 hex。
42
+ * 外部入站专用:规范化并断言 64 位小写 hex。
43
43
  * @param {unknown} value 原始值
44
44
  * @param {string} [label='hex64'] 字段名(错误信息)
45
45
  * @returns {string} 小写 64 位 hex
@@ -56,5 +56,5 @@ export function assertHex64(value, label = 'hex64') {
56
56
  * @returns {boolean} 是否为 128 位签名 hex
57
57
  */
58
58
  export function isSignatureHex128(value) {
59
- return SIGNATURE_HEX_128.test(String(value ?? '').trim())
59
+ return SIGNATURE_HEX_128.test(String(value ?? ''))
60
60
  }
@@ -1,4 +1,4 @@
1
- import { sha256TextHex } from '../crypto/crypto.mjs'
1
+ import { sha256Hex } from '../crypto/crypto.mjs'
2
2
 
3
3
  import { encodeEntityHash, parseEntityHash } from './entity_id_parse.mjs'
4
4
 
@@ -10,9 +10,8 @@ export const LOGICAL_ENTITY_SENTINEL_NODE_HASH = '0'.repeat(64)
10
10
  * @returns {string} 128 位 logical entityHash
11
11
  */
12
12
  export function logicalEntityHash(subject) {
13
- const s = String(subject || '').trim()
14
- if (!s) throw new Error('subject required')
15
- return encodeEntityHash(LOGICAL_ENTITY_SENTINEL_NODE_HASH, sha256TextHex(s))
13
+ if (!subject) throw new Error('subject required')
14
+ return encodeEntityHash(LOGICAL_ENTITY_SENTINEL_NODE_HASH, sha256Hex(subject))
16
15
  }
17
16
 
18
17
  /**
@@ -5,13 +5,9 @@
5
5
  import { Buffer } from 'node:buffer'
6
6
  import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto'
7
7
 
8
- import { unwrapKeyEcies, wrapKeyEcies } from './key.mjs'
9
-
10
8
  /** @type {'channel-key'} 频道消息 content 加密 scheme */
11
9
  export const CHANNEL_KEY_SCHEME = 'channel-key'
12
10
 
13
- /** @typedef {{ ephemPub: string, iv: string, ciphertext: string, authTag: string }} EciesWrapBlob */
14
-
15
11
  /**
16
12
  * 生成随机 32 字节频道密钥(hex)。
17
13
  * @returns {string} 32 字节 hex 频道密钥
@@ -20,25 +16,6 @@ export function generateChannelKey() {
20
16
  return randomBytes(32).toString('hex')
21
17
  }
22
18
 
23
- /**
24
- * X25519 ECIES 包装 K_ch 给成员 Ed25519 公钥。
25
- * @param {string} channelKeyHex 32 字节 hex
26
- * @param {string} memberEdPubKeyHex 64 位十六进制
27
- * @returns {EciesWrapBlob} ECIES 包装结果
28
- */
29
- export function wrapChannelKey(channelKeyHex, memberEdPubKeyHex) {
30
- return wrapKeyEcies(channelKeyHex, memberEdPubKeyHex)
31
- }
32
-
33
- /**
34
- * @param {EciesWrapBlob} wrap ECIES 密文
35
- * @param {Uint8Array} myEdPrivKeySeed 32 字节 Ed25519 种子
36
- * @returns {string | null} K_ch hex
37
- */
38
- export function unwrapChannelKey(wrap, myEdPrivKeySeed) {
39
- return unwrapKeyEcies(wrap, myEdPrivKeySeed)
40
- }
41
-
42
19
  /**
43
20
  * @param {string} channelKeyHex K_ch
44
21
  * @param {string} channelId 频道 id(AAD 盐)
@@ -49,7 +26,7 @@ function messageAesKey(channelKeyHex, channelId, generation) {
49
26
  return Buffer.from(hkdfSync(
50
27
  'sha256',
51
28
  Buffer.from(channelKeyHex, 'hex'),
52
- `${CHANNEL_KEY_SCHEME}:${String(channelId)}:${String(generation)}`,
29
+ `${CHANNEL_KEY_SCHEME}:${channelId}:${generation}`,
53
30
  '',
54
31
  32,
55
32
  ))
@@ -71,7 +48,7 @@ export function encryptWithChannelKey(plaintext, channelKeyHex, channelId, gener
71
48
  const authTag = cipher.getAuthTag()
72
49
  return {
73
50
  scheme: CHANNEL_KEY_SCHEME,
74
- channelId: String(channelId),
51
+ channelId,
75
52
  generation: Number(generation) || 0,
76
53
  payload: `${iv.toString('base64')}.${ciphertext.toString('base64')}.${authTag.toString('base64')}`,
77
54
  }
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from 'node:buffer'
2
2
 
3
3
  import { canonicalStringify } from '../core/canonical_json.mjs'
4
+ import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
4
5
  import { merkleRoot } from '../dag/index.mjs'
5
6
 
6
7
  import { sign, verify } from './crypto.mjs'
@@ -26,8 +27,8 @@ export async function signCheckpoint(payload, secretKey) {
26
27
  * @returns {Promise<boolean>} 合法为 true
27
28
  */
28
29
  export async function verifyCheckpointSignature(checkpoint, ownerPublicKey) {
29
- const raw = checkpoint.checkpoint_signature.trim()
30
- if (!/^[\da-f]{128}$/iu.test(raw)) return false
30
+ const raw = String(checkpoint.checkpoint_signature || '')
31
+ if (!isSignatureHex128(raw)) return false
31
32
  const body = { ...checkpoint }
32
33
  delete body.checkpoint_signature
33
34
  const messageBytes = Buffer.from(canonicalStringify(body), 'utf8')
@@ -40,7 +41,7 @@ export async function verifyCheckpointSignature(checkpoint, ownerPublicKey) {
40
41
  * @returns {boolean} 签名格式合法为 true
41
42
  */
42
43
  export function isSignedCheckpoint(checkpoint) {
43
- return /^[\da-f]{128}$/iu.test(String(checkpoint?.checkpoint_signature || '').trim())
44
+ return isSignatureHex128(String(checkpoint?.checkpoint_signature || ''))
44
45
  }
45
46
 
46
47
  /**
@@ -59,7 +60,7 @@ export async function verifyRemoteCheckpoint(checkpoint) {
59
60
  const ownerHash = checkpoint.members_record?.delegatedOwnerPubKeyHash
60
61
  const owner = checkpoint.members_record?.members?.[ownerHash]
61
62
  const pubHex = owner?.pubKeyHex
62
- if (!pubHex || !/^[\da-f]{64}$/iu.test(pubHex))
63
+ if (!pubHex || !isHex64(pubHex))
63
64
  return { valid: false, reason: 'delegated owner pubkey missing' }
64
65
  if (!await verifyCheckpointSignature(checkpoint, Buffer.from(pubHex, 'hex')))
65
66
  return { valid: false, reason: 'checkpoint signature invalid' }
package/crypto/crypto.mjs CHANGED
@@ -81,11 +81,3 @@ export function pubKeyHash(publicKey) {
81
81
  export function sha256Hex(data) {
82
82
  return bytesToHex(sha256(inputBytes(data)))
83
83
  }
84
-
85
- /**
86
- * @param {string} text 文本
87
- * @returns {string} 64 字符 hex
88
- */
89
- export function sha256TextHex(text) {
90
- return sha256Hex(String(text ?? ''))
91
- }
package/crypto/key.mjs CHANGED
@@ -73,7 +73,7 @@ function toHBuf(H) {
73
73
  * @returns {Buffer} 32 字节 AES-256 密钥
74
74
  */
75
75
  export function deriveFileKey(H, fileId) {
76
- return kdf(toHBuf(H), 'file', String(fileId))
76
+ return kdf(toHBuf(H), 'file', fileId)
77
77
  }
78
78
 
79
79
  /**
@@ -83,7 +83,7 @@ export function deriveFileKey(H, fileId) {
83
83
  * @returns {Buffer} 32 字节 AES-256 密钥
84
84
  */
85
85
  export function deriveSocialPostKey(H, postId) {
86
- return kdf(toHBuf(H), 'post', String(postId))
86
+ return kdf(toHBuf(H), 'post', postId)
87
87
  }
88
88
 
89
89
  /**
@@ -93,7 +93,7 @@ export function deriveSocialPostKey(H, postId) {
93
93
  * @returns {Buffer} 32 字节 HMAC 密钥
94
94
  */
95
95
  export function deriveStreamingAuthKey(H, groupId) {
96
- return kdf(toHBuf(H), 'streaming', String(groupId))
96
+ return kdf(toHBuf(H), 'streaming', groupId)
97
97
  }
98
98
 
99
99
  // ─── H 轮换(§11.2、§6.3 key_rotate)──────────────────────────────────────
@@ -109,8 +109,8 @@ export function deriveStreamingAuthKey(H, groupId) {
109
109
  export function deriveNextFileMasterKey(oldKeyHex, eventId, nonce) {
110
110
  return createHash('sha256')
111
111
  .update(Buffer.from(oldKeyHex, 'hex'))
112
- .update(String(eventId))
113
- .update(String(nonce))
112
+ .update(eventId)
113
+ .update(nonce)
114
114
  .digest('hex')
115
115
  }
116
116
 
@@ -245,7 +245,7 @@ export function encryptUtf8ForMember(utf8Text, memberEdPubKeyHex) {
245
245
  const wrapKey = createHash('sha256').update(sharedSecret).digest()
246
246
  const iv = randomBytes(12)
247
247
  const cipher = createCipheriv('aes-256-gcm', wrapKey, iv)
248
- const plain = Buffer.from(String(utf8Text), 'utf8')
248
+ const plain = Buffer.from(utf8Text, 'utf8')
249
249
  const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
250
250
  const authTag = cipher.getAuthTag()
251
251
  return {
@@ -356,7 +356,7 @@ export function decryptConvergentCiphertext(raw, contentHashHex) {
356
356
  decipher.setAuthTag(authTag)
357
357
  const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()])
358
358
  const check = createHash('sha256').update(plain).digest('hex')
359
- if (check !== contentHashHex.toLowerCase()) return null
359
+ if (check !== contentHashHex) return null
360
360
  return plain
361
361
  }
362
362
  catch { return null }
@@ -379,10 +379,9 @@ export function decryptRandomCiphertext(raw, contentKey, contentHashHex = '') {
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()])
382
- const expect = String(contentHashHex || '').trim().toLowerCase()
383
- if (expect) {
382
+ if (contentHashHex) {
384
383
  const check = createHash('sha256').update(plain).digest('hex')
385
- if (check !== expect) return null
384
+ if (check !== contentHashHex) return null
386
385
  }
387
386
  return plain
388
387
  }
@@ -28,7 +28,7 @@ export function canonicalizeRowContent(content, hexKeys, entityHashKeys = new Se
28
28
  canonicalizeHexField(out, key)
29
29
  for (const key of entityHashKeys) {
30
30
  if (!out[key]) continue
31
- const entityHash = String(out[key]).toLowerCase()
31
+ const entityHash = String(out[key] || '')
32
32
  if (!isEntityHash128(entityHash))
33
33
  throw new Error(`${key} must be 128 hex characters`)
34
34
  out[key] = entityHash
@@ -21,7 +21,7 @@ export function noteAdvertPeerHints(verifiedNodeHash, body, meta) {
21
21
  // lanHosts 先写(差→优,unshift 后优在前),再写观测 address,使 address 最优先
22
22
  for (const host of normalizeLanHosts(body?.lanHosts).toReversed())
23
23
  noteLanPeerHint(verifiedNodeHash, { host, port: tcpPort })
24
- const address = String(meta?.address || '').trim()
24
+ const address = String(meta?.address || '')
25
25
  if (address)
26
26
  noteLanPeerHint(verifiedNodeHash, { host: address, port: tcpPort })
27
27
  }
@@ -69,11 +69,9 @@ export function encryptAdvertForScope(scope, localIdentity, advertBody) {
69
69
  * Untrusted ingress:解密并验签 advert;失败返回 null,不抛。不写入可见池 / peer hints。
70
70
  * @param {string} rendezvousKey rendezvous 键
71
71
  * @param {Uint8Array} bytes 加密 advert
72
- * @param {object} [meta] 元数据
73
72
  * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
74
73
  */
75
- export async function ingestEncryptedAdvert(rendezvousKey, bytes, meta) {
76
- void meta
74
+ export async function ingestEncryptedAdvert(rendezvousKey, bytes) {
77
75
  const packet = decryptSignalPacket(rendezvousKey, bytes)
78
76
  if (packet?.type !== 'advert' || !packet.body) return null
79
77
  const verifiedNodeHash = await verifySignedAdvert(rendezvousKey, packet.body)
@@ -84,31 +82,28 @@ export async function ingestEncryptedAdvert(rendezvousKey, bytes, meta) {
84
82
  /**
85
83
  * Untrusted ingress:验签 network-scope advert;失败返回 null。不写盘 / 不写 hints。
86
84
  * @param {Uint8Array} bytes 加密 advert
87
- * @param {object} [meta] 元数据
88
85
  * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
89
86
  */
90
- export async function ingestNetworkAdvert(bytes, meta) {
91
- return ingestEncryptedAdvert(networkRendezvousKey(), bytes, meta)
87
+ export async function ingestNetworkAdvert(bytes) {
88
+ return ingestEncryptedAdvert(networkRendezvousKey(), bytes)
92
89
  }
93
90
 
94
91
  /**
95
92
  * Untrusted ingress:验签 node-scope advert;失败返回 null。不写盘 / 不写 hints。
96
93
  * @param {string} nodeHash 目标 nodeHash
97
94
  * @param {Uint8Array} bytes 加密 advert
98
- * @param {object} [meta] 元数据
99
95
  * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
100
96
  */
101
- export async function ingestNodeAdvert(nodeHash, bytes, meta) {
102
- return ingestEncryptedAdvert(nodeRendezvousKey(normalizeHex64(nodeHash)), bytes, meta)
97
+ export async function ingestNodeAdvert(nodeHash, bytes) {
98
+ return ingestEncryptedAdvert(nodeRendezvousKey(normalizeHex64(nodeHash)), bytes)
103
99
  }
104
100
 
105
101
  /**
106
102
  * Untrusted ingress:验签 group-scope advert;失败返回 null。不写盘 / 不写 hints。
107
103
  * @param {string} roomSecret 房间密钥
108
104
  * @param {Uint8Array} bytes 加密 advert
109
- * @param {object} [meta] 元数据
110
105
  * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
111
106
  */
112
- export async function ingestGroupAdvert(roomSecret, bytes, meta) {
113
- return ingestEncryptedAdvert(groupRendezvousKey(roomSecret), bytes, meta)
107
+ export async function ingestGroupAdvert(roomSecret, bytes) {
108
+ return ingestEncryptedAdvert(groupRendezvousKey(roomSecret), bytes)
114
109
  }
@@ -9,8 +9,10 @@ import { noteDiscoveryPeerClue } from '../peer_clue.mjs'
9
9
  import { getBtPeerHint } from './peer_hints.mjs'
10
10
  import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
11
11
 
12
- /** 重导出 waitPoweredOn,供 discovery 调用方使用。 */
13
- export { waitPoweredOn } from './runtime.mjs'
12
+ /**
13
+ * 重导出有关 BT 生命期的工具函数。
14
+ */
15
+ export { canUseBluetoothRuntime, waitPoweredOn }
14
16
 
15
17
  const BT_SERVICE_UUID = 'f017f017f017f017f017f017f017f017'
16
18
  const BT_CHARACTERISTIC_UUID = 'f017f017f017f017f017f017f017f018'
@@ -53,13 +55,6 @@ export function clearBtVisibleNodes() {
53
55
  visibleByHash.clear()
54
56
  }
55
57
 
56
- /**
57
- * @returns {Promise<boolean>} 可用为 true
58
- */
59
- export async function canUseBluetoothDiscovery() {
60
- return canUseBluetoothRuntime()
61
- }
62
-
63
58
  /**
64
59
  * @param {Map<string, Uint8Array>} presence nodeHash → encrypted advert
65
60
  * @returns {Buffer} JSON blob
@@ -100,9 +95,9 @@ function parsePresenceBlob(raw) {
100
95
  * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签结果
101
96
  */
102
97
  export async function acceptBtScannedPresence(bytes, meta) {
103
- const peripheralId = String(meta?.peripheralId || '').trim()
98
+ const peripheralId = String(meta?.peripheralId || '')
104
99
  if (!peripheralId || !bytes?.byteLength) return null
105
- const ingested = await ingestNetworkAdvert(bytes, meta)
100
+ const ingested = await ingestNetworkAdvert(bytes)
106
101
  if (!ingested) return null
107
102
  const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
108
103
  noteBtVisibleNode(ingested.verifiedNodeHash)
@@ -15,9 +15,8 @@ const hints = createTtlMap(BT_PEER_HINT_TTL_MS)
15
15
  */
16
16
  export function noteBtPeerHint(nodeHash, peripheralId) {
17
17
  const hash = normalizeHex64(nodeHash)
18
- const id = String(peripheralId || '').trim()
19
- if (!hash || !id) return
20
- hints.set(hash, { peripheralId: id })
18
+ if (!hash || !peripheralId) return
19
+ hints.set(hash, { peripheralId })
21
20
  }
22
21
 
23
22
  /**
@@ -96,7 +96,7 @@ export async function loadBleno() {
96
96
  if (probeBluetoothHardware() === false)
97
97
  throw new Error('p2p: no bluetooth adapter')
98
98
  const mod = await import('@stoprocent/bleno')
99
- if (typeof mod.withBindings === 'function') return mod.withBindings('default')
99
+ if (mod.withBindings) return mod.withBindings('default')
100
100
  return mod.default ?? mod
101
101
  }
102
102
 
@@ -27,7 +27,7 @@ export function setDiscoveryLinkDialer(dialer) {
27
27
  */
28
28
  export function registerDiscoveryProvider(provider) {
29
29
  if (!provider?.id) throw new Error('p2p: discovery provider requires id')
30
- providers.set(String(provider.id), provider)
30
+ providers.set(provider.id, provider)
31
31
  return () => unregisterDiscoveryProvider(provider.id)
32
32
  }
33
33
 
@@ -36,8 +36,8 @@ export function registerDiscoveryProvider(provider) {
36
36
  * @returns {void}
37
37
  */
38
38
  export function unregisterDiscoveryProvider(id) {
39
- const provider = providers.get(String(id))
40
- providers.delete(String(id))
39
+ const provider = providers.get(id)
40
+ providers.delete(id)
41
41
  try { provider?.dispose?.() } catch { /* ignore */ }
42
42
  }
43
43
 
@@ -59,7 +59,7 @@ export function listDiscoveryProviders() {
59
59
  * @returns {DiscoveryProvider | undefined} 对应 id 的提供者,未注册为 undefined
60
60
  */
61
61
  export function getDiscoveryProvider(id) {
62
- return providers.get(String(id))
62
+ return providers.get(id)
63
63
  }
64
64
 
65
65
  /**
@@ -67,17 +67,17 @@ export function getDiscoveryProvider(id) {
67
67
  * @returns {DiscoveryProvider[]} 实现了指定方法的提供者列表
68
68
  */
69
69
  function providersWith(method) {
70
- return listDiscoveryProviders().filter(provider => typeof provider[method] === 'function')
70
+ return listDiscoveryProviders().filter(provider => provider[method])
71
71
  }
72
72
 
73
73
  /**
74
- * @param {Array<(() => void) | null | undefined>} cleanups 清理函数
74
+ * @param {Array<() => void>} cleanups 清理函数
75
75
  * @returns {() => void} 调用时依次执行各 cleanup
76
76
  */
77
77
  function composeCleanups(cleanups) {
78
78
  return () => {
79
79
  for (const stop of cleanups)
80
- if (typeof stop === 'function') try { stop() } catch { /* ignore */ }
80
+ try { stop() } catch { /* ignore */ }
81
81
  }
82
82
  }
83
83
 
@@ -92,15 +92,15 @@ async function fanInProviderMethod(method, methodArguments, options = {}) {
92
92
  const requireAny = options.requireAny !== false
93
93
  const capable = providersWith(method)
94
94
  if (requireAny && !capable.length) throw new Error(options.errorLabel || `p2p: ${method} unavailable`)
95
- const cleanups = await Promise.all(capable.map(async provider => {
95
+ const cleanups = (await Promise.all(capable.map(async provider => {
96
96
  try {
97
97
  return await provider[method](...methodArguments)
98
98
  }
99
99
  catch {
100
100
  return null
101
101
  }
102
- }))
103
- if (requireAny && !cleanups.some(stop => typeof stop === 'function'))
102
+ }))).filter(Boolean)
103
+ if (requireAny && !cleanups.length)
104
104
  throw new Error(options.errorLabel || `p2p: ${method} unavailable`)
105
105
  return composeCleanups(cleanups)
106
106
  }
@@ -116,7 +116,7 @@ export async function listVisibleNodeHashes(options = {}) {
116
116
  /** @type {Record<string, string[]>} */
117
117
  const byProvider = {}
118
118
  for (const provider of listDiscoveryProviders()) {
119
- if (typeof provider.listVisibleNodeHashes !== 'function') continue
119
+ if (!provider.listVisibleNodeHashes) continue
120
120
  try {
121
121
  const hashes = await provider.listVisibleNodeHashes(options)
122
122
  byProvider[provider.id] = hashes.map(hash => shortHash(hash))
@@ -151,7 +151,7 @@ export async function listVisibleNodeHashes(options = {}) {
151
151
  export async function prepareConnectToNode(nodeHash, options = {}) {
152
152
  const hash = normalizeHex64(nodeHash)
153
153
  for (const provider of listDiscoveryProviders()) {
154
- if (typeof provider.connectToNode !== 'function') continue
154
+ if (!provider.connectToNode) continue
155
155
  try { await provider.connectToNode(hash, options) }
156
156
  catch { /* prepare next medium */ }
157
157
  }
@@ -304,7 +304,7 @@ export function noteVisibleNodeFromAdvert(verifiedNodeHash, options = {}) {
304
304
  */
305
305
  export async function watchVerifiedNodeAdvert(nodeHash, onAdvert) {
306
306
  return await watchNodeAdvert(nodeHash, async (bytes, meta) => {
307
- const ingested = await ingestNodeAdvert(nodeHash, bytes, meta)
307
+ const ingested = await ingestNodeAdvert(nodeHash, bytes)
308
308
  if (!ingested) return
309
309
  noteVisibleNodeFromAdvert(ingested.verifiedNodeHash)
310
310
  await Promise.resolve(onAdvert(ingested.verifiedNodeHash, ingested.body, meta))
@@ -319,7 +319,7 @@ export async function watchVerifiedNodeAdvert(nodeHash, onAdvert) {
319
319
  */
320
320
  export async function watchVerifiedGroupAdverts(roomSecret, onAdvert) {
321
321
  return await watchGroupAdverts(roomSecret, async (bytes, meta) => {
322
- const ingested = await ingestGroupAdvert(roomSecret, bytes, meta)
322
+ const ingested = await ingestGroupAdvert(roomSecret, bytes)
323
323
  if (!ingested) return
324
324
  noteVisibleNodeFromAdvert(ingested.verifiedNodeHash, { roomSecret })
325
325
  await Promise.resolve(onAdvert(ingested.verifiedNodeHash, ingested.body, meta))
package/discovery/lan.mjs CHANGED
@@ -59,7 +59,7 @@ export function clearLanVisibleNodes() {
59
59
  */
60
60
  export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
61
61
  if (!advertBytes?.byteLength) return null
62
- const ingested = await ingestNetworkAdvert(advertBytes, meta)
62
+ const ingested = await ingestNetworkAdvert(advertBytes)
63
63
  if (!ingested) return null
64
64
  const skipHash = meta.skipNodeHash ? normalizeHex64(meta.skipNodeHash) : null
65
65
  if (skipHash && ingested.verifiedNodeHash === skipHash) return ingested
@@ -68,7 +68,7 @@ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
68
68
  noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
69
69
  if (firstSeen) {
70
70
  noteDiscoveryPeerClue(ingested.verifiedNodeHash)
71
- const host = String(meta.address || '').trim()
71
+ const host = String(meta.address || '')
72
72
  nodeDebug('p2p:lan peer visible', {
73
73
  peer: shortHash(ingested.verifiedNodeHash),
74
74
  host: host || undefined,
@@ -85,7 +85,7 @@ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
85
85
  */
86
86
  export function createLanDiscoveryProvider(options = {}) {
87
87
  const port = Number(options.port) || DEFAULT_PORT
88
- const group = String(options.group || DEFAULT_GROUP)
88
+ const group = options.group || DEFAULT_GROUP
89
89
  /** @type {import('node:dgram').Socket | null} */
90
90
  let socket = null
91
91
  let bound = false