@steve02081504/fount-p2p 0.0.14 → 0.0.16

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 CHANGED
@@ -30,7 +30,9 @@ await ensureUserRoom() // slot + runtime only
30
30
  attachUserRoomDefaultWires({ replicaUsername: 'alice' }) // full business wires
31
31
  ```
32
32
 
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.
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
+
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).
34
36
 
35
37
  Public transport subpaths: `link_registry`, `user_room`, `group_link_set`, `node_scope`, `room_scopes`, `remote_user_room`. Other `transport/*` modules are internal.
36
38
 
@@ -44,6 +46,14 @@ npx @steve02081504/fount-p2p
44
46
 
45
47
  Default `nodeDir`: Windows `%LOCALAPPDATA%/fount-p2p/node`; elsewhere `~/.local/share/fount-p2p/node`.
46
48
 
49
+ Deno (optional; WebRTC needs explicit native scripts — **not** blanket `--allow-scripts`):
50
+
51
+ ```bash
52
+ deno run -A --minimum-dependency-age=0 --node-modules-dir=auto --allow-scripts=npm:node-datachannel npm:@steve02081504/fount-p2p
53
+ ```
54
+
55
+ See [docs/runtime.md](./docs/runtime.md).
56
+
47
57
  ```javascript
48
58
  import { initNode, startNode, startInfra, stopInfra, setInfraPriority } from '@steve02081504/fount-p2p'
49
59
 
@@ -78,7 +88,7 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
78
88
  | `user_room.mjs` / `group_link_set.mjs` / `node_scope.mjs` | rooms + composable node-scope wires |
79
89
  | `room_scopes.mjs` / `remote_user_room.mjs` | scope constants / remote user slot |
80
90
 
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).
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).
82
92
 
83
93
  Root contains only the facade and package metadata; all modules live in layered subdirectories.
84
94
 
@@ -91,7 +101,7 @@ npm run test:live # link / LAN / glare smoke
91
101
  npm run test:sim # tunables co-evolution sim (dev only, not published; --social-tunables to write back)
92
102
  ```
93
103
 
94
- During development: `node scripts/check-imports.mjs` validates relative imports. After a layout migration, `node scripts/cleanup-root-duplicates.mjs` removes stale root-level stubs.
104
+ During development: `node scripts/check-imports.mjs` validates relative imports; `node scripts/find-unused-exports.mjs` scans dead exports (`--fount <path>` optional).
95
105
 
96
106
  Maintainer notes for agents / contributors: [AGENTS.md](./AGENTS.md). Sim harness fidelity: [sim/AGENTS.md](./sim/AGENTS.md).
97
107
 
@@ -99,7 +109,7 @@ Maintainer notes for agents / contributors: [AGENTS.md](./AGENTS.md). Sim harnes
99
109
 
100
110
  - `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth (optionalDependencies). Hardware probe is subprocess-only; see [docs/runtime.md](./docs/runtime.md).
101
111
  - `node-datachannel` — WebRTC DataChannels (dependency).
102
- - `ws` — Nostr discovery/signaling WebSockets (dependency).
112
+ - `ws` — Nostr discovery / signaling / last-resort link WebSockets (dependency).
103
113
 
104
114
  Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStoragePlugin` and injected; see `node/storage_plugins.mjs` for the local reference implementation.
105
115
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Domain-key 信封:X25519 ECIES 包装与 AES-GCM 消息载荷(wire scheme 由消费方定义,如 ckg)。
2
+ * Domain-key 信封:X25519 ECIES 包装与 AES-GCM 消息载荷(wire scheme:channel-key)。
3
3
  * 解密 payload 不可脱离外层 DAG Ed25519 签名上下文单独传递或信任。
4
4
  */
5
5
  import { Buffer } from 'node:buffer'
@@ -7,8 +7,8 @@ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:cr
7
7
 
8
8
  import { unwrapKeyEcies, wrapKeyEcies } from './key.mjs'
9
9
 
10
- /** @type {'ckg'} 频道消息 content 加密 scheme */
11
- export const CKG_SCHEME = 'ckg'
10
+ /** @type {'channel-key'} 频道消息 content 加密 scheme */
11
+ export const CHANNEL_KEY_SCHEME = 'channel-key'
12
12
 
13
13
  /** @typedef {{ ephemPub: string, iv: string, ciphertext: string, authTag: string }} EciesWrapBlob */
14
14
 
@@ -49,7 +49,7 @@ function messageAesKey(channelKeyHex, channelId, generation) {
49
49
  return Buffer.from(hkdfSync(
50
50
  'sha256',
51
51
  Buffer.from(channelKeyHex, 'hex'),
52
- `ckg:${String(channelId)}:${String(generation)}`,
52
+ `${CHANNEL_KEY_SCHEME}:${String(channelId)}:${String(generation)}`,
53
53
  '',
54
54
  32,
55
55
  ))
@@ -60,7 +60,7 @@ function messageAesKey(channelKeyHex, channelId, generation) {
60
60
  * @param {string} channelKeyHex K_ch
61
61
  * @param {string} channelId 频道 ID
62
62
  * @param {number} generation 密钥代际
63
- * @returns {{ scheme: typeof CKG_SCHEME, channelId: string, generation: number, payload: string }} 频道密钥信封
63
+ * @returns {{ scheme: typeof CHANNEL_KEY_SCHEME, channelId: string, generation: number, payload: string }} 频道密钥信封
64
64
  */
65
65
  export function encryptWithChannelKey(plaintext, channelKeyHex, channelId, generation) {
66
66
  const key = messageAesKey(channelKeyHex, channelId, generation)
@@ -70,7 +70,7 @@ export function encryptWithChannelKey(plaintext, channelKeyHex, channelId, gener
70
70
  const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
71
71
  const authTag = cipher.getAuthTag()
72
72
  return {
73
- scheme: CKG_SCHEME,
73
+ scheme: CHANNEL_KEY_SCHEME,
74
74
  channelId: String(channelId),
75
75
  generation: Number(generation) || 0,
76
76
  payload: `${iv.toString('base64')}.${ciphertext.toString('base64')}.${authTag.toString('base64')}`,
@@ -84,7 +84,7 @@ export function encryptWithChannelKey(plaintext, channelKeyHex, channelId, gener
84
84
  * @returns {string | null} 明文 UTF-8
85
85
  */
86
86
  export function decryptWithChannelKey(envelope, channelKeyHex, channelId) {
87
- if (envelope?.scheme !== CKG_SCHEME || !envelope.payload) return null
87
+ if (envelope?.scheme !== CHANNEL_KEY_SCHEME || !envelope.payload) return null
88
88
  try {
89
89
  const parts = envelope.payload.split('.')
90
90
  if (parts.length !== 3) return null
package/deno.json CHANGED
@@ -1,4 +1,7 @@
1
1
  {
2
2
  "nodeModulesDir": "none",
3
- "lock": false
3
+ "lock": false,
4
+ "allowScripts": [
5
+ "npm:node-datachannel"
6
+ ]
4
7
  }
@@ -1,8 +1,6 @@
1
1
  import { normalizeHex64 } from '../core/hexIds.mjs'
2
2
  import { buildSignedAdvert, verifySignedAdvert } from '../link/handshake.mjs'
3
3
 
4
- import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
5
-
6
4
  import {
7
5
  decryptSignalPacket,
8
6
  encryptSignalPacket,
@@ -10,6 +8,7 @@ import {
10
8
  networkRendezvousKey,
11
9
  nodeRendezvousKey,
12
10
  } from './internal/signal_crypto.mjs'
11
+ import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
13
12
 
14
13
  /** @typedef {'node' | 'network' | { roomSecret: string }} AdvertScope */
15
14
 
@@ -4,6 +4,7 @@ import { isHex64, normalizeHex64 } from '../../core/hexIds.mjs'
4
4
  import { nodeDebug, shortHash } from '../../node/log.mjs'
5
5
  import { noteAdvertPeerHints } from '../advert_peer_hints.mjs'
6
6
  import { ingestNetworkAdvert } from '../adverts.mjs'
7
+ import { noteDiscoveryPeerClue } from '../peer_clue.mjs'
7
8
 
8
9
  import { getBtPeerHint } from './peer_hints.mjs'
9
10
  import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
@@ -106,11 +107,13 @@ export async function acceptBtScannedPresence(bytes, meta) {
106
107
  const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
107
108
  noteBtVisibleNode(ingested.verifiedNodeHash)
108
109
  noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
109
- if (firstSeen)
110
+ if (firstSeen) {
111
+ noteDiscoveryPeerClue(ingested.verifiedNodeHash)
110
112
  nodeDebug('p2p:bt peer visible', {
111
113
  peer: shortHash(ingested.verifiedNodeHash),
112
114
  peripheralId,
113
115
  })
116
+ }
114
117
  return ingested
115
118
  }
116
119
 
package/discovery/lan.mjs CHANGED
@@ -9,6 +9,7 @@ import { noteAdvertPeerHints } from './advert_peer_hints.mjs'
9
9
  import { ingestNetworkAdvert } from './adverts.mjs'
10
10
  import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
11
11
  import { getLanPeerHint } from './lan_peer_hints.mjs'
12
+ import { noteDiscoveryPeerClue } from './peer_clue.mjs'
12
13
 
13
14
  const DEFAULT_PORT = 53531
14
15
  const DEFAULT_GROUP = '239.255.42.99'
@@ -66,6 +67,7 @@ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
66
67
  noteLanVisibleNode(ingested.verifiedNodeHash)
67
68
  noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
68
69
  if (firstSeen) {
70
+ noteDiscoveryPeerClue(ingested.verifiedNodeHash)
69
71
  const host = String(meta.address || '').trim()
70
72
  nodeDebug('p2p:lan peer visible', {
71
73
  peer: shortHash(ingested.verifiedNodeHash),
@@ -78,7 +80,7 @@ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
78
80
 
79
81
  /**
80
82
  * LAN UDP presence:段内 beacon,非 topic 订阅模型。
81
- * @param {{ port?: number, group?: string }} [options] 配置
83
+ * @param {{ port?: number, group?: string, localNodeHash?: string }} [options] 配置
82
84
  * @returns {import('./index.mjs').DiscoveryProvider} LAN 发现提供者
83
85
  */
84
86
  export function createLanDiscoveryProvider(options = {}) {
@@ -91,12 +93,14 @@ export function createLanDiscoveryProvider(options = {}) {
91
93
  let refs = 0
92
94
  /** @type {ReturnType<typeof setInterval> | null} */
93
95
  let beaconTimer = null
96
+ const seededSelf = normalizeHex64(options.localNodeHash)
94
97
  /** @type {string | null} */
95
- let selfNodeHash = null
98
+ let selfNodeHash = isHex64(seededSelf) ? seededSelf : null
96
99
  /** @type {Set<string>} */
97
100
  const joinedAddresses = new Set()
98
101
 
99
102
  /**
103
+ * 在每个本机 IPv4 接口上加入组播组;全部失败则回退到默认接口。
100
104
  * @param {import('node:dgram').Socket} sock UDP socket
101
105
  * @returns {void}
102
106
  */
@@ -112,6 +116,11 @@ export function createLanDiscoveryProvider(options = {}) {
112
116
  if (!joinedAddresses.size)
113
117
  try { sock.addMembership(group) } catch { /* ignore */ }
114
118
  }
119
+
120
+ /**
121
+ * 懒创建复用的 udp4 socket(reuseAddr,便于同机多实例绑同一 port)。
122
+ * @returns {import('node:dgram').Socket} 组播 presence socket
123
+ */
115
124
  function getSocket() {
116
125
  return socket ||= dgram.createSocket({ type: 'udp4', reuseAddr: true })
117
126
  }
@@ -190,7 +199,7 @@ export function createLanDiscoveryProvider(options = {}) {
190
199
  let sent = 0
191
200
  /** @type {unknown} */
192
201
  let lastError = null
193
- for (const addr of addrs) {
202
+ for (const addr of addrs)
194
203
  try {
195
204
  await sendOnce(addr)
196
205
  sent++
@@ -198,7 +207,6 @@ export function createLanDiscoveryProvider(options = {}) {
198
207
  catch (error) {
199
208
  lastError = error
200
209
  }
201
- }
202
210
  if (!sent) throw lastError || new Error('p2p: lan multicast send failed')
203
211
  }
204
212