@steve02081504/fount-p2p 0.0.31 → 0.0.33
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/AGENTS.md +79 -0
- package/README.md +1 -1
- package/discovery/nostr.mjs +11 -0
- package/docs/evfs.md +32 -0
- package/docs/infra.md +25 -0
- package/docs/mesh.md +67 -0
- package/docs/reputation.md +22 -0
- package/docs/runtime.md +74 -0
- package/docs/signaling.md +33 -0
- package/docs/transports.md +92 -0
- package/docs/wire.md +21 -0
- package/index.mjs +2 -0
- package/link/providers/nostr.mjs +2 -12
- package/link/providers/webrtc.mjs +1 -1
- package/link/rtc/polyfill.mjs +1 -1
- package/node/instance.mjs +7 -3
- package/node/signaling_config.mjs +52 -29
- package/package.json +6 -10
- package/sim/AGENTS.md +29 -0
- package/sim/admission.mjs +56 -0
- package/sim/anomaly.mjs +39 -0
- package/sim/apply.mjs +77 -0
- package/sim/attack_space.mjs +113 -0
- package/sim/attacks.mjs +614 -0
- package/sim/behavior.mjs +78 -0
- package/sim/cli.mjs +282 -0
- package/sim/coevolution.mjs +245 -0
- package/sim/cold_start.md +13 -0
- package/sim/constants.mjs +44 -0
- package/sim/discovery.mjs +259 -0
- package/sim/duration.mjs +64 -0
- package/sim/graph_adj.mjs +28 -0
- package/sim/integrity.mjs +58 -0
- package/sim/metrics.mjs +423 -0
- package/sim/model.mjs +1176 -0
- package/sim/optimizer.mjs +250 -0
- package/sim/propagation.mjs +57 -0
- package/sim/report.mjs +176 -0
- package/sim/reputation_social.tunables.json +6 -0
- package/sim/rng.mjs +62 -0
- package/sim/scenarios.mjs +345 -0
- package/sim/sim_pool.mjs +169 -0
- package/sim/sim_worker.mjs +31 -0
- package/sim/social_reputation.mjs +30 -0
- package/sim/space.mjs +312 -0
- package/sim/test/admission.test.mjs +25 -0
- package/sim/test/attack_space.test.mjs +39 -0
- package/sim/test/behavior_vector.test.mjs +37 -0
- package/sim/test/coevolution.test.mjs +32 -0
- package/sim/test/cold_start.test.mjs +46 -0
- package/sim/test/defense.test.mjs +320 -0
- package/sim/test/discovery.test.mjs +68 -0
- package/sim/test/duration.test.mjs +20 -0
- package/sim/test/fidelity.test.mjs +151 -0
- package/sim/test/integrity.test.mjs +56 -0
- package/sim/test/optimizer.test.mjs +24 -0
- package/sim/test/propagation.test.mjs +29 -0
- package/sim/test/sim_model_regressions.test.mjs +110 -0
- package/sim/test/slow_drip.test.mjs +28 -0
- package/sim/test/smoke.test.mjs +52 -0
- package/sim/test/transport.test.mjs +55 -0
- package/sim/test/tunables_apply.test.mjs +64 -0
- package/sim/test/tunables_param_space.test.mjs +74 -0
- package/sim/test/vulnerability.test.mjs +127 -0
- package/sim/transport.mjs +135 -0
- package/sim/tunables_bundle.mjs +71 -0
- package/sim/vulnerability.mjs +532 -0
- package/transport/link_registry.mjs +7 -0
- package/transport/runtime_bootstrap.mjs +120 -103
- package/deno.json +0 -7
package/AGENTS.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# P2P / Federation / Entity Files Guide
|
|
2
|
+
|
|
3
|
+
## Package layers (`@steve02081504/fount-p2p`)
|
|
4
|
+
|
|
5
|
+
| Layer | Directory | Role |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| L0 | `core/` | IDs, logical entity, canonical JSON, bytes |
|
|
8
|
+
| L1 | `crypto/`, `wire/`, `schemas/` | Crypto, wire ingress, canonical validation |
|
|
9
|
+
| L2 | `node/` | `initNode`, identity, entity store, denylist, reputation, storage plugins |
|
|
10
|
+
| L3 | `discovery/`, `link/`, `transport/` | fount network (registry/rooms); `registerLinkProvider` via `./link` or facade |
|
|
11
|
+
| L4 | `trust_graph/`, `mailbox/`, `dag/`, `federation/`, `files/`, `governance/`, `reputation/` | Federation, store-and-forward, DAG, EVFS, tunables |
|
|
12
|
+
|
|
13
|
+
**Outside the package** (shell / frontend; p2p must not import): chat/social semantics, mention rendering, entity identity provisioning, etc. Standalone clients: `import { startNode } from '@steve02081504/fount-p2p'`.
|
|
14
|
+
|
|
15
|
+
**Facade:** `index.mjs`; subpath exports mirror directories.
|
|
16
|
+
|
|
17
|
+
Detail docs: [transports](docs/transports.md) · [mesh](docs/mesh.md) · [signaling](docs/signaling.md) · [runtime](docs/runtime.md) · [infra](docs/infra.md) · [wire](docs/wire.md) · [evfs](docs/evfs.md) · [reputation](docs/reputation.md)
|
|
18
|
+
|
|
19
|
+
### Runtime: isomorphic vs Node
|
|
20
|
+
|
|
21
|
+
| Surface | Runtime | Notes |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `core/*` | Node + browser | No `node:*` builtins |
|
|
24
|
+
| `crypto/crypto.mjs` | Node + browser | `@noble/hashes` + `@noble/curves` only — never `node:crypto` |
|
|
25
|
+
| `crypto/key.mjs` / `crypto/channel.mjs`, disk I/O, LAN/BT, `ws`, CLI / `startNode` | Node (+ Deno bridge) | Do not load the whole package via esm.sh in the browser |
|
|
26
|
+
|
|
27
|
+
Deno / native / BT: [runtime.md](docs/runtime.md).
|
|
28
|
+
|
|
29
|
+
## Conventions
|
|
30
|
+
|
|
31
|
+
- Prefer shared helpers under `utils/`, `core/`, `wire/subscribe`, `wire/adapter` — do not reimplement LRU/TTL/inflight/atomic-fs/shuffle.
|
|
32
|
+
- **No pure-forward aliases:** do not add `fooText`/`fooAlias` that only `return foo(sameArgs)` when the callee already accepts those types (e.g. never wrap `sha256Hex` as `sha256TextHex`). Domain names must add logic or type narrowing, not just rename.
|
|
33
|
+
- **Heterogeneous backends:** normalize at the load boundary (e.g. `link/rtc/ice_local_hostname.mjs` wraps W3C RTC backends); call sites speak one contract.
|
|
34
|
+
- **File naming:** parent directory is scope — short child names. Tunables default `<dir>/tunables.json` (exception: `schemas/part_query.tunables.json`). Subpath `package.json` exports mirror filenames.
|
|
35
|
+
- **Import boundary:** `test/integration/p2p_shell_import_guard.test.mjs`.
|
|
36
|
+
- **No scattered `trim` / `toLowerCase`:** hex IDs must already be lowercase; `normalizeHex64` strips an optional `0x` only — mixed case / whitespace is rejected. Exceptions: JSONL blank lines, SDP fingerprint, CLI/`scripts` parsing.
|
|
37
|
+
- **No `String(x)` / `x || ''` on typed `string`:** if `@param {string}`, use it directly; `String(...)` / `|| ''` / `?? ''` only at optional / `unknown` / disk / inbound boundaries, or number→string.
|
|
38
|
+
- **Optional methods:** `if (fn) return await fn(...)` / `if (fn) …` — never `typeof x === 'function'`.
|
|
39
|
+
|
|
40
|
+
## Tests / tools
|
|
41
|
+
|
|
42
|
+
- `npm test` — pure + integration (Node; `--test-force-exit`)
|
|
43
|
+
- `npm run test:live` — live link / LAN smoke
|
|
44
|
+
- `npm run test:fount` — cross-repo Deno bridge (`test/fount/`); see [runtime.md](docs/runtime.md)
|
|
45
|
+
- `npm run test:sim` — tunables co-evolution (dev-only; [sim/AGENTS.md](sim/AGENTS.md))
|
|
46
|
+
- `node scripts/check-imports.mjs` — relative import check
|
|
47
|
+
- `node scripts/find-unused-exports.mjs` — dead-export scan (`--fount <path>` optional)
|
|
48
|
+
- Assertions: `test/helpers/assert.mjs`
|
|
49
|
+
- Fixed-seed identity: `test/helpers/identity.mjs`
|
|
50
|
+
- Mock discovery: `test/helpers/mock_discovery.mjs`
|
|
51
|
+
|
|
52
|
+
## Hard rules
|
|
53
|
+
|
|
54
|
+
### Trust / ingress
|
|
55
|
+
|
|
56
|
+
- **Untrusted ingress only:** discovery adverts/signals, link/overlay envelopes, group federation frames, `remoteIngest`, `part_timeline_*` / `part_invoke`, `part_query_*`, public manifest (`fed_manifest_data`). Validate / `canonicalize*` / `verifySignedPublicManifest` **only** here.
|
|
57
|
+
- **Trusted after disk:** from `events.jsonl`, only `stripDagEventLocalExtensions` — no re-canonicalization upstream.
|
|
58
|
+
- **Fanout vs targeted / timed collect:** [wire.md](docs/wire.md).
|
|
59
|
+
- **Channel encryption:** per-channel `K_ch`, scheme `channel-key` (`CHANNEL_KEY_SCHEME`); decrypted payloads are untrusted outside DAG Ed25519 context.
|
|
60
|
+
- **Denylist vs personal lists:** node `denylist.json` vs per-entity `personal_block.json` / `personal_hide.json`.
|
|
61
|
+
- **Manifest ACL / transfer owner:** shells register matchers; core does not hard-code chat/social types.
|
|
62
|
+
|
|
63
|
+
### Node / network
|
|
64
|
+
|
|
65
|
+
- **Node data:** `initNode({ nodeDir, entityStore? })` — `node.json`, `network.json`, `denylist.json`, `reputation.json`, `mailbox/`, `chunks/`. Default EntityStore: `{nodeDir}/entities/`. No `FOUNT_*` env knobs — subprocess IPC uses argv.
|
|
66
|
+
- **fount network:** shells use `startNode` / `ensureLinkToNode` / `sendToNodeLink` / rooms — never import `link/` internals or pick a transport. Providers: `registerLinkProvider` from `./link` or facade.
|
|
67
|
+
- **Link `level` vs discovery `priority`:** descending `level` picks data transport (`nostr` = −∞ last resort); ascending `priority` orders handshake/presence media only. [transports.md](docs/transports.md)
|
|
68
|
+
- **Mesh first / no versioning:** ≥N links (K acquaintances + N−K explore); discovery API is `listVisibleNodeHashes` + `connectToNode` only; no topic on the fount-network surface; no version/compat fields. [mesh.md](docs/mesh.md)
|
|
69
|
+
- **Room / registry:** `configureLinkRegistry(opts)` before first `getLinkRegistry`. `startNode` does not take registry options. `createGroupLinkSet` is the kernel; `createScopedLinkRoom` is a dial-all preset. Rooms call `registry.ensureRuntime()` before subscribe/advertise — [runtime.md](docs/runtime.md)
|
|
70
|
+
- **Fetch ≠ apply:** `ingestEncryptedAdvert` vs `noteAdvertPeerHints`; reputation pull never writes — [reputation.md](docs/reputation.md). Public-manifest cache/fanout: [evfs.md](docs/evfs.md). Node-scope attaches: [infra.md](docs/infra.md).
|
|
71
|
+
|
|
72
|
+
## Tunables JSON
|
|
73
|
+
|
|
74
|
+
| File | Directory |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `tunables.json` | `reputation/`, `trust_graph/`, `mailbox/`, `governance/`, `dag/` |
|
|
77
|
+
| `part_query.tunables.json` | `schemas/` |
|
|
78
|
+
|
|
79
|
+
Sim harness: `sim/tunables_bundle.mjs` (dev-only). See [sim/AGENTS.md](sim/AGENTS.md).
|
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
|
|
|
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 |
|
|
90
90
|
|
|
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).
|
|
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. Call `ensureChannelAvailable('bt')` to await BT discovery readiness. `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
|
|
package/discovery/nostr.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import WebSocket from 'ws'
|
|
|
6
6
|
import { base64ToBytes, hexToBytes, bytesToBase64, bytesToHex } from '../core/bytes_codec.mjs'
|
|
7
7
|
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
8
8
|
import { sha256Hex } from '../crypto/crypto.mjs'
|
|
9
|
+
import { getNodeTransportSettings } from '../node/identity.mjs'
|
|
10
|
+
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
9
11
|
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
10
12
|
|
|
11
13
|
import { noteAdvertPeerHints } from './advert_peer_hints.mjs'
|
|
@@ -200,6 +202,15 @@ export function mergeSignalingRelayUrls(userRelayUrls) {
|
|
|
200
202
|
return merged.length ? merged : [...DEFAULT_RELAY_URLS]
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
/**
|
|
206
|
+
* 当前可用 nostr 中继:nostr 通道配置 relay(替换默认),否则节点默认 + 公共中继。
|
|
207
|
+
* @returns {string[]} relay URL 列表
|
|
208
|
+
*/
|
|
209
|
+
export function resolveNostrRelayUrls() {
|
|
210
|
+
const relay = getSignalingRuntimeConfig().channels?.nostr?.relay
|
|
211
|
+
return relay?.length ? relay : mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
|
|
212
|
+
}
|
|
213
|
+
|
|
203
214
|
/**
|
|
204
215
|
* @param {number} kind 事件 kind
|
|
205
216
|
* @param {string[][]} tags 事件标签
|
package/docs/evfs.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# EVFS public manifests & fetch
|
|
2
|
+
|
|
3
|
+
Day-to-day package rules: [AGENTS.md](../AGENTS.md). Implementation: `files/` (`evfs`, `evfs_ref`, `chunk/*`, `manifest/*`).
|
|
4
|
+
|
|
5
|
+
## Storage
|
|
6
|
+
|
|
7
|
+
| What | Path |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Ciphertext chunks (CAS) | `{nodeDir}/chunks/` |
|
|
10
|
+
| Manifests | `{EntityStoreRoot}/{entityHash}/files/{path}.manifest.json` |
|
|
11
|
+
|
|
12
|
+
## Public publish / verify
|
|
13
|
+
|
|
14
|
+
- `publishPublicFile` signs with the recovery key.
|
|
15
|
+
- Remote path: `fed_manifest_get` → `verifySignedPublicManifest` → cache.
|
|
16
|
+
- Signature covers **content fields only**. After verify, drop incoming `meta` except `publicSig`.
|
|
17
|
+
- Profile / avatar meaning belongs in the shell — not in this package.
|
|
18
|
+
|
|
19
|
+
## `fetchPublicManifest`
|
|
20
|
+
|
|
21
|
+
| Case | Behavior |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Default options | No cache write |
|
|
24
|
+
| Local hit with `publicSig` | Return immediately; fanout revalidates in the background. With `cache: true`, write only when remote `publishedAt` is newer |
|
|
25
|
+
| Cold miss | Await fanout |
|
|
26
|
+
| Same key in flight | Deduped via `utils/inflight_table` |
|
|
27
|
+
|
|
28
|
+
Outer caller timeouts must **not** abort the in-flight work — background fill continues after the caller gives up.
|
|
29
|
+
|
|
30
|
+
## ACL
|
|
31
|
+
|
|
32
|
+
Shells register matchers via the ACL registry. Core does not hard-code chat/social entity types.
|
package/docs/infra.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Infra relay & node-scope attaches
|
|
2
|
+
|
|
3
|
+
Optional public-good overlay + mailbox. Day-to-day rules: [AGENTS.md](../AGENTS.md). Wire part attaches: [wire.md](wire.md).
|
|
4
|
+
|
|
5
|
+
## `startInfra` / `stopInfra`
|
|
6
|
+
|
|
7
|
+
Call after `initNode`. CLI: `npx @steve02081504/fount-p2p`.
|
|
8
|
+
|
|
9
|
+
- **Connectivity debug:** CLI default on; `--quiet` off. Non-CLI: `setConnectivityDebug(true)`.
|
|
10
|
+
- **Priority:** `setInfraPriority({ useLocalReputation })` reads local `reputation.json` only. `stopInfra` resets priority config so the next start does not inherit a ghost weight.
|
|
11
|
+
- **Reputation pull/export is separate:** `pullReputationFromNode` → JSON; `setReputationTable` to apply. Infra does **not** attach `rep_sync`. Donor must `attachReputationSyncWire()` (+ export allowlist); pull side auto-attaches.
|
|
12
|
+
- **`lockReputationMax` / `unlockReputationMax`:** unlock restores the pre-lock score.
|
|
13
|
+
- **`stopInfra` scope:** releases only its own attach refs, restores `maxActive`, clears rate/debug/priority weight.
|
|
14
|
+
|
|
15
|
+
## Node-scope attaches
|
|
16
|
+
|
|
17
|
+
Composable attaches under `transport/node_scope/` — import concrete modules, no directory barrel:
|
|
18
|
+
|
|
19
|
+
- `transport/node_scope/wire` — `ensureNodeScope`, `registerNodeScopeWireHook` (shell custom actions when wire is ready), helpers
|
|
20
|
+
- `transport/node_scope/features` — `attachNodeScopeMailbox` / `Part` / `PartQuery` / `Chunks`, `attachNodeScopeDefaultFeatures`, `attachNodeScopeFeature`
|
|
21
|
+
|
|
22
|
+
Each attach returns `dispose` and is **refcount-shared** (infra + default features can both hold mailbox without double handlers).
|
|
23
|
+
|
|
24
|
+
- `ensureUserRoom` is **slot + runtime only** (default `attachDefaultWires: false`).
|
|
25
|
+
- Full preset: `attachNodeScopeDefaultFeatures()` or `ensureUserRoom({ attachDefaultWires: true })`.
|
package/docs/mesh.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Mesh policy
|
|
2
|
+
|
|
3
|
+
A fount node's **first job is to join and stay on the fount network**. Link presence is not business trust: discovery and dialing stay aggressive; trust and fanout are constrained by reputation / TrustGraph / denylist.
|
|
4
|
+
|
|
5
|
+
Do **not** treat "unrelated ⇒ no interconnect" or "no common group ⇒ no dial" as design premises. Nodes with no acquaintances, no shared groups, and no shell intro must still reach the network via discovery media.
|
|
6
|
+
|
|
7
|
+
No versioning — see [transports.md](transports.md).
|
|
8
|
+
|
|
9
|
+
## Discovery: no topic on the fount-network surface
|
|
10
|
+
|
|
11
|
+
Each discovery / scannable medium exposes only:
|
|
12
|
+
|
|
13
|
+
1. `listVisibleNodeHashes({ limit? })` → `nodeHash[]` visible on that medium
|
|
14
|
+
2. `connectToNode(nodeHash)` → dial via registry (`ensureLinkToNode`)
|
|
15
|
+
|
|
16
|
+
Scans with `roomSecret` return **only that group's visible pool**. Media without group semantics (LAN / BT) return `[]` — do not pour network/LAN nodes into group membership.
|
|
17
|
+
|
|
18
|
+
Topic, relay tags, rendezvous keys, and signal crypto live **only** under `discovery/`. Group flows pass `roomSecret` and other business keys; transport only sees `nodeHash` / opaque bytes.
|
|
19
|
+
|
|
20
|
+
Visible pools (Nostr / BT / LAN) only accept **signature-verified** adverts; forged `body.nodeHash` after decrypt alone does not enter the pool. BT scan also records `peripheralId` via `noteAdvertPeerHints` so `connectToNode` / `ble_gatt` can dial.
|
|
21
|
+
|
|
22
|
+
Group presence / advert watch / signaling fan-in **all** providers that implement the methods — do not hard-bind `nostr` id.
|
|
23
|
+
|
|
24
|
+
No discovery-layer mDNS. LAN UDP presence must carry signed+encrypted network `advertBytes`; unsigned `nodeHash` beacons are ignored. WebRTC ICE `.local` host-candidate filtering is `iceLocalHostnamePolicy` (unrelated to discovery).
|
|
25
|
+
|
|
26
|
+
| Medium | Scan | Dial |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| Nostr | Internal rendezvous; surface is hash list only | Internal signal + trigger link |
|
|
29
|
+
| lan_tcp | Segment presence/beacon (not topic) | TCP dial |
|
|
30
|
+
| BT | Near-field scan | GATT / near-field assist |
|
|
31
|
+
|
|
32
|
+
## Keep-alive: N links, K + (N−K)
|
|
33
|
+
|
|
34
|
+
After `ensureRuntime`, aim for at least **N** active peer links (subject to `maxActive` / routing profile — "no acquaintances" is never an excuse for N=0):
|
|
35
|
+
|
|
36
|
+
| Slot | Count | Source | Selection |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| Acquaintance | **K** (`K ≤ N`) | `trustedPeers` / high reputation / recent stable peers | High confidence first |
|
|
39
|
+
| Explore | **N−K** | `listVisibleNodeHashes` + PEX/hints | Continuous try; rotate on failure |
|
|
40
|
+
|
|
41
|
+
Explore keeps the node reachable when acquaintances are offline. Stable explore peers (tunable `meshPromoteStableMs`) promote via `promoteExplorePeer` into `trustedPeers`.
|
|
42
|
+
|
|
43
|
+
`transport/mesh_keepalive.mjs`: explore eviction when full; acquaintance rebalance may kick explore slots; proactive close does not sticky re-dial; inbound non-acquaintance peers are tagged explore.
|
|
44
|
+
|
|
45
|
+
### K = 0 (new node / empty trust table)
|
|
46
|
+
|
|
47
|
+
**N−K = N**: automatically `listVisibleNodeHashes` + `connectToNode` on each medium — do not idle waiting for the shell to inject friends.
|
|
48
|
+
|
|
49
|
+
## Trust boundary (linked ≠ trusted)
|
|
50
|
+
|
|
51
|
+
- **Dial / keep-alive / overlay:** connectivity first.
|
|
52
|
+
- **Federation fanout, reputation seeding:** TrustGraph / reputation / denylist; explore slots default low trust.
|
|
53
|
+
- **Inbound payloads:** untrusted ingress — see [AGENTS.md](../AGENTS.md).
|
|
54
|
+
|
|
55
|
+
## Sparse group linking (orthogonal)
|
|
56
|
+
|
|
57
|
+
Within a **known member set**, `group_link_set` dials sparsely for budget (`selectLinkTargetsFromMembers` within `resolveFederationPoolLimits`: top trusted + explore, denylist/quarantine filtered, **anchors always included**). Member discovery uses `listVisibleNodeHashes({ roomSecret })` + `connectToNode` — **not** `subscribe(groupTopic)`.
|
|
58
|
+
|
|
59
|
+
`start()` dials once; membership changes debounce via `notePeerCandidate` (dial newly selected only; never proactive cut — `trimToBudget` is the backstop). This is a budget choice on the member graph, not a ban on linking outside the group.
|
|
60
|
+
|
|
61
|
+
On a sparse group mesh, first-seen valid events forward to federation targets minus sender. Relaying is not a reputation penalty.
|
|
62
|
+
|
|
63
|
+
## See also
|
|
64
|
+
|
|
65
|
+
- [transports.md](transports.md)
|
|
66
|
+
- [signaling.md](signaling.md)
|
|
67
|
+
- [runtime.md](runtime.md)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Subjective reputation
|
|
2
|
+
|
|
3
|
+
Day-to-day package rules: [AGENTS.md](../AGENTS.md). Tunables: `reputation/tunables.json`. Sim co-evolution: [sim/AGENTS.md](../sim/AGENTS.md).
|
|
4
|
+
|
|
5
|
+
## Storage
|
|
6
|
+
|
|
7
|
+
One global score per peer at `{nodeDir}/reputation.json`.
|
|
8
|
+
|
|
9
|
+
## Slash / anti-Sybil
|
|
10
|
+
|
|
11
|
+
- **Subjective slash:** `subjectiveSlashPenalty` — influence scales with sender trust.
|
|
12
|
+
- **Anti-Sybil:** `applyDecayCollusionAfterSlash` after slash / kick / ban.
|
|
13
|
+
- **Safe penalties:** self-observed attributable signals only.
|
|
14
|
+
|
|
15
|
+
## Do not add
|
|
16
|
+
|
|
17
|
+
- Penalties for merely relaying invalid events
|
|
18
|
+
- Penalties for RPC timeouts or empty responses
|
|
19
|
+
|
|
20
|
+
## Fetch ≠ apply
|
|
21
|
+
|
|
22
|
+
`pullReputationFromNode` returns JSON only — never writes. Apply via `setReputationTable` (or equivalent). Infra does not attach `rep_sync` by default — see [infra.md](infra.md).
|
package/docs/runtime.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Runtime bootstrap & lifecycle
|
|
2
|
+
|
|
3
|
+
`ensureRuntime`, startup/shutdown budgets, and Bluetooth hardware probe. Day-to-day shell rules: [AGENTS.md](../AGENTS.md). Providers: [transports.md](transports.md). Mesh N/K: [mesh.md](mesh.md).
|
|
4
|
+
|
|
5
|
+
## `ensureRuntime` contract
|
|
6
|
+
|
|
7
|
+
Returns after registering lan / nostr discovery providers (subject to the signaling `channels` whitelist), registering all enabled link providers (including `ble_gatt`), and scheduling background warm — does **not** await lan_tcp listen, Nostr relays, or BT availability.
|
|
8
|
+
|
|
9
|
+
| Who waits | For what |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Shells (`startNode` / `ensureUserRoom`) | Nothing beyond `ensureRuntime` itself — never read `lanTcpPort` or await public-signaling warm-up |
|
|
12
|
+
| `buildLocalAdvert` / `whenListening` | Local lan_tcp listen only |
|
|
13
|
+
| `ensureLinkToNode` | `whenSignalListening` (Nostr `listenNodeSignals` attached) before dial, so offer/answer does not drop the first signal |
|
|
14
|
+
| `ensureChannelAvailable('bt')` | BT discovery provider registered and BT signal/presence fan-in restarted |
|
|
15
|
+
|
|
16
|
+
Nostr / LAN hooks are progressive. The `bt` discovery provider is **not** registered by `ensureRuntime`; it is registered on demand via `ensureChannelAvailable('bt')`.
|
|
17
|
+
|
|
18
|
+
## `ensureChannelAvailable(channel)` contract
|
|
19
|
+
|
|
20
|
+
Await this to make a specific channel available:
|
|
21
|
+
|
|
22
|
+
- `bt` — probes the Bluetooth runtime, registers the `bt` discovery provider if available, and restarts the signal/presence fan-in so BT participates.
|
|
23
|
+
- `lan` — awaits `whenListening` (local lan_tcp listen).
|
|
24
|
+
- `nostr` — awaits `whenSignalListening` (Nostr `listenNodeSignals` attached).
|
|
25
|
+
- `webrtc` — awaits `whenSignalListening` (the shared signal medium WebRTC depends on).
|
|
26
|
+
|
|
27
|
+
`startNode` does not call `ensureChannelAvailable` automatically; shells that need BT discovery should `await getLinkRegistry().ensureChannelAvailable('bt')` (or the top-level `ensureChannelAvailable('bt')` export) after `startNode`.
|
|
28
|
+
|
|
29
|
+
### Fast-listen & dial path
|
|
30
|
+
|
|
31
|
+
- Fast-listen skips providers with `caps.probe: 'native'` (webrtc/ble) — never call their `isAvailable()` on the startup path.
|
|
32
|
+
- Dial path: `canReach` then `isAvailable` per provider (do not `await listAvailableLinkProviders()` first).
|
|
33
|
+
|
|
34
|
+
### Regression budgets
|
|
35
|
+
|
|
36
|
+
| Check | Bound | Test |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| Cold `ensureRuntime` | ≤50ms | `test/pure/startup_budget.test.mjs` |
|
|
39
|
+
| Warm `ensureRuntime` | ≤5ms | same |
|
|
40
|
+
| init → shutdown → natural exit | ≤10s | `test/pure/shutdown_exit.test.mjs` |
|
|
41
|
+
| 10s warm → shutdown → exit | ≤2s | same |
|
|
42
|
+
|
|
43
|
+
Shutdown-exit tests use the production path (default public Nostr + lan). Do not use per-channel relay / dead-relay crutches there.
|
|
44
|
+
|
|
45
|
+
## Nostr cleanup
|
|
46
|
+
|
|
47
|
+
Use the `ws` package (not global `WebSocket`) — platform WebSocket lacks force-drop / unref needed for clean shutdown. Subscriptions share one WebSocket per relay URL (signal / network advert / per-node advert / group advert multiplex `REQ`s). Active subscriptions reconnect after drop (`NOSTR_RECONNECT_DELAY_MS`) and re-send `REQ`s. On intentional shutdown: `close()`, then `terminate()` after `NOSTR_CLOSE_GRACE_MS` (1s) if the socket is not yet `CLOSED`. Presence publish still uses short-lived sockets.
|
|
48
|
+
|
|
49
|
+
Self presence echo from relays is filtered (`skipNodeHash`, same idea as LAN) and omitted from `listVisibleNodeHashes`. First-seen peer clues notify the link registry (`noteDiscoveryPeerClue`) so dial cooldown unlocks when a peer reappears.
|
|
50
|
+
|
|
51
|
+
## Deno vs Node native addons
|
|
52
|
+
|
|
53
|
+
| Surface | Node | Deno |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| Package tests / production CLI | `node` / `npx @steve02081504/fount-p2p` | Not primary |
|
|
56
|
+
| fount bridge (`npm run test:fount`) | — | `deno.json` keeps `"nodeModulesDir": "none"` |
|
|
57
|
+
| WebRTC | `node-datachannel` when native loads; else pure-JS `node-rtc-connection` (Termux/Android skips native) | same; native needs local `node_modules` + scripts for **only** `node-datachannel` |
|
|
58
|
+
| BLE (`noble` / `bleno`) | optionalDependencies; lazy-loaded | do **not** blanket `--allow-scripts` (optional native builds can abort the whole run) |
|
|
59
|
+
|
|
60
|
+
Recommended Deno one-shot for the published CLI when you want native WebRTC:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
deno run -A --minimum-dependency-age=0 --node-modules-dir=auto --allow-scripts=npm:node-datachannel npm:@steve02081504/fount-p2p
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`deno.json` lists `"allowScripts": ["npm:node-datachannel"]` so project-local `deno install` does not try to compile noble/bleno.
|
|
67
|
+
|
|
68
|
+
On Android/Termux, `node-datachannel` has no Bionic prebuild — loader skips it and uses `node-rtc-connection` (data-channel only; since ≥2.1.0 it exposes native W3C handlers and `bufferedAmount`/`bufferedamountlow` backpressure).
|
|
69
|
+
|
|
70
|
+
## Bluetooth probe
|
|
71
|
+
|
|
72
|
+
`canUseBluetoothRuntime` probes in-process: sysfs hint (Linux) → `loadNoble` → `waitPoweredOn` → `stop()`. Requires `@stoprocent/noble>=2.5.9` on Windows (safe teardown after probe). On failure, discovery/link fall back to other paths.
|
|
73
|
+
|
|
74
|
+
Actual scan/GATT uses the same `loadNoble` / `loadBleno` path. Win defaults to scan-only. Shutdown does not await BT warm; a generation counter invalidates late side effects.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Signaling
|
|
2
|
+
|
|
3
|
+
Internal WebRTC (`needsOfferAnswer`) glare and handshake. Shells use the fount-network API only — [transports.md](transports.md). Discovery / mesh surface: [mesh.md](mesh.md).
|
|
4
|
+
|
|
5
|
+
## Glare: connId dual-PC pick-one
|
|
6
|
+
|
|
7
|
+
`node-datachannel` / `node-rtc-connection` cannot safely host two simultaneous glare dials on one PC. Resolution in `transport/offer_answer.mjs`: both sides dial with a random `connId`; on true glare each side builds an independent answer PC, then **keeps the link initiated by the smaller `nodeHash`** (`linkIsPreferred`). Only the canonical link fires `linkUp` / `linkDown`.
|
|
8
|
+
|
|
9
|
+
- Outbound: `ensureDirectLinkToNode` → `dialOfferAnswer`.
|
|
10
|
+
- Inbound offer with unknown `connId`: new answer PC via `accept` — **not** gated by per-`nodeHash` inflights.
|
|
11
|
+
- One-way dial never builds a second PC.
|
|
12
|
+
|
|
13
|
+
## Handshake: buffer early `auth`
|
|
14
|
+
|
|
15
|
+
Frames: `hello` then `auth`. On simultaneous dial, peer `auth` can arrive before peer `hello` — buffer it (`pendingAuth` in `link/pipe.mjs`); never drop.
|
|
16
|
+
|
|
17
|
+
## Windows / `trickleIceOff`
|
|
18
|
+
|
|
19
|
+
When set: send final offer/answer after ICE gathering, dedupe remote signals, queue remote ICE until both descriptions are ready.
|
|
20
|
+
|
|
21
|
+
## Runtime channels
|
|
22
|
+
|
|
23
|
+
`setSignalingRuntimeConfig({ channels })` after `initNode` (or pass `signaling` once on first `startNode`). Changes emit `signaling-changed` and trigger `reloadDiscoveryRelays` (rebind discovery providers + node presence/signals).
|
|
24
|
+
|
|
25
|
+
`channels` selects which discovery/link media are active — `nostr`, `lan`, `bt`, `webrtc`. Semantics per channel key: `false` **disables**; any other value (`undefined`, `true`, or an object) **enables** — an object is merged over that channel's default config. Channels not mentioned keep their default enabled state.
|
|
26
|
+
|
|
27
|
+
Channel → components:
|
|
28
|
+
- `nostr` (nostr discovery + nostr link) — config `relay` **replaces** the default public relay list (do not merge defaults back in).
|
|
29
|
+
- `lan` (lan discovery + lan_tcp link)
|
|
30
|
+
- `bt` (bt discovery + ble_gatt link)
|
|
31
|
+
- `webrtc` (webrtc link data-transport fallback) — config `iceLocalHostnamePolicy` (`none` / `rewrite-loopback` / `drop`, Windows defaults `drop`) and `trickleIceOff` (send final offer/answer after ICE gathering).
|
|
32
|
+
|
|
33
|
+
To run a node on only a few channels, use the `disableAllChannels` helper: `{ channels: disableAllChannels({ nostr: { relay: [...] } }) }` enables only `nostr` (with its per-channel relay) and disables the rest; use `true` to enable a channel with its default config (`disableAllChannels({ nostr: true, webrtc: true })`).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# fount network vs internal transports
|
|
2
|
+
|
|
3
|
+
Mesh keep-alive / discovery: [mesh.md](mesh.md). WebRTC glare / handshake: [signaling.md](signaling.md). Runtime lifecycle: [runtime.md](runtime.md).
|
|
4
|
+
|
|
5
|
+
## No versioning
|
|
6
|
+
|
|
7
|
+
Do **not** introduce version fields, constants, or suffixes (`v`, `version`, `FRAME_VERSION`, `:v1`, …). Changing a shape means changing it; no dual-read / backward-compat paths. Exception: npm `package.json` `version` is for package publish only.
|
|
8
|
+
|
|
9
|
+
## Public contract (shell / L4)
|
|
10
|
+
|
|
11
|
+
This package exposes a **fount network**: talk to `nodeHash` peers with envelopes.
|
|
12
|
+
|
|
13
|
+
Typical entrypoints:
|
|
14
|
+
|
|
15
|
+
- `startNode` / `getLinkRegistry().ensureRuntime()`
|
|
16
|
+
- `ensureLinkToNode` / `sendToNodeLink` / `subscribeScope`
|
|
17
|
+
- `createGroupLinkSet` / `createScopedLinkRoom` / `ensureUserRoom`
|
|
18
|
+
|
|
19
|
+
`registerScopeAuthorizer` may be called before `initNode` / `getLinkRegistry` — it only buffers policy until the default registry is created.
|
|
20
|
+
|
|
21
|
+
Callers do **not** choose WebRTC, BLE, ICE, or DataChannels. If a path is unavailable on the host, the registry tries the next internal provider; the API surface stays the same.
|
|
22
|
+
|
|
23
|
+
**Public registration only:** `registerLinkProvider` / `registerDiscoveryProvider` from the package facade or `@steve02081504/fount-p2p/link` / `./discovery`. Provider *implementations* under `link/providers/*` remain package-internal — shells must not import them or pick transports.
|
|
24
|
+
|
|
25
|
+
Public `./transport/*` subpaths: `link_registry`, `user_room`, `group_link_set`, `node_scope/wire`, `node_scope/features`, `room_scopes`, `remote_user_room`, `scoped_link`. There is no `transport/node_scope` barrel — import the concrete modules. Modules such as `offer_answer`, `runtime_bootstrap` are internal.
|
|
26
|
+
|
|
27
|
+
Topic / rendezvous / signal crypto live under `discovery/` (`nostr.mjs`, `internal/signal_crypto.mjs`, `adverts.mjs`) — not in `transport/`, not a package export. Do not export `advertiseTopic` / `subscribeTopic` / `sendSignal(topic)` on the fount-network surface. ICE `.local` host-candidate filtering is `iceLocalHostnamePolicy` only.
|
|
28
|
+
|
|
29
|
+
## Internal layers
|
|
30
|
+
|
|
31
|
+
| Layer | Role |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| **Discovery** (`discovery/`) | Per medium: `listVisibleNodeHashes` + `connectToNode`. Encrypted adverts/signals via `adverts.mjs` + `index.mjs` helpers. |
|
|
34
|
+
| **Link providers** (`link/providers/`) | Open a duplex pipe; sorted by **`level` (descending)** — not a shell import |
|
|
35
|
+
| **Registry** (`transport/link_registry.mjs`) | fount-network facade: dial fallback, scope/overlay, one canonical link per peer |
|
|
36
|
+
| **Bootstrap** (`transport/runtime_bootstrap.mjs`) | `ensureRuntime` register + progressive listen/discovery; `ensureChannelAvailable` for BT discovery; `reloadDiscoveryRelays` on `signaling-changed` |
|
|
37
|
+
| **Offer/answer** (`transport/offer_answer.mjs`) | Discovery-signal glare path for `caps.needsOfferAnswer` (**internal**; uses `sendNodeSignalPacket`) |
|
|
38
|
+
| **Mesh keepalive** (`transport/mesh_keepalive.mjs`) | N/K pool, explore eviction, stable promote to `trustedPeers` |
|
|
39
|
+
|
|
40
|
+
LinkHandle for upper layers: `ready` / `nodeHash` / `send` / `onEnvelope` / `onDown` / `close` / `stats`. Transport-specific fields are for in-package scheduling only.
|
|
41
|
+
|
|
42
|
+
Provider optional hooks (package-internal): `ensureListening`, `localEndpoint`, `canReach`, `caps.probe: 'sync' | 'native'` (`native` = skipped on ensureRuntime fast-listen). Discovery `connectToNode` / `sendNodeSignal` may return `false` when the path is unavailable; fan-out treats that as silent skip. Per-provider throw/false in discovery and link dial fallback are silent; only total failure of the abstraction surfaces to the caller.
|
|
43
|
+
|
|
44
|
+
Each registry only calls `ensureListening` on **its own** `lan_tcp` / `ble_gatt` instances (unique registry ids like `lan_tcp:ab12cd34`). Never fan out listening to other registries' sockets.
|
|
45
|
+
|
|
46
|
+
Chain `providerId` on the LinkHandle stays the short name (`lan_tcp` / `ble_gatt` / `webrtc` / `nostr`) for scheduling/stats.
|
|
47
|
+
|
|
48
|
+
## Level table
|
|
49
|
+
|
|
50
|
+
| id | level |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `lan_tcp` | 80 |
|
|
53
|
+
| `webrtc` | 70 |
|
|
54
|
+
| `ble_gatt` | 40 |
|
|
55
|
+
| `nostr` | −∞ |
|
|
56
|
+
|
|
57
|
+
Constants: `link/providers/levels.mjs`. Discovery uses ascending **`priority`** (handshake / presence / signal media order — Nostr stays last at `100`). Link selection uses descending **`level`** (data transport); Nostr is −∞ so it is dialed only after LAN / WebRTC / BLE fail.
|
|
58
|
+
|
|
59
|
+
## Fallback
|
|
60
|
+
|
|
61
|
+
1. `canReach` false → skip (no dial)
|
|
62
|
+
2. `isAvailable()` fails → skip (probed per provider on the dial path; never via `listAvailableLinkProviders()` first)
|
|
63
|
+
3. dial/handshake fails or soft-fails (`null`) → next lower level
|
|
64
|
+
4. races: higher `level` wins; same level → smaller `nodeHash` initiates
|
|
65
|
+
|
|
66
|
+
`caps.needsOfferAnswer` providers use the shared discovery-signal glare path (`dial`/`accept` + signal session) — not hard-coded to `id === 'webrtc'`.
|
|
67
|
+
|
|
68
|
+
Dial miss / exhausted peers get exponential cooldown so mesh ticks do not busy-loop on stale acquaintances. First-seen discovery peer clues (and `watchNodeAdvert` ingest) clear that peer's cooldown.
|
|
69
|
+
|
|
70
|
+
## Providers (internal)
|
|
71
|
+
|
|
72
|
+
### `lan_tcp` (80)
|
|
73
|
+
|
|
74
|
+
Plain TCP on the LAN. Registry schedules listen in the background after `ensureRuntime`; `buildLocalAdvert` waits for local listen so signed adverts include `tcpPort`. Peers learn `{ host, port }` from discovery meta + advert `tcpPort`. Binding = shared `linkId`; length-prefix framing. No discovery signal / offer-answer. Shells never read `tcpPort`.
|
|
75
|
+
|
|
76
|
+
### `webrtc` (70)
|
|
77
|
+
|
|
78
|
+
Discovery signal + dual DataChannel; DTLS fingerprint as handshake binding; `needsOfferAnswer` glare path. Soft-fail (`null`) continues to lower-level providers. Backend: `node-datachannel` when the native addon loads, else pure-JS `node-rtc-connection` (Android/Termux skips native). See [runtime.md](runtime.md).
|
|
79
|
+
|
|
80
|
+
### `ble_gatt` (40)
|
|
81
|
+
|
|
82
|
+
GATT write/notify; binding = shared `linkId`; needs BT peer hint (`peripheralId` in discovery meta); optional noble/bleno. Per-registry instance like `lan_tcp`; `isAvailable` / `canReach` gate dial. On Win32, scan-only stacks cannot accept inbound BLE links. One BLE adapter cannot host two independent peripherals in-process — production is one node per process. Hardware probe: [runtime.md](runtime.md).
|
|
83
|
+
|
|
84
|
+
### `nostr` (−∞)
|
|
85
|
+
|
|
86
|
+
Last-resort duplex pipe over discovery signal packets (`type: 'link'`), demuxed from WebRTC `type: 'signal'`. Same node rendezvous encryption as signaling; not a second relay subscription. Longer handshake / heartbeat / idle than LAN. Discovery **`priority`** is unchanged (still handshake-last); only link **`level`** is −∞.
|
|
87
|
+
|
|
88
|
+
### Bluetooth discovery signal
|
|
89
|
+
|
|
90
|
+
`discovery/bt` carries short signal blobs on GATT so WebRTC can negotiate near-field when LAN/nostr are unavailable (package-internal).
|
|
91
|
+
|
|
92
|
+
Discovery peripheral and `ble_gatt` both use bleno + name `fount-bt`. On one adapter they contend — last `setServices`/`startAdvertising` wins. Production: one node per process.
|
package/docs/wire.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Wire part / fanout attaches
|
|
2
|
+
|
|
3
|
+
Day-to-day trust rules: [AGENTS.md](../AGENTS.md). Node-scope presets: [infra.md](infra.md).
|
|
4
|
+
|
|
5
|
+
## Fanout vs targeted (attach inventory)
|
|
6
|
+
|
|
7
|
+
| Use | API | Notes |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Timeline / chunk exploration | `fanoutToTopNodes` | TrustGraph-ranked fanout |
|
|
10
|
+
| Mailbox / targeted packets | `sendToNode` / User Room | Never fanout |
|
|
11
|
+
| part_invoke RPC collect | `wire/part/fanout.collectPartInvokeResponses` | Requires `attachPartWire` already; `timeoutMs` bounds end-to-end |
|
|
12
|
+
| Group-room part | `wire/part/group.attachGroupPartWire` | Group federation frames |
|
|
13
|
+
| TrustGraph / group Trystero chunk | `files/chunk/responder.attachTrustGraphFedChunkResponder` | Chunk responder on trust/group path |
|
|
14
|
+
|
|
15
|
+
Part query runtime lives under `federation/part_query/*`; wire attach only in `wire/part/query.mjs`.
|
|
16
|
+
|
|
17
|
+
## Timed collect
|
|
18
|
+
|
|
19
|
+
APIs with `timeoutMs` (e.g. `collectPartInvokeResponses`) must register the wait **first** and must **not** `await` fanout/send on the return path — a stuck `discoverRoute` / `link.send` otherwise defeats the timeout.
|
|
20
|
+
|
|
21
|
+
Pattern: `beginFedFanoutFetch` / fire-and-forget fanout + `sent === 0 → finish()`.
|
package/index.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { getRoutingProfile, setRoutingProfile } from './node/routing_profile.mjs
|
|
|
36
36
|
import { createGroupLinkSet } from './transport/group_link_set.mjs'
|
|
37
37
|
import {
|
|
38
38
|
configureLinkRegistry,
|
|
39
|
+
ensureChannelAvailable,
|
|
39
40
|
ensureLinkToNode,
|
|
40
41
|
ensureOverlayRouter,
|
|
41
42
|
getLinkRegistry,
|
|
@@ -63,6 +64,7 @@ export {
|
|
|
63
64
|
configureLinkRegistry,
|
|
64
65
|
createGroupLinkSet,
|
|
65
66
|
createScopedLinkRoom,
|
|
67
|
+
ensureChannelAvailable,
|
|
66
68
|
ensureLinkToNode,
|
|
67
69
|
ensureNodeDefaults,
|
|
68
70
|
ensureNodeScope,
|
package/link/providers/nostr.mjs
CHANGED
|
@@ -4,9 +4,7 @@ import { randomBytes } from 'node:crypto'
|
|
|
4
4
|
import { base64ToBytes, bytesToBase64 } from '../../core/bytes_codec.mjs'
|
|
5
5
|
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
6
|
import { getDiscoveryProvider, sendNodeSignalPacket } from '../../discovery/index.mjs'
|
|
7
|
-
import {
|
|
8
|
-
import { getNodeTransportSettings } from '../../node/identity.mjs'
|
|
9
|
-
import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
|
|
7
|
+
import { resolveNostrRelayUrls } from '../../discovery/nostr.mjs'
|
|
10
8
|
import { ms } from '../../utils/duration.mjs'
|
|
11
9
|
import { createLruMap } from '../../utils/lru.mjs'
|
|
12
10
|
import { asLinkHandle } from '../pipe.mjs'
|
|
@@ -25,14 +23,6 @@ const NOSTR_HEARTBEAT_MS = ms('60s')
|
|
|
25
23
|
/** Nostr 链空闲超时。 */
|
|
26
24
|
const NOSTR_IDLE_TIMEOUT_MS = ms('3m')
|
|
27
25
|
|
|
28
|
-
/**
|
|
29
|
-
* @returns {string[]} 当前可用中继 URL
|
|
30
|
-
*/
|
|
31
|
-
function resolveDefaultRelayUrls() {
|
|
32
|
-
return getSignalingRuntimeConfig().relayOverride
|
|
33
|
-
?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
26
|
/**
|
|
37
27
|
* @param {string} remoteNodeHash 对端
|
|
38
28
|
* @param {object} packet link 包
|
|
@@ -48,7 +38,7 @@ async function publishLinkPacket(remoteNodeHash, packet) {
|
|
|
48
38
|
* @returns {import('./index.mjs').LinkProvider & { deliverPacket: (packet: object) => void }} provider
|
|
49
39
|
*/
|
|
50
40
|
export function createNostrLinkProvider(options = {}) {
|
|
51
|
-
const resolveRelayUrls = options.getRelayUrls ||
|
|
41
|
+
const resolveRelayUrls = options.getRelayUrls || resolveNostrRelayUrls
|
|
52
42
|
|
|
53
43
|
/** @type {((link: import('./index.mjs').LinkHandle) => void) | null} */
|
|
54
44
|
let onInbound = null
|
|
@@ -68,7 +68,7 @@ export async function createWebRtcLink(options) {
|
|
|
68
68
|
const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
|
|
69
69
|
const channelOpenTimeoutMs = Math.max(handshakeTimeoutMs, ms('30s'))
|
|
70
70
|
const rtc = options.rtc ?? await loadNodeRtcPolyfill()
|
|
71
|
-
const trickleIceOff = getSignalingRuntimeConfig().trickleIceOff === true
|
|
71
|
+
const trickleIceOff = getSignalingRuntimeConfig().channels.webrtc?.trickleIceOff === true
|
|
72
72
|
const peerConnection = new rtc.RTCPeerConnection(options.iceServers?.length ? { iceServers: options.iceServers } : undefined)
|
|
73
73
|
const remoteSignalQueue = []
|
|
74
74
|
const seenRemoteSignals = createLruMap(1024)
|
package/link/rtc/polyfill.mjs
CHANGED
|
@@ -99,7 +99,7 @@ async function loadNodeRtcPolyfillUncached(options) {
|
|
|
99
99
|
for (const backend of backends)
|
|
100
100
|
try {
|
|
101
101
|
const mod = await backend.load()
|
|
102
|
-
const
|
|
102
|
+
const iceLocalHostnamePolicy = getSignalingRuntimeConfig().channels.webrtc?.iceLocalHostnamePolicy
|
|
103
103
|
return {
|
|
104
104
|
RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(
|
|
105
105
|
mod.RTCPeerConnection,
|
package/node/instance.mjs
CHANGED
|
@@ -83,9 +83,13 @@ export function setNodeLogger(logger) {
|
|
|
83
83
|
*/
|
|
84
84
|
export function setSignalingRuntimeConfig(config) {
|
|
85
85
|
if (!runtime) throw new Error('p2p: setSignalingRuntimeConfig requires initNode')
|
|
86
|
-
const previousPolicy = runtime.signaling.iceLocalHostnamePolicy
|
|
87
|
-
runtime.signaling = resolveSignalingRuntimeConfig({
|
|
88
|
-
|
|
86
|
+
const previousPolicy = runtime.signaling.channels.webrtc?.iceLocalHostnamePolicy
|
|
87
|
+
runtime.signaling = resolveSignalingRuntimeConfig({
|
|
88
|
+
...runtime.signaling,
|
|
89
|
+
...config,
|
|
90
|
+
channels: { ...runtime.signaling.channels, ...config?.channels },
|
|
91
|
+
})
|
|
92
|
+
if (runtime.signaling.channels.webrtc?.iceLocalHostnamePolicy !== previousPolicy)
|
|
89
93
|
rtcPolyfillCacheEpoch++
|
|
90
94
|
emitNodeChange('signaling-changed', runtime.signaling)
|
|
91
95
|
}
|