@steve02081504/fount-p2p 0.0.22 → 0.0.24

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 (71) hide show
  1. package/README.md +8 -6
  2. package/core/entity_id.mjs +5 -5
  3. package/core/logical_entity.mjs +1 -1
  4. package/core/object.mjs +9 -0
  5. package/core/partpath.mjs +10 -0
  6. package/core/random_id.mjs +4 -2
  7. package/crypto/crypto.mjs +26 -93
  8. package/dag/storage.mjs +0 -5
  9. package/dag/strip_extensions.mjs +1 -3
  10. package/{wire/part_query_cache.mjs → federation/part_query/cache.mjs} +7 -7
  11. package/{wire/part_query.mjs → federation/part_query/runtime.mjs} +36 -77
  12. package/files/assemble.mjs +5 -5
  13. package/files/assemble_stream.mjs +2 -2
  14. package/files/chunk/fetch.mjs +101 -0
  15. package/files/chunk/pending.mjs +59 -0
  16. package/files/{chunk_provider_registry.mjs → chunk/provider_registry.mjs} +4 -5
  17. package/files/chunk/responder.mjs +104 -0
  18. package/files/{chunk_store.mjs → chunk/store.mjs} +2 -2
  19. package/files/{chunk_fetch_verify.mjs → chunk/verify.mjs} +2 -2
  20. package/files/evfs.mjs +18 -19
  21. package/files/fed/fetch_shared.mjs +39 -0
  22. package/files/fed/responder.mjs +47 -0
  23. package/files/{acl.mjs → manifest/acl.mjs} +4 -4
  24. package/files/{manifest_acl_registry.mjs → manifest/acl_registry.mjs} +14 -17
  25. package/files/{manifest_fetch.mjs → manifest/fetch.mjs} +47 -52
  26. package/files/{manifest.mjs → manifest/normalize.mjs} +2 -2
  27. package/files/manifest/pending.mjs +53 -0
  28. package/files/{public_manifest.mjs → manifest/public.mjs} +12 -12
  29. package/files/transfer_key.mjs +2 -2
  30. package/files/transfer_key_registry.mjs +7 -7
  31. package/index.mjs +4 -4
  32. package/infra/service.mjs +1 -1
  33. package/link/handshake.mjs +0 -3
  34. package/mailbox/deliver_or_store.mjs +0 -2
  35. package/mailbox/parse.mjs +1 -1
  36. package/mailbox/store.mjs +0 -9
  37. package/mailbox/wire.mjs +24 -16
  38. package/node/reputation_sync.mjs +33 -29
  39. package/package.json +9 -8
  40. package/registries/inbound.mjs +19 -9
  41. package/registries/part_path.mjs +3 -8
  42. package/schemas/discovery.mjs +1 -1
  43. package/schemas/federation_pull.mjs +1 -1
  44. package/schemas/part_query.mjs +6 -5
  45. package/transport/group_link_set.mjs +2 -2
  46. package/transport/link_registry.mjs +2 -2
  47. package/transport/node_scope/features.mjs +166 -0
  48. package/transport/node_scope/wire.mjs +124 -0
  49. package/transport/remote_user_room.mjs +0 -1
  50. package/{rooms → transport}/scoped_link.mjs +1 -1
  51. package/transport/user_room.mjs +15 -28
  52. package/utils/fetch_wait.mjs +98 -0
  53. package/wire/adapter.mjs +13 -0
  54. package/wire/ingress.mjs +1 -8
  55. package/wire/part/fanout.mjs +99 -0
  56. package/wire/part/group.mjs +45 -0
  57. package/wire/{part_ingress.mjs → part/ingress.mjs} +58 -67
  58. package/wire/{part_invoke.mjs → part/invoke.mjs} +1 -22
  59. package/wire/part/pending.mjs +32 -0
  60. package/wire/part/query.mjs +91 -0
  61. package/wire/subscribe.mjs +13 -0
  62. package/federation/chunk_fetch_pending.mjs +0 -124
  63. package/federation/manifest_fetch_pending.mjs +0 -88
  64. package/files/chunk_fetch.mjs +0 -99
  65. package/files/chunk_responder.mjs +0 -142
  66. package/transport/advert_ingest.mjs +0 -17
  67. package/transport/node_scope.mjs +0 -289
  68. package/wire/group_part.mjs +0 -43
  69. package/wire/part_common.mjs +0 -55
  70. package/wire/part_fanout.mjs +0 -52
  71. /package/{wire → schemas}/part_query.tunables.json +0 -0
package/README.md CHANGED
@@ -21,20 +21,20 @@ Requires **Node.js ≥ 20** (ESM + `import ... with { type: 'json' }`).
21
21
  import {
22
22
  startNode,
23
23
  ensureUserRoom,
24
- attachUserRoomDefaultWires,
24
+ attachNodeScopeDefaultFeatures,
25
25
  createScopedLinkRoom,
26
26
  } from '@steve02081504/fount-p2p'
27
27
 
28
28
  await startNode({ nodeDir: '/path/to/p2p/node' })
29
29
  await ensureUserRoom() // slot + runtime only
30
- attachUserRoomDefaultWires({ replicaUsername: 'alice' }) // full business wires
30
+ attachNodeScopeDefaultFeatures({ replicaUsername: 'alice' }) // full business wires
31
31
  ```
32
32
 
33
33
  Shells talk to the **fount network** (`ensureLinkToNode` / `sendToNodeLink` / rooms). Do not import `link/providers/*` or choose WebRTC / BLE / LAN / Nostr yourself. Provider registration: `registerLinkProvider` from `@steve02081504/fount-p2p/link` or the facade.
34
34
 
35
35
  Dial order is descending link **`level`**: `lan_tcp` → `webrtc` → `ble_gatt` → `nostr` (−∞ last resort). Discovery **`priority`** only orders handshake / presence media. Details: [docs/transports.md](./docs/transports.md).
36
36
 
37
- Public transport subpaths: `link_registry`, `user_room`, `group_link_set`, `node_scope`, `room_scopes`, `remote_user_room`. Other `transport/*` modules are internal.
37
+ Public transport subpaths: `link_registry`, `user_room`, `group_link_set`, `node_scope`, `room_scopes`, `remote_user_room`, `scoped_link`. Other `transport/*` modules are internal.
38
38
 
39
39
  ## Infra relay (optional)
40
40
 
@@ -73,7 +73,7 @@ Reputation pull/apply is separate: `pullReputationFromNode` → JSON; `setReputa
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` |
76
- | L3 | `discovery/`, `link/`, `transport/`, `rooms/` | Discovery + fount-network registry/rooms (`./link` = provider registration only) |
76
+ | L3 | `discovery/`, `link/`, `transport/` | Discovery + fount-network registry/rooms (`./link` = provider registration only) |
77
77
  | L4 | `trust_graph/`, `mailbox/`, `dag/`, `federation/`, `files/`, `governance/`, `reputation/` | Federation, store-and-forward, DAG, EVFS, tunables |
78
78
  | — | `infra/` | Optional public-good relay (`startInfra` / CLI) |
79
79
  | — | `registries/` | Pluggable registries (event type, part path, room provider, …) |
@@ -85,10 +85,10 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
85
85
  | Module | Role |
86
86
  |---|---|
87
87
  | `link_registry.mjs` | fount-network facade: dial fallback, scope/overlay |
88
- | `user_room.mjs` / `group_link_set.mjs` / `node_scope.mjs` | rooms + composable node-scope wires |
88
+ | `user_room.mjs` / `group_link_set.mjs` / `node_scope/` | rooms + composable node-scope wires |
89
89
  | `room_scopes.mjs` / `remote_user_room.mjs` | scope constants / remote user slot |
90
90
 
91
- `runtime_bootstrap`, `offer_answer`, `advert_ingest` are **internal** (transport). Signal crypto / rendezvous live under `discovery/internal/signal_crypto.mjs` (used by discovery `nostr.mjs` / `adverts.mjs`; not a package export). The Nostr **link** provider (`link/providers/nostr.mjs`) reuses the same discovery signal path (`type: 'link'`) as a last-resort duplex pipe. `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).
91
+ `runtime_bootstrap`, `offer_answer` are **internal** (transport). Signal crypto / rendezvous live under `discovery/internal/signal_crypto.mjs` (used by discovery `nostr.mjs` / `adverts.mjs`; not a package export). The Nostr **link** provider (`link/providers/nostr.mjs`) reuses the same discovery signal path (`type: 'link'`) as a last-resort duplex pipe. `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).
92
92
 
93
93
  Root contains only the facade and package metadata; all modules live in layered subdirectories.
94
94
 
@@ -121,3 +121,5 @@ Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStora
121
121
  - Signaling and WebRTC glare: [`docs/signaling.md`](./docs/signaling.md)
122
122
  - Runtime bootstrap / BT probe: [`docs/runtime.md`](./docs/runtime.md)
123
123
  - Infra relay / node-scope attaches: [`docs/infra.md`](./docs/infra.md)
124
+ - Wire part / fanout attaches: [`docs/wire.md`](./docs/wire.md)
125
+ - EVFS public manifests / fetch: [`docs/evfs.md`](./docs/evfs.md)
@@ -1,7 +1,6 @@
1
- import { Buffer } from 'node:buffer'
2
-
3
1
  import { pubKeyHash } from '../crypto/crypto.mjs'
4
2
 
3
+ import { hexToBytes } from './bytes_codec.mjs'
5
4
  import {
6
5
  encodeEntityHash,
7
6
  ENTITY_HASH_RE,
@@ -26,9 +25,10 @@ export {
26
25
  */
27
26
  export function hashFromPubKeyHex(pubKeyHex) {
28
27
  const hex = normalizeHex64(pubKeyHex)
29
- if (!isHex64(hex) || Buffer.from(hex, 'hex').length !== 32)
30
- throw new Error('invalid pubKeyHex')
31
- return pubKeyHash(Buffer.from(hex, 'hex'))
28
+ if (!isHex64(hex)) throw new Error('invalid pubKeyHex')
29
+ const bytes = hexToBytes(hex)
30
+ if (bytes.length !== 32) throw new Error('invalid pubKeyHex')
31
+ return pubKeyHash(bytes)
32
32
  }
33
33
 
34
34
  /**
@@ -1,6 +1,6 @@
1
1
  import { sha256TextHex } from '../crypto/crypto.mjs'
2
2
 
3
- import { encodeEntityHash, parseEntityHash } from './entity_id.mjs'
3
+ import { encodeEntityHash, parseEntityHash } from './entity_id_parse.mjs'
4
4
 
5
5
  /** @type {string} 逻辑实体 sentinel nodeHash(非物理节点绑定) */
6
6
  export const LOGICAL_ENTITY_SENTINEL_NODE_HASH = '0'.repeat(64)
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 判定 `{}` 形态对象(原型为 Object.prototype)。
3
+ * 数组、Date/Map/类实例、null 原型对象一律否。
4
+ * @param {unknown} value 待判定值
5
+ * @returns {value is Record<string, unknown>} 是否为普通对象
6
+ */
7
+ export function isPlainObject(value) {
8
+ return value != null && Object.getPrototypeOf(value) === Object.prototype
9
+ }
@@ -0,0 +1,10 @@
1
+ const PARTPATH_RE = /^[\w-]+(?:\/[\w-]+)*$/
2
+
3
+ /**
4
+ * 入站 partpath 形校验(不改写)。仅用于非本机网络边界。
5
+ * @param {unknown} value 原始 partpath
6
+ * @returns {string | null} 合法则原样返回;否则 null
7
+ */
8
+ export function parsePartpath(value) {
9
+ return typeof value === 'string' && PARTPATH_RE.test(value) ? value : null
10
+ }
@@ -1,9 +1,11 @@
1
- import { randomUUID } from 'node:crypto'
1
+ /**
2
+ * 跨端随机 ID(Web Crypto / Node globalThis.crypto)。
3
+ */
2
4
 
3
5
  /**
4
6
  * @param {string} prefix ID 前缀(如 channel_)
5
7
  * @returns {string} 带前缀的随机 ID
6
8
  */
7
9
  export function prefixedRandomId(prefix) {
8
- return `${prefix}${randomUUID().replace(/-/g, '')}`
10
+ return `${prefix}${globalThis.crypto.randomUUID().replace(/-/g, '')}`
9
11
  }
package/crypto/crypto.mjs CHANGED
@@ -1,105 +1,65 @@
1
- import { Buffer } from 'node:buffer'
2
- import {
3
- createHash,
4
- createPrivateKey,
5
- createPublicKey,
6
- randomBytes,
7
- sign as nodeSign,
8
- verify as nodeVerify,
9
- } from 'node:crypto'
10
-
11
- /** 固定 DER 头(非秘密),让 Node 把 32 字节种子当成私钥 */
12
- const PKCS8_RAW_SEED_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex')
13
- /** 固定 DER 头(非秘密),让 Node 把 32 字节当成公钥 */
14
- const SPKI_RAW_PUB_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
15
-
16
1
  /**
17
- * @param {Uint8Array|Buffer} seed32 32 字节私钥种子
18
- * @returns {import('node:crypto').KeyObject} Node 私钥对象
2
+ * 跨端 Ed25519 / SHA-256(Node 与浏览器共用;不依赖 node:crypto)。
19
3
  */
20
- function privateKeyFromSeed(seed32) {
21
- return createPrivateKey({
22
- key: Buffer.concat([PKCS8_RAW_SEED_PREFIX, Buffer.from(seed32).subarray(0, 32)]),
23
- format: 'der',
24
- type: 'pkcs8',
25
- })
26
- }
4
+ import { ed25519 } from '@noble/curves/ed25519.js'
5
+ import { sha256 } from '@noble/hashes/sha2.js'
27
6
 
28
- /**
29
- * @param {Uint8Array|Buffer} rawPublicKey 32 字节公钥
30
- * @returns {import('node:crypto').KeyObject} Node 公钥对象
31
- */
32
- function publicKeyFromRaw(rawPublicKey) {
33
- return createPublicKey({
34
- key: Buffer.concat([SPKI_RAW_PUB_PREFIX, Buffer.from(rawPublicKey).subarray(0, 32)]),
35
- format: 'der',
36
- type: 'spki',
37
- })
38
- }
7
+ import { bytesToHex, toBytes } from '../core/bytes_codec.mjs'
39
8
 
40
9
  /**
41
- * @param {Uint8Array|Buffer} seed32 32 字节私钥种子
42
- * @returns {Uint8Array} 32 字节公钥
10
+ * @param {Uint8Array|ArrayBufferView|ArrayBuffer|string} data 字节或 UTF-8 文本
11
+ * @returns {Uint8Array} 规范化字节
43
12
  */
44
- function rawPublicKeyFromSeed(seed32) {
45
- const der = createPublicKey(privateKeyFromSeed(seed32)).export({ type: 'spki', format: 'der' })
46
- return new Uint8Array(der.subarray(-32))
13
+ function inputBytes(data) {
14
+ if (typeof data === 'string') return new TextEncoder().encode(data)
15
+ return toBytes(data)
47
16
  }
48
17
 
49
18
  /**
50
- * 由种子字节派生密钥对
51
- *
52
- * @param {Uint8Array|Buffer} seed 任意长度;非 32 字节时 sha256 派生
19
+ * @param {Uint8Array|ArrayBufferView|ArrayBuffer} seed 任意长度;非 32 字节时 sha256 派生
53
20
  * @returns {{ publicKey: Uint8Array, secretKey: Uint8Array }} 32 字节私钥与对应公钥
54
21
  */
55
22
  export function keyPairFromSeed(seed) {
56
- const u = seed instanceof Uint8Array ? seed : new Uint8Array(seed)
57
- const sk = u.length === 32 ? u : new Uint8Array(createHash('sha256').update(u).digest())
58
- return { publicKey: rawPublicKeyFromSeed(sk), secretKey: new Uint8Array(sk) }
23
+ const u = toBytes(seed)
24
+ const sk = u.length === 32 ? u : sha256(u)
25
+ return { publicKey: ed25519.getPublicKey(sk), secretKey: new Uint8Array(sk) }
59
26
  }
60
27
 
61
28
  /**
62
- * 随机生成密钥对
63
- *
64
29
  * @returns {Promise<{ publicKey: Uint8Array, secretKey: Uint8Array }>} 随机密钥对
65
30
  */
66
31
  export async function randomKeyPair() {
67
- const sk = randomBytes(32)
32
+ const sk = new Uint8Array(32)
33
+ globalThis.crypto.getRandomValues(sk)
68
34
  return keyPairFromSeed(sk)
69
35
  }
70
36
 
71
37
  /**
72
- * 由 32 字节私钥种子导出对应公钥
73
38
  * @param {Uint8Array} secretKey 私钥种子
74
39
  * @returns {Uint8Array} 公钥
75
40
  */
76
41
  export function publicKeyFromSeed(secretKey) {
77
- return rawPublicKeyFromSeed(secretKey)
42
+ return ed25519.getPublicKey(toBytes(secretKey).subarray(0, 32))
78
43
  }
79
44
 
80
45
  /**
81
- * 签名
82
- *
83
- * @param {Uint8Array} message 待签名消息字节
46
+ * @param {Uint8Array|ArrayBufferView|ArrayBuffer|string} message 待签名消息
84
47
  * @param {Uint8Array} secretKey 私钥(取前 32 字节为种子)
85
48
  * @returns {Promise<Uint8Array>} 64 字节签名
86
49
  */
87
50
  export async function sign(message, secretKey) {
88
- const sig = nodeSign(null, Buffer.from(message), privateKeyFromSeed(secretKey))
89
- return new Uint8Array(sig)
51
+ return ed25519.sign(inputBytes(message), toBytes(secretKey).subarray(0, 32))
90
52
  }
91
53
 
92
54
  /**
93
- * 校验签名
94
- *
95
- * @param {Uint8Array} signature 64 字节签名
96
- * @param {Uint8Array} message 原始消息字节
97
- * @param {Uint8Array} publicKey 公钥
55
+ * @param {Uint8Array|ArrayBufferView|ArrayBuffer} signature 64 字节签名
56
+ * @param {Uint8Array|ArrayBufferView|ArrayBuffer|string} message 原始消息
57
+ * @param {Uint8Array|ArrayBufferView|ArrayBuffer} publicKey 公钥
98
58
  * @returns {Promise<boolean>} 合法为 true;异常或失败为 false
99
59
  */
100
60
  export async function verify(signature, message, publicKey) {
101
61
  try {
102
- return nodeVerify(null, Buffer.from(message), publicKeyFromRaw(publicKey), Buffer.from(signature))
62
+ return ed25519.verify(toBytes(signature), inputBytes(message), toBytes(publicKey))
103
63
  }
104
64
  catch {
105
65
  return false
@@ -107,52 +67,25 @@ export async function verify(signature, message, publicKey) {
107
67
  }
108
68
 
109
69
  /**
110
- * 公钥的 sha256 十六进制指纹(小写、无 0x 前缀)
111
- *
112
- * @param {Uint8Array} publicKey 公钥字节
70
+ * @param {Uint8Array|ArrayBufferView|ArrayBuffer} publicKey 公钥字节
113
71
  * @returns {string} 64 字符 hex
114
72
  */
115
73
  export function pubKeyHash(publicKey) {
116
- return bufferToHexSimple(hashPubKeyBytes(publicKey))
117
- }
118
-
119
- /**
120
- * 公钥 sha256 摘要
121
- *
122
- * @param {Uint8Array} publicKey 公钥字节
123
- * @returns {Buffer} 32 字节 digest
124
- */
125
- function hashPubKeyBytes(publicKey) {
126
- return createHash('sha256').update(publicKey).digest()
74
+ return bytesToHex(sha256(toBytes(publicKey)))
127
75
  }
128
76
 
129
77
  /**
130
- * 任意输入的 SHA-256 十六进制(小写、无 0x 前缀)。
131
- *
132
- * @param {Uint8Array|Buffer|string} data 字节或 UTF-8 文本
78
+ * @param {Uint8Array|ArrayBufferView|ArrayBuffer|string} data 字节或 UTF-8 文本
133
79
  * @returns {string} 64 字符 hex
134
80
  */
135
81
  export function sha256Hex(data) {
136
- const input = typeof data === 'string' ? Buffer.from(data, 'utf8') : Buffer.from(data)
137
- return createHash('sha256').update(input).digest('hex')
82
+ return bytesToHex(sha256(inputBytes(data)))
138
83
  }
139
84
 
140
85
  /**
141
- * UTF-8 文本 SHA-256 十六进制(小写、无 0x 前缀)。
142
- *
143
86
  * @param {string} text 文本
144
87
  * @returns {string} 64 字符 hex
145
88
  */
146
89
  export function sha256TextHex(text) {
147
90
  return sha256Hex(String(text ?? ''))
148
91
  }
149
-
150
- /**
151
- * Buffer / Uint8Array → 小写 hex 字符串
152
- *
153
- * @param {Uint8Array|Buffer} buffer 二进制缓冲
154
- * @returns {string} 小写十六进制文本
155
- */
156
- function bufferToHexSimple(buffer) {
157
- return Buffer.from(buffer).toString('hex')
158
- }
package/dag/storage.mjs CHANGED
@@ -9,11 +9,6 @@ import { pipeline } from 'node:stream/promises'
9
9
  import { withAsyncMutex } from '../utils/async_mutex.mjs'
10
10
  import { atomicTemporaryPath, finalizeAtomicRename } from '../utils/atomic_fs.mjs'
11
11
 
12
- /**
13
- * 原子重命名 finalize(re-export)。
14
- */
15
- export { finalizeAtomicRename } from '../utils/atomic_fs.mjs'
16
-
17
12
  /** 流式重写 JSONL 时分块写入的行数上限 */
18
13
  const WRITE_JSONL_CHUNK_LINES = 1000
19
14
 
@@ -1,17 +1,15 @@
1
1
  /**
2
2
  * DAG 事件行读盘时剥离的本地扩展键(不得进入验签域 / 联邦 wire)。
3
3
  */
4
- import { isPlainObject } from '../wire/ingress.mjs'
5
4
 
6
5
  /** 落盘后 trusted 读路径仍须剥除的 sidecar 键。 */
7
6
  export const DAG_EVENT_LOCAL_EXTENSION_KEYS = new Set(['receivedAt', 'isRemote'])
8
7
 
9
8
  /**
10
- * @param {unknown} row JSONL 行
9
+ * @param {object} row JSONL 行
11
10
  * @returns {object} 剥离扩展键后的副本
12
11
  */
13
12
  export function stripDagEventLocalExtensions(row) {
14
- if (!isPlainObject(row)) return row
15
13
  const out = { ...row }
16
14
  for (const key of DAG_EVENT_LOCAL_EXTENSION_KEYS) delete out[key]
17
15
  return out
@@ -1,8 +1,7 @@
1
- import { sha256Hex } from '../crypto/crypto.mjs'
2
- import { normalizePartQueryCacheMaterial } from '../schemas/part_query.mjs'
3
- import { createLruMap } from '../utils/lru.mjs'
4
-
5
- import partQueryTunables from './part_query.tunables.json' with { type: 'json' }
1
+ import { sha256Hex } from '../../crypto/crypto.mjs'
2
+ import { normalizePartQueryCacheMaterial } from '../../schemas/part_query.mjs'
3
+ import partQueryTunables from '../../schemas/part_query.tunables.json' with { type: 'json' }
4
+ import { createLruMap } from '../../utils/lru.mjs'
6
5
 
7
6
  /**
8
7
  * @typedef {{ rows: unknown[], storedAt: number }} PartQueryCacheEntry
@@ -71,13 +70,14 @@ export function createPartQueryCache(options = {}) {
71
70
  * @param {string} partpath part 路径
72
71
  * @param {string} kind 查询标签
73
72
  * @param {unknown} query 查询体
74
- * @param {unknown[]} rows 聚合 rows
73
+ * @param {unknown[]} rows 聚合 rows(空数组不入库)
75
74
  * @param {number} [now=Date.now()] 当前时间
76
75
  * @returns {void}
77
76
  */
78
77
  set(partpath, kind, query, rows, now = Date.now()) {
79
78
  const key = partQueryCacheKey(partpath, kind, query)
80
- if (!key || !rows) return
79
+ // miss 不缓存:mesh 晚就绪 / 超时早查询不应被长 TTL 负缓存粘住
80
+ if (!key || !Array.isArray(rows) || rows.length === 0) return
81
81
  sweep(now)
82
82
  map.touch(key, {
83
83
  rows: rows.slice(0, maxHits),
@@ -1,29 +1,27 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
 
3
- import { createDedupeSlot } from '../federation/dedupe_slot.mjs'
4
- import { getNodeHash } from '../node/identity.mjs'
5
- import { loadReputation } from '../node/reputation_store.mjs'
6
- import { isQuarantinedPure } from '../reputation/engine.mjs'
3
+ import { compositeKey } from '../../core/composite_key.mjs'
4
+ import { getNodeHash } from '../../node/identity.mjs'
5
+ import { loadReputation } from '../../node/reputation_store.mjs'
6
+ import { isQuarantinedPure } from '../../reputation/engine.mjs'
7
7
  import {
8
8
  clampPartQueryRows,
9
9
  parsePartQueryReq,
10
- parsePartQueryRes,
11
- } from '../schemas/part_query.mjs'
12
- import { sendToNodeLink } from '../transport/link_registry.mjs'
13
- import { buildMergedGraph } from '../trust_graph/build.mjs'
14
- import { pickTopFromGraph } from '../trust_graph/engine.mjs'
15
- import { resolveFederationFanoutTopK } from '../trust_graph/resolve.mjs'
16
- import trustGraphTunables from '../trust_graph/tunables.json' with { type: 'json' }
17
-
18
- import { isPlainObject } from './ingress.mjs'
19
- import partQueryTunables from './part_query.tunables.json' with { type: 'json' }
20
- import { createPartQueryCache, partQueryCache } from './part_query_cache.mjs'
21
- import { consumeWireRateBucket } from './rate_bucket.mjs'
22
- import { finishMultiWireWaiters, registerMultiWireWait } from './wait.mjs'
23
-
24
- /** @typedef {import('../schemas/part_query.mjs').PartQueryReq} PartQueryReq */
25
- /** @typedef {import('../schemas/part_query.mjs').PartQueryRes} PartQueryRes */
26
- /** @typedef {import('./part_ingress.mjs').PartWireAdapter} PartWireAdapter */
10
+ } from '../../schemas/part_query.mjs'
11
+ import partQueryTunables from '../../schemas/part_query.tunables.json' with { type: 'json' }
12
+ import { buildMergedGraph } from '../../trust_graph/build.mjs'
13
+ import { pickTopFromGraph } from '../../trust_graph/engine.mjs'
14
+ import { resolveFederationFanoutTopK } from '../../trust_graph/resolve.mjs'
15
+ import trustGraphTunables from '../../trust_graph/tunables.json' with { type: 'json' }
16
+ import { finishMultiWireWaiters, registerMultiWireWait } from '../../wire/wait.mjs'
17
+ import { createDedupeSlot } from '../dedupe_slot.mjs'
18
+
19
+ import { createPartQueryCache, partQueryCache } from './cache.mjs'
20
+
21
+ /** @typedef {import('../../schemas/part_query.mjs').PartQueryReq} PartQueryReq */
22
+ /** @typedef {import('../../schemas/part_query.mjs').PartQueryRes} PartQueryRes */
23
+
24
+ /** @typedef {import('../../wire/adapter.mjs').WireAdapter} PartQueryWire */
27
25
 
28
26
  /**
29
27
  * @typedef {{
@@ -41,7 +39,7 @@ import { finishMultiWireWaiters, registerMultiWireWait } from './wait.mjs'
41
39
  * @typedef {{
42
40
  * takeDedupe: (key: string) => boolean
43
41
  * relayPending: Map<string, RelayPending>
44
- * originWaits: Map<string, Map<string, import('./wait.mjs').WireWaiter[]>>
42
+ * originWaits: Map<string, Map<string, import('../../wire/wait.mjs').WireWaiter[]>>
45
43
  * originBags: Map<string, { rows: unknown[], maxHits: number, expected: number, received: number, respondedPeers: Set<string>, rowKey?: (row: unknown) => string }>
46
44
  * cache: ReturnType<typeof createPartQueryCache>
47
45
  * handlers: Map<string, QueryInboundHandler>
@@ -61,7 +59,7 @@ import { finishMultiWireWaiters, registerMultiWireWait } from './wait.mjs'
61
59
  /**
62
60
  * @typedef {{
63
61
  * upstreamPeerId: string
64
- * wire: PartWireAdapter
62
+ * wire: PartQueryWire
65
63
  * request: PartQueryReq
66
64
  * localRows: unknown[]
67
65
  * remoteRows: unknown[]
@@ -100,7 +98,7 @@ const defaultState = createPartQueryNodeState({ cache: partQueryCache })
100
98
  * @param {PartQueryDependencies} [dependencies] 依赖
101
99
  * @returns {PartQueryNodeState} 节点状态
102
100
  */
103
- function resolveState(dependencies = {}) {
101
+ export function resolvePartQueryState(dependencies = {}) {
104
102
  return dependencies.state || defaultState
105
103
  }
106
104
 
@@ -110,7 +108,7 @@ function resolveState(dependencies = {}) {
110
108
  * @returns {string} handler 键
111
109
  */
112
110
  function handlerKey(partpath, kind) {
113
- return `${partpath}\0${kind}`
111
+ return compositeKey(partpath, kind)
114
112
  }
115
113
 
116
114
  /**
@@ -122,7 +120,7 @@ function handlerKey(partpath, kind) {
122
120
  * @returns {void}
123
121
  */
124
122
  export function registerQueryInboundHandler(partpath, kind, handler, state = defaultState) {
125
- state.handlers.set(handlerKey(String(partpath || '').trim(), String(kind || '').trim()), handler)
123
+ state.handlers.set(handlerKey(partpath, kind), handler)
126
124
  }
127
125
 
128
126
  /**
@@ -202,17 +200,15 @@ async function selectQueryNeighbors(username, exclude, dependencies) {
202
200
  }
203
201
 
204
202
  /**
205
- * @param {string} username 用户
206
203
  * @param {string} nodeHash 目标
207
204
  * @param {string} action action 名
208
205
  * @param {unknown} payload 载荷
209
206
  * @param {PartQueryDependencies} dependencies 依赖
210
207
  * @returns {Promise<boolean>} 是否发出
211
208
  */
212
- async function deliverQuery(username, nodeHash, action, payload, dependencies) {
213
- void username
214
- if (dependencies.deliver) return Boolean(await dependencies.deliver(nodeHash, action, payload))
215
- return sendToNodeLink(nodeHash, { scope: 'node', action, payload })
209
+ async function deliverQuery(nodeHash, action, payload, dependencies) {
210
+ if (!dependencies.deliver) return false
211
+ return Boolean(await dependencies.deliver(nodeHash, action, payload))
216
212
  }
217
213
 
218
214
  /**
@@ -254,14 +250,14 @@ function flushRelayPending(pending) {
254
250
 
255
251
  /**
256
252
  * @param {{ replicaUsername?: string }} wireContext attach 上下文
257
- * @param {PartWireAdapter} wire wire
253
+ * @param {PartQueryWire} wire wire
258
254
  * @param {PartQueryReq} request 已校验请求
259
255
  * @param {string} peerId 来路
260
256
  * @param {PartQueryDependencies} dependencies 依赖
261
257
  * @returns {Promise<void>}
262
258
  */
263
- async function processIncomingRequest(wireContext, wire, request, peerId, dependencies) {
264
- const state = resolveState(dependencies)
259
+ export async function processIncomingPartQueryRequest(wireContext, wire, request, peerId, dependencies) {
260
+ const state = resolvePartQueryState(dependencies)
265
261
  const nodeHashOf = dependencies.getNodeHash || getNodeHash
266
262
  const now = dependencies.now || Date.now
267
263
  const username = String(wireContext.replicaUsername || '')
@@ -311,7 +307,7 @@ async function processIncomingRequest(wireContext, wire, request, peerId, depend
311
307
 
312
308
  let sent = 0
313
309
  for (const target of neighbors)
314
- if (await deliverQuery(username, target, 'part_query_req', forwardPayload, dependencies)) sent++
310
+ if (await deliverQuery(target, 'part_query_req', forwardPayload, dependencies)) sent++
315
311
  pending.expected = sent
316
312
 
317
313
  if (sent === 0 || pending.received >= pending.expected) {
@@ -322,39 +318,6 @@ async function processIncomingRequest(wireContext, wire, request, peerId, depend
322
318
  pending.timer = setTimeout(() => flushRelayPending(pending), resolvePartQueryHopTimeoutMs(request.ttl))
323
319
  }
324
320
 
325
- /**
326
- * 挂载 part_query_req / part_query_res。
327
- * @param {{ replicaUsername?: string }} wireContext 入站上下文
328
- * @param {PartWireAdapter} wire wire
329
- * @param {PartQueryDependencies} [dependencies] 可注入依赖(含 per-node state)
330
- * @returns {() => void} 取消挂载的 dispose
331
- */
332
- export function attachPartQueryWire(wireContext, wire, dependencies = {}) {
333
- const state = resolveState(dependencies)
334
- const offs = [
335
- wire.on('part_query_req', (data, peerId) => {
336
- if (!isPlainObject(data)) return
337
- const request = parsePartQueryReq(data)
338
- if (!request) return
339
- if (!state.takeDedupe(request.requestId)) return
340
- const source = String(peerId || request.originNodeHash || '').trim().toLowerCase()
341
- if (source && !consumeWireRateBucket(`part_query:${source}`, {
342
- maxCount: partQueryTunables.ratePerSourcePerMin,
343
- })) return
344
- void processIncomingRequest(wireContext, wire, request, String(peerId || ''), dependencies)
345
- }),
346
- wire.on('part_query_res', (data, peerId) => {
347
- const response = parsePartQueryRes(data)
348
- if (!response) return
349
- handleIncomingPartQueryResponse(response, String(peerId || ''), dependencies)
350
- }),
351
- ]
352
- return () => {
353
- for (const off of offs)
354
- try { off?.() } catch { /* ignore */ }
355
- }
356
- }
357
-
358
321
  /**
359
322
  * @param {PartQueryRes} response 响应
360
323
  * @param {string} peerId 来路
@@ -362,8 +325,7 @@ export function attachPartQueryWire(wireContext, wire, dependencies = {}) {
362
325
  * @returns {void}
363
326
  */
364
327
  export function handleIncomingPartQueryResponse(response, peerId = '', dependencies = {}) {
365
- const state = resolveState(dependencies)
366
- // 同一 peer 只计一次,防重复回包灌水/提早凑齐 expected
328
+ const state = resolvePartQueryState(dependencies)
367
329
  const responderKey = String(peerId || response.fromNodeHash || '').trim().toLowerCase()
368
330
  const relay = state.relayPending.get(response.requestId)
369
331
  if (relay) {
@@ -405,7 +367,7 @@ export function handleIncomingPartQueryResponse(response, peerId = '', dependenc
405
367
  * @returns {Promise<unknown[]>} 合并后的 rows
406
368
  */
407
369
  export async function queryNetwork(username, partpath, kind, query, options = {}) {
408
- const state = resolveState(options)
370
+ const state = resolvePartQueryState(options)
409
371
  const now = options.now || Date.now
410
372
  const nodeHashOf = options.getNodeHash || getNodeHash
411
373
 
@@ -420,7 +382,6 @@ export async function queryNetwork(username, partpath, kind, query, options = {}
420
382
  partQueryTunables.maxHits,
421
383
  Math.max(1, Math.floor(Number(options.maxHits ?? options.budget?.maxHits) || partQueryTunables.maxHits)),
422
384
  )
423
- // 第一跳中继等待 hopTimeout(ttl) 才 flush,发起端默认取 ttl+1 档以免先行超时
424
385
  const timeoutMs = Math.max(
425
386
  1,
426
387
  Math.floor(Number(options.timeoutMs) || resolvePartQueryHopTimeoutMs(ttl + 1)),
@@ -435,8 +396,8 @@ export async function queryNetwork(username, partpath, kind, query, options = {}
435
396
  const request = {
436
397
  requestId: randomUUID(),
437
398
  originNodeHash: nodeHashOf(),
438
- partpath: String(partpath || '').trim(),
439
- kind: String(kind || '').trim(),
399
+ partpath,
400
+ kind,
440
401
  query,
441
402
  ttl,
442
403
  budget: { maxHits },
@@ -444,7 +405,6 @@ export async function queryNetwork(username, partpath, kind, query, options = {}
444
405
  const parsed = parsePartQueryReq(request)
445
406
  if (!parsed) return mergeQueryRows([localRows], maxHits, options.rowKey)
446
407
 
447
- // 预占 dedupe:若查询绕环回流到本机,入站侧直接丢弃
448
408
  state.takeDedupe(parsed.requestId)
449
409
 
450
410
  const bag = {
@@ -463,10 +423,9 @@ export async function queryNetwork(username, partpath, kind, query, options = {}
463
423
  const neighbors = await selectQueryNeighbors(username, exclude, options)
464
424
  let sent = 0
465
425
  for (const target of neighbors)
466
- if (await deliverQuery(username, target, 'part_query_req', parsed, options)) sent++
426
+ if (await deliverQuery(target, 'part_query_req', parsed, options)) sent++
467
427
  bag.expected = sent
468
428
 
469
- // deliver 可能同步回流;在赋值 expected 后再检查是否已齐
470
429
  if (sent === 0 || bag.received >= bag.expected)
471
430
  finishMultiWireWaiters(state.originWaits, parsed.requestId, '')
472
431
  await waitPromise
@@ -10,7 +10,7 @@ import {
10
10
  wrapContentKey,
11
11
  } from '../crypto/key.mjs'
12
12
 
13
- import { normalizeFileManifest, publicTransferKeyDescriptor } from './manifest.mjs'
13
+ import { normalizeFileManifest, publicTransferKeyDescriptor } from './manifest/normalize.mjs'
14
14
 
15
15
  /** @type {Record<string, (plain: Buffer) => { contentHash: string, ciphertextHash: string, raw: Buffer, contentKey?: Buffer }>} */
16
16
  const ENCRYPTION_STRATEGIES = {
@@ -35,8 +35,8 @@ const ENCRYPTION_STRATEGIES = {
35
35
  }
36
36
 
37
37
  /**
38
- * @typedef {import('./manifest.mjs').FileManifest} FileManifest
39
- * @typedef {import('./manifest.mjs').CeMode} CeMode
38
+ * @typedef {import('./manifest/normalize.mjs').FileManifest} FileManifest
39
+ * @typedef {import('./manifest/normalize.mjs').CeMode} CeMode
40
40
  */
41
41
 
42
42
  /**
@@ -154,7 +154,7 @@ export async function encryptPlaintextToMultiPartsAsync(plaintext, ceMode = 'con
154
154
  * @param {string} [parameters.name] 文件名
155
155
  * @param {string} [parameters.mimeType] MIME
156
156
  * @param {CeMode} [parameters.ceMode] 加密模式
157
- * @param {import('./manifest.mjs').TransferKeyDescriptor} [parameters.transferKeyDescriptor] 传递密钥
157
+ * @param {import('./manifest/normalize.mjs').TransferKeyDescriptor} [parameters.transferKeyDescriptor] 传递密钥
158
158
  * @param {object} [parameters.meta] 元数据
159
159
  * @returns {FileManifest} manifest(未写盘)
160
160
  */
@@ -224,7 +224,7 @@ export function buildFileManifestFromEnc(parameters, enc) {
224
224
  * @param {string} fileId 文件 ID
225
225
  * @param {Buffer} contentKey 随机密钥
226
226
  * @param {Buffer | string} H vault H
227
- * @returns {import('./manifest.mjs').TransferKeyDescriptor} 传输密钥描述
227
+ * @returns {import('./manifest/normalize.mjs').TransferKeyDescriptor} 传输密钥描述
228
228
  */
229
229
  export function vaultWrapDescriptor(entityHash, fileId, contentKey, H) {
230
230
  return {