first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
# @suaveplan/network-transport-libp2p - Universal libp2p Transport for @suaveplan/network — WebSockets/WebRTC sub-transports with Noise + Yamux, identify + ping + bootstrap + circuit-relay-v2, fully injectable libp2p factory
## Summary
Universal libp2p `Transport` for `@suaveplan/network`. Composes WebSockets or WebRTC sub-transports with Noise + Yamux, identify + ping + bootstrap + circuit-relay-v2, all wired through an injectable libp2p factory so the package itself never bundles `libp2p` at module-eval.
<!-- begin:auto-docs -->
## Documentation tree
- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [frame](./src/messaging/frame.md) — Implements length-prefixed binary framing for envelopes traversing a libp2p stream. Each frame is a 4-byte big-endian l…
- [peer-message](./src/messaging/peer-message.md) — Defines the wire-format codec for envelopes flowing between libp2p peers. Encoding uses the `@suaveplan/serialize` Mess…
- [send](./src/messaging/send.md) — Outbound stream-based envelope shipping over libp2p. Each `send` opens a new protocol stream, writes a single framed en…
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [frame](./src/messaging/frame.md) — Implements length-prefixed binary framing for envelopes traversing a libp2p stream. Each frame is a 4-byte big-endian l…
- [peer-message](./src/messaging/peer-message.md) — Defines the wire-format codec for envelopes flowing between libp2p peers. Encoding uses the `@suaveplan/serialize` Mess…
- [send](./src/messaging/send.md) — Outbound stream-based envelope shipping over libp2p. Each `send` opens a new protocol stream, writes a single framed en…
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_- [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [frame](./src/messaging/frame.md) — Implements length-prefixed binary framing for envelopes traversing a libp2p stream. Each frame is a 4-byte big-endian l…
- [peer-message](./src/messaging/peer-message.md) — Defines the wire-format codec for envelopes flowing between libp2p peers. Encoding uses the `@suaveplan/serialize` Mess…
- [send](./src/messaging/send.md) — Outbound stream-based envelope shipping over libp2p. Each `send` opens a new protocol stream, writes a single framed en…
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_- [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [frame](./src/messaging/frame.md) — Implements length-prefixed binary framing for envelopes traversing a libp2p stream. Each frame is a 4-byte big-endian l…
- [peer-message](./src/messaging/peer-message.md) — Defines the wire-format codec for envelopes flowing between libp2p peers. Encoding uses the `@suaveplan/serialize` Mess…
- [send](./src/messaging/send.md) — Outbound stream-based envelope shipping over libp2p. Each `send` opens a new protocol stream, writes a single framed en…
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_- [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [connection](./src/connection/connection.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ - [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [frame](./src/messaging/frame.md) — Implements length-prefixed binary framing for envelopes traversing a libp2p stream. Each frame is a 4-byte big-endian l…
- [peer-message](./src/messaging/peer-message.md) — Defines the wire-format codec for envelopes flowing between libp2p peers. Encoding uses the `@suaveplan/serialize` Mess…
- [send](./src/messaging/send.md) — Outbound stream-based envelope shipping over libp2p. Each `send` opens a new protocol stream, writes a single framed en…
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [plugin](./src/plugin/plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_ - [streams](./src/streams/streams.md) — <!-- begin:auto-docs --> _(no submodules)_- [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_ - [transport](./src/transport/transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/network-transport-libp2p
```
## Quick Start
```ts
import { Libp2pPlugin, Libp2pTransport } from "@suaveplan/network-transport-libp2p";
const transport = new Libp2pTransport({
libp2pFactory: async (cfg) => {
const { createLibp2p } = await import("libp2p");
const { websockets } = await import("@libp2p/websockets");
const { noise } = await import("@chainsafe/libp2p-noise");
const { yamux } = await import("@chainsafe/libp2p-yamux");
const { bootstrap } = await import("@libp2p/bootstrap");
const { identify } = await import("@libp2p/identify");
const { ping } = await import("@libp2p/ping");
return (await createLibp2p({
addresses: { listen: cfg.addresses.listen as string[] },
transports: [websockets()],
connectionEncryption: [noise()],
streamMuxers: [yamux()],
peerDiscovery: [bootstrap({ list: cfg.bootstrap as string[] })],
services: { identify: identify(), ping: ping() },
connectionManager: {
maxConnections: cfg.maxConnections,
dialTimeout: cfg.dialTimeoutMs,
},
})) as unknown as import("@suaveplan/network-transport-libp2p").Libp2pNodeLike;
},
bootstrap: ["/dns4/bootstrap.example/tcp/443/wss/p2p/Qm…"],
subTransport: "websockets",
});
await transport.start();
```
## Sub-Transport Selection
| `subTransport` | Default `addresses.listen` | Notes |
|---|---|---|
| `"websockets"` (default) | `/ip4/0.0.0.0/tcp/0/ws` | Universal — works in browser and Node. |
| `"webrtc"` | `/webrtc` | Browser-tier sub-transport; relies on the libp2p `webrtc` module. |
TCP and mDNS are deferred to a server-tier sibling package.
## Plugin Wrapper
```ts
import { Libp2pPlugin } from "@suaveplan/network-transport-libp2p";
const plugin = new Libp2pPlugin({
libp2pFactory,
bootstrap: bootstrapList,
});
node.use(plugin);
await node.start();
```
## Security & Hardening
- `bootstrap` is **mandatory** — empty list rejected at construction with `INVALID_OPTIONS`. There is no public-bootstrap fallback.
- `libp2pFactory` is **mandatory** — the package never falls back to a default libp2p constructor; callers control the security posture.
- All public boundaries validated by Zod.
- 11-code structured error registry.
- `AbortSignal` threaded through every async path.
- Default frame cap: 1 MiB. Default message timeout: 5 s. Default dial timeout: 10 s.
## Observability
| Instrument | Type | Attributes |
|---|---|---|
| `libp2p.connections_opened_total` | counter | `outcome` |
| `libp2p.streams_opened_total` | counter | — |
| `libp2p.bytes_total` | counter | `direction` |
| `libp2p.dial_ms` | histogram | — |
| `libp2p.stream_ttfb_ms` | histogram | — |
| `libp2p.active_peers` | gauge | — |
## Demo
The 749-line React demo lives in a separate Wave-7 package (`@suaveplan/react-network-libp2p-demo`) so this package stays React-free.
## Module Layout
- [`src/types/`](./src/types/types.md) — structural type contracts.
- [`src/errors/`](./src/errors/errors.md) — `NET_LIBP2P_CODES` + `NetLibp2pError`.
- [`src/otel/`](./src/otel/otel.md) — counters, histograms, gauges, span helper.
- [`src/messaging/peer-message.md`](./src/messaging/peer-message.md) — wire format.
- [`src/messaging/frame.md`](./src/messaging/frame.md) — length-prefixed framing.
- [`src/messaging/send.md`](./src/messaging/send.md) — outbound stream send.
- [`src/streams/`](./src/streams/streams.md) — inbound stream handler + drain.
- [`src/connection/`](./src/connection/connection.md) — node lifecycle.
- [`src/transport/`](./src/transport/transport.md) — `Libp2pTransport`.
- [`src/plugin/`](./src/plugin/plugin.md) — `Libp2pPlugin`.

View File

@@ -0,0 +1,78 @@
# connection
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Manages the libp2p node lifecycle on behalf of `Libp2pTransport`. Three small async functions wrap the injected factory: one to construct and start a node, one to dial a peer by multiaddr, and one to stop a node cleanly. All three translate every underlying libp2p surface error into a typed `NetLibp2pError` so callers branch on `error.code` without needing to introspect the libp2p exception hierarchy.
## Features
- `createAndStartNode(factory, config, signal?)` — composes factory + `start()`; emits OTel outcome on every path.
- `dialPeerAddress(node, address, signal?)` — wraps `node.dial(multiaddr)` with OTel dial-duration tracking.
- `stopNode(node)` — translates `node.stop()` failures to `TRANSPORT_CLOSED`.
- Honours pre-aborted `AbortSignal` at every step; aborted-between-factory-and-start is treated as a complete failure and the freshly-created node is stopped to avoid leaking native handles.
- Records `connections_opened_total{outcome}` and `dial_ms` automatically.
## Usage: Basic Example
```ts
import {
createAndStartNode,
dialPeerAddress,
stopNode,
} from "@suaveplan/network-transport-libp2p";
const node = await createAndStartNode(libp2pFactory, {
subTransport: "websockets",
addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] },
bootstrap: ["/dns4/bootstrap.example/tcp/443/wss/p2p/Qm…"],
maxConnections: 100,
dialTimeoutMs: 10_000,
protocolId: "/suaveplan/network/1.0.0",
});
await dialPeerAddress(node, "/dns4/peer.example/tcp/443/wss/p2p/Qm…");
await stopNode(node);
```
## Usage: Advanced Example
```ts
import {
createAndStartNode,
dialPeerAddress,
NET_LIBP2P_CODES,
NetLibp2pError,
} from "@suaveplan/network-transport-libp2p";
const ac = new AbortController();
const cancel = setTimeout(() => ac.abort(), 30_000);
try {
const node = await createAndStartNode(factory, baseConfig, ac.signal);
for (const addr of bootstrapAddresses) {
try {
await dialPeerAddress(node, addr, ac.signal);
} catch (err) {
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.DIAL_FAILED) {
console.warn(`bootstrap miss: ${addr}`);
continue;
}
throw err;
}
}
} finally {
clearTimeout(cancel);
}
```
## API Reference
- `createAndStartNode(factory, config, signal?): Promise<Libp2pNodeLike>` — throws `BOOTSTRAP_FAILED` on factory or start failure (and on signal pre-abort).
- `dialPeerAddress(node, address, signal?): Promise<void>` — throws `DIAL_FAILED` with `context: { address }` on failure.
- `stopNode(node): Promise<void>` — throws `TRANSPORT_CLOSED` on stop failure.
## Implementation Notes
The factory pattern is the single critical design choice here — the package never imports `libp2p` at module-eval. The "aborted after factory but before start" branch is intentionally treated as a hard failure (rather than letting the node start and trusting the caller to stop it later); leaking a half-started libp2p node into the runtime is the kind of bug that's near-impossible to reproduce in CI.

View File

@@ -0,0 +1,63 @@
# errors
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the structured-error registry for `@suaveplan/network-transport-libp2p`. Every public boundary in the package raises a `NetLibp2pError` carrying a stable `code` drawn from `NET_LIBP2P_CODES`. Consumers branch on `error.code` rather than parsing message text — this is the recommended pattern across the entire `@suaveplan/*` ecosystem.
## Features
- 11-code registry (`BOOTSTRAP_FAILED`, `DIAL_FAILED`, `STREAM_FAILED`, `PROTOCOL_NOT_SUPPORTED`, `IDENTIFY_REJECTED`, `INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`, `PEER_ALREADY_CONNECTED`, `PEER_UNKNOWN`, `TRANSPORT_CLOSED`).
- Built via `defineCodes("NET_LIBP2P", [...])` — yields fully qualified codes like `"NET_LIBP2P.DIAL_FAILED"` that don't collide with other packages.
- `NetLibp2pError extends SuaveplanError<NetLibp2pErrorCode>` so callers get cause-chain traversal, structured context, redaction, and OTel span-event integration for free.
- Frozen registry — adding new codes requires editing this file and bumping the package version.
## Usage: Basic Example
```ts
import {
NET_LIBP2P_CODES,
NetLibp2pError,
} from "@suaveplan/network-transport-libp2p";
try {
await transport.start();
} catch (err) {
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.BOOTSTRAP_FAILED) {
// retry with backoff
}
throw err;
}
```
## Usage: Advanced Example
```ts
import { NET_LIBP2P_CODES, NetLibp2pError } from "@suaveplan/network-transport-libp2p";
function classify(err: unknown): "retryable" | "fatal" | "config" {
if (!(err instanceof NetLibp2pError)) return "fatal";
switch (err.code) {
case NET_LIBP2P_CODES.INVALID_OPTIONS:
return "config";
case NET_LIBP2P_CODES.DIAL_FAILED:
case NET_LIBP2P_CODES.STREAM_FAILED:
case NET_LIBP2P_CODES.BOOTSTRAP_FAILED:
return "retryable";
default:
return "fatal";
}
}
```
## API Reference
- `NET_LIBP2P_CODES`: frozen lookup of code-name → fully qualified code string.
- `NetLibp2pErrorCode`: literal union over the registry.
- `NetLibp2pError(code, messageKey, options?)`: constructor accepts the standard `SuaveplanErrorOptions` shape, including `cause` (Error chaining) and `context` (structured details).
## Implementation Notes
We deliberately avoid a single catch-all code. Hyperswarm's reputation engine taught us that operators need to slice failures by reason to decide what to do — `STREAM_FAILED` warrants reputation tracking; `DIAL_FAILED` warrants retry; `INVALID_OPTIONS` warrants a config-fix; `BOOTSTRAP_FAILED` warrants surfacing to the user. Adding more codes is a non-breaking change as long as nothing existing is removed or repurposed.

View File

@@ -0,0 +1,60 @@
# frame
## Purpose
Implements length-prefixed binary framing for envelopes traversing a libp2p stream. Each frame is a 4-byte big-endian length header followed by an exact-length MessagePack payload. The `FrameDecoder` is stateful — it buffers partial reads across `push()` calls so callers can stream chunks directly from `stream.source` without re-implementing the buffering protocol on every transport.
## Features
- `encodeFrame(envelope)` → `Uint8Array` length-prefix + MessagePack payload.
- `FrameDecoder.push(chunk)` → `MessageEnvelope[]`, emits zero or more complete envelopes.
- `FrameDecoder.bufferedBytes` — observability hook.
- `FrameDecoder.reset()` — drops the partial buffer on socket close.
- `DEFAULT_MAX_FRAME_SIZE = 1 MiB` plaintext cap (configurable per-decoder).
- `STREAM_FAILED` on zero-length headers and oversized frames; `INVALID_OPTIONS` if `maxFrameSize` is non-positive.
## Usage: Basic Example
```ts
import { encodeFrame, FrameDecoder } from "@suaveplan/network-transport-libp2p";
const frame = encodeFrame({ id: "m1", type: "ping", payload: { n: 1 } });
const decoder = new FrameDecoder();
const envelopes = decoder.push(frame);
console.log(envelopes[0]?.id); // "m1"
```
## Usage: Advanced Example
```ts
import { FrameDecoder } from "@suaveplan/network-transport-libp2p";
async function* drainStream(
stream: { source: AsyncIterable<{ subarray(): Uint8Array }> },
maxFrameSize = 256 * 1024
) {
const decoder = new FrameDecoder(maxFrameSize);
for await (const chunk of stream.source) {
for (const envelope of decoder.push(chunk.subarray())) {
yield envelope;
}
}
}
for await (const envelope of drainStream(someStream, 65_536)) {
handleEnvelope(envelope);
}
```
## API Reference
- `DEFAULT_MAX_FRAME_SIZE: 1048576`.
- `encodeFrame(envelope: MessageEnvelope): Uint8Array`.
- `class FrameDecoder` — constructor: `(maxFrameSize?: number)` (default `DEFAULT_MAX_FRAME_SIZE`).
- `push(chunk: Uint8Array): MessageEnvelope[]` — appends chunk, returns every complete envelope; throws `NetLibp2pError(STREAM_FAILED)` on zero-length, oversized, or malformed payloads.
- `get bufferedBytes(): number`.
- `reset(): void`.
## Implementation Notes
The decoder's hot path uses a single `Uint8Array.slice(4 + length)` per emitted frame. For workloads dominated by tiny frames this allocates more than a copy-into-ring-buffer design, but the operational simplicity has been worth the trade — the equivalent ring-buffer in the upstream Hyperswarm package was a recurring source of off-by-one bugs. We may revisit this design if a benchmarked workload demands it.

View File

@@ -0,0 +1,66 @@
# peer-message
## Purpose
Defines the wire-format codec for envelopes flowing between libp2p peers. Encoding uses the `@suaveplan/serialize` MessagePack binary profile, which round-trips Date / Map / Set / BigInt / Uint8Array natively — strictly more capable than `JSON.stringify`. Every error surfaces as a typed `NetLibp2pError(STREAM_FAILED)` so the transport layer can route malformed inputs to its reputation engine.
## Features
- `encodePeerMessage(envelope)` → `Uint8Array` using the shared MessagePack profile.
- `decodePeerMessage(bytes)` → `MessageEnvelope`, throwing `STREAM_FAILED` on malformed input.
- `peerIdToString(id)` → `string`, the single point that normalises libp2p's peer-id object to a string `PeerId`.
- Zero dependency on the concrete `libp2p` package — types only.
## Usage: Basic Example
```ts
import {
decodePeerMessage,
encodePeerMessage,
} from "@suaveplan/network-transport-libp2p";
const bytes = encodePeerMessage({ id: "m1", type: "ping", payload: { n: 1 } });
const back = decodePeerMessage(bytes);
console.log(back.id); // "m1"
```
## Usage: Advanced Example
```ts
import {
decodePeerMessage,
encodePeerMessage,
NET_LIBP2P_CODES,
NetLibp2pError,
peerIdToString,
} from "@suaveplan/network-transport-libp2p";
function safeDecode(bytes: Uint8Array): { ok: true; envelope: unknown } | { ok: false; code: string } {
try {
const envelope = decodePeerMessage(bytes);
return { ok: true, envelope };
} catch (err) {
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.STREAM_FAILED) {
return { ok: false, code: err.code };
}
throw err;
}
}
const sender = peerIdToString({ toString: () => "Qm-sender" });
const encoded = encodePeerMessage({ id: "x", type: "tx", payload: new Map([["k", 1n]]) });
```
## API Reference
- `encodePeerMessage(envelope: MessageEnvelope): Uint8Array`.
- `decodePeerMessage(bytes: Uint8Array): MessageEnvelope` — throws `NetLibp2pError(STREAM_FAILED)` on malformed input.
- `peerIdToString(id: { toString(): string }): string`.
## Implementation Notes
Choosing `@suaveplan/serialize`'s MessagePack profile over raw JSON gives ≈30 % smaller frames on typical payloads while preserving Map / Set / BigInt / Date / Uint8Array round-trip semantics. The decode path swallows the underlying `msgpack-decode` error and rebrands it as `STREAM_FAILED` because the transport layer cannot distinguish "wire format violation" from "byzantine peer" — both warrant the same reputation impact and stream tear-down.
The peer-id pass-through is intentionally minimal. libp2p peer-ids are multihash-encoded buffers but their `toString()` returns the stable base58btc form; we treat that string as opaque throughout the transport so the package never needs to depend on `@libp2p/peer-id` or any specific peer-id concrete class. The decoupling pays off in two places: tests can supply a trivial `{ toString }` object as a peer-id without instantiating real cryptographic identities, and downstream consumers can integrate alternative peer-id systems by wrapping their identifier behind the same single-method contract.
Future work: stream-level codec swapping (caller-injected encode/decode) so the wire format can be replaced with CBOR, Protobuf, or any other byte-exact codec without changing the transport. The structural pass-through cost of MessagePack on every send is negligible compared to the libp2p transport stack itself, so the optimisation has not yet been worth the API surface.

View File

@@ -0,0 +1,66 @@
# send
## Purpose
Outbound stream-based envelope shipping over libp2p. Each `send` opens a new protocol stream, writes a single framed envelope, and closes — modelled after libp2p's standard request/response idiom. The functions thread an `AbortSignal` to cancel the dial and bound the in-flight write with a per-call timeout so a stalled remote cannot pin the calling fiber.
## Features
- `sendEnvelopeFrame(node, peerId, protocolId, frame, timeoutMs, signal?)` for callers that pre-encoded a frame (useful for broadcast fan-out).
- `sendEnvelope(node, peerId, protocolId, envelope, timeoutMs, signal?)` convenience that encodes inline.
- Translates every libp2p surface error into `NetLibp2pError(STREAM_FAILED)` with the cause preserved.
- Records `streams_opened_total`, `stream_ttfb_ms`, and `bytes_total{direction=out}` on success.
- Honours pre-aborted `AbortSignal` instantly with `STREAM_FAILED`.
## Usage: Basic Example
```ts
import { sendEnvelope } from "@suaveplan/network-transport-libp2p";
await sendEnvelope(
node,
{ toString: () => "Qm-peer-id" },
"/suaveplan/network/1.0.0",
{ id: "m1", type: "ping", payload: { n: 1 } },
5_000
);
```
## Usage: Advanced Example
```ts
import {
encodeFrame,
sendEnvelopeFrame,
} from "@suaveplan/network-transport-libp2p";
async function broadcast(node: Libp2pNodeLike, peers: Libp2pPeerIdLike[]) {
const envelope = { id: "bcast", type: "tick", payload: {} } as MessageEnvelope;
const frame = encodeFrame(envelope); // encode once, reuse
const controller = new AbortController();
setTimeout(() => controller.abort(), 1_000);
await Promise.allSettled(
peers.map((p) =>
sendEnvelopeFrame(
node,
p,
"/suaveplan/network/1.0.0",
frame,
5_000,
controller.signal
)
)
);
}
```
## API Reference
- `sendEnvelopeFrame(node, peerId, protocolId, frame, timeoutMs, signal?): Promise<void>`.
- `sendEnvelope(node, peerId, protocolId, envelope, timeoutMs, signal?): Promise<void>`.
Both throw `NetLibp2pError(STREAM_FAILED)` on dialProtocol failure, on `sink`/`close` failure, on timeout, or on a pre-aborted signal.
## Implementation Notes
The function builds a manual `Promise` rather than using `async`/`await` because we need an explicit `setTimeout` cancellable from both the success and failure paths. The pattern mirrors the upstream Hyperswarm and WebRTC transport implementations exactly. Reusing a pre-encoded frame across many peers (broadcast) saves N-1 MessagePack encodes — for chatty fan-out workloads this is significant.

View File

@@ -0,0 +1,74 @@
# otel
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Centralises the OpenTelemetry instrument creation and span helpers for the libp2p transport. Every counter, histogram, and gauge that other modules emit goes through a helper exported here — this keeps OTel boilerplate out of the messaging and transport code and makes the metric surface easy to audit in one place.
## Features
- `connections_opened_total{outcome}` counter — total connection-open attempts.
- `streams_opened_total` counter — opened libp2p protocol streams.
- `bytes_total{direction}` counter — bytes flowing in/out.
- `dial_ms` histogram — wall-clock duration of dialing a peer.
- `stream_ttfb_ms` histogram — wall-clock time to first byte on outbound streams.
- `active_peers` up/down counter (gauge) — currently-connected peer count.
- `withLibp2pSpan(name, fn)` — async-span wrapper that always ends the span.
- All record helpers clamp non-positive values to zero so out-of-order timestamps don't poison the histograms.
## Usage: Basic Example
```ts
import { recordDial, recordBytes, withLibp2pSpan } from "@suaveplan/network-transport-libp2p";
await withLibp2pSpan("custom.op", async () => {
const t0 = Date.now();
// ...do work
recordDial(Date.now() - t0);
recordBytes("out", 1024);
});
```
## Usage: Advanced Example
```ts
import {
adjustActivePeers,
recordConnectionOutcome,
recordStreamOpened,
recordStreamTtfb,
} from "@suaveplan/network-transport-libp2p";
class ObservabilityAdapter {
onPeerJoin(peer: string): void {
adjustActivePeers(+1);
recordConnectionOutcome("ok");
}
onPeerLeave(peer: string): void {
adjustActivePeers(-1);
}
onStreamOpened(ttfbMs: number): void {
recordStreamOpened();
recordStreamTtfb(ttfbMs);
}
onPeerRejected(reason: string): void {
recordConnectionOutcome("rejected");
}
}
```
## API Reference
- `withLibp2pSpan<T>(name: string, fn: (span) => Promise<T>): Promise<T>` — wraps `fn` in a span; always ends.
- `recordConnectionOutcome(outcome: string): void` — `ok` / `error` / `rejected`.
- `recordStreamOpened(): void` — increment streams counter.
- `recordBytes(direction: "in" | "out", count: number): void` — count > 0 only.
- `recordDial(ms: number): void` — clamped non-negative.
- `recordStreamTtfb(ms: number): void` — clamped non-negative.
- `adjustActivePeers(delta: number): void` — gauge delta.
## Implementation Notes
The tracer and meter are constructed at module-eval time via `trace.getTracer` and `metrics.getMeter`. Both APIs return no-op instances when no SDK is registered, so importing this module never forces an OTel SDK initialisation. The counter/histogram names are stable public API — operators may have dashboards keyed on them — so we never rename without a major-version bump and a deprecation alias.

View File

@@ -0,0 +1,77 @@
# plugin
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`Libp2pPlugin implements NetworkPlugin` — the canonical wrapper that lets `@suaveplan/network`'s `NetworkNode` install, start, stop, and dispose a `Libp2pTransport` through a uniform plugin contract. Every transport in the `@suaveplan/network-transport-*` family follows this exact shape so the `network-plugin-transport-manager` can swap transports without code changes.
## Features
- `install(node)` registers the transport with the network node.
- `start()` brings the transport up — composes the libp2p stack, registers the inbound stream handler.
- `stop()` closes the transport and unregisters it from the node.
- `dispose()` releases the node reference; idempotent across repeated calls.
- Exposes the underlying `Libp2pTransport` instance via the `transport` field for tests and tracing hooks.
## Usage: Basic Example
```ts
import { Libp2pPlugin } from "@suaveplan/network-transport-libp2p";
import { NetworkNode } from "@suaveplan/network";
const node = new NetworkNode({ id: "node-1" });
const plugin = new Libp2pPlugin({
libp2pFactory: realLibp2pFactory,
bootstrap: ["/dns4/bootstrap.example/tcp/443/wss/p2p/Qm…"],
});
plugin.install(node);
await plugin.start();
// ... node is now reachable over libp2p
await plugin.stop();
plugin.dispose();
```
## Usage: Advanced Example
```ts
import { Libp2pPlugin } from "@suaveplan/network-transport-libp2p";
import { NetworkNode } from "@suaveplan/network";
const wsPlugin = new Libp2pPlugin({
libp2pFactory: wsFactory,
bootstrap: bootstrapList,
subTransport: "websockets",
});
const rtcPlugin = new Libp2pPlugin({
libp2pFactory: rtcFactory,
bootstrap: bootstrapList,
subTransport: "webrtc",
});
const node = new NetworkNode({ id: "dual" });
node.use(wsPlugin);
node.use(rtcPlugin);
await node.start();
// Later:
await node.stop();
wsPlugin.dispose();
rtcPlugin.dispose();
```
## API Reference
- `new Libp2pPlugin(config: Libp2pConfig)` — constructs the transport eagerly so `config` validation runs at plugin instantiation, not at first `start()`.
- `name: "libp2p"` (readonly).
- `transport: Libp2pTransport` (readonly) — exposed for tracing/tests.
- `install(node: NetworkNode): void`.
- `start(): Promise<void>`.
- `stop(): Promise<void>`.
- `dispose(): void` — idempotent.
## Implementation Notes
Like the other transport plugins, this class is a deliberately thin wrapper — the heavy lifting lives in `Libp2pTransport`. The split exists so that `network-plugin-transport-manager` can introspect and swap plugins uniformly without knowing transport-specific configuration shapes. `dispose()` only clears the node reference; the transport's `close()` runs on `stop()` so a caller may safely re-install the same plugin instance against a different node after `stop()`.

View File

@@ -0,0 +1,66 @@
# streams
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Inbound protocol-handler registration and stream draining. `registerStreamHandler` binds the application protocol id to a callback that ingests each inbound stream through a `FrameDecoder` and emits one `MessageEnvelope` per complete frame. `consumeInboundStream` is the underlying coroutine — exposed separately so callers can re-use it without registering a handler.
## Features
- `registerStreamHandler(node, protocolId, maxFrameSize, onEnvelope, signal?)` — single call to wire the inbound handler.
- `consumeInboundStream(stream, maxFrameSize, onEnvelope, signal?)` — drains a single stream, returns the count of envelopes delivered.
- Records `bytes_total{direction=in}` on every chunk.
- Honours pre-aborted `AbortSignal` instantly and cuts the loop on mid-iteration abort.
- Always closes the stream in a `finally` block, swallowing any close failure (stream is already torn down).
## Usage: Basic Example
```ts
import { registerStreamHandler } from "@suaveplan/network-transport-libp2p";
await registerStreamHandler(
node,
"/suaveplan/network/1.0.0",
1024 * 1024,
(peerId, envelope) => {
console.log(`Received ${envelope.id} from ${peerId}`);
}
);
```
## Usage: Advanced Example
```ts
import {
consumeInboundStream,
NET_LIBP2P_CODES,
NetLibp2pError,
} from "@suaveplan/network-transport-libp2p";
async function drainWithReputation(stream: Libp2pStreamLike, peer: string) {
try {
const count = await consumeInboundStream(
stream,
64 * 1024,
(envelope) => routeEnvelope(peer, envelope)
);
recordPeerHealthy(peer, count);
} catch (err) {
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.STREAM_FAILED) {
recordPeerByzantine(peer);
}
throw err;
}
}
```
## API Reference
- `registerStreamHandler(node, protocolId, maxFrameSize, onEnvelope, signal?): Promise<void>` — throws `NetLibp2pError(STREAM_FAILED)` if `signal` is pre-aborted.
- `consumeInboundStream(stream, maxFrameSize, onEnvelope, signal?): Promise<number>` — throws `NetLibp2pError(STREAM_FAILED)` on any framing/decoding error.
## Implementation Notes
The handler returns synchronously after firing the consume coroutine (`void` return) — libp2p's handler API does not await its callback, so we never block its event loop. Errors raised during decoding propagate up through the `consumeInboundStream` promise, but because the transport never awaits that promise (it's a fire-and-forget on each new stream), errors surface in OTel counters rather than as uncaught exceptions. Future work: explicit reputation hook to tear down the offending peer's other streams when a single stream signals byzantine behaviour.

View File

@@ -0,0 +1,95 @@
# transport
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`Libp2pTransport implements Transport` from `@suaveplan/network`. The class composes a libp2p stack (production or fake) through an injectable factory and wires the universal `Transport` contract: `start` / `connect` / `disconnect` / `send` / `onMessage` / `onPeerEvent` / `getConnectedPeers` / `close`. It owns the lifecycle of the libp2p node, registers the inbound stream handler, and converts libp2p's `peer:connect` / `peer:disconnect` events into the `Transport.onPeerEvent` shape.
## Features
- Universal — zero direct dependency on the concrete `libp2p` package at module-eval; production wiring lives in caller code.
- Mandatory `libp2pFactory` and non-empty `bootstrap` enforced by `Libp2pConfigSchema` at construction.
- Sub-transport selection (`websockets` | `webrtc`); TCP / mDNS deferred to a server-tier sibling.
- Idempotent `start()` and `close()`; `start()` after `close()` rejects with `DISPOSED`.
- Translates every internal failure into a typed `NetLibp2pError`.
- Records `connections_opened_total`, `active_peers`, and (via `send` / `streams` modules) `bytes_total` + `stream_ttfb_ms`.
- Honours pre-aborted `AbortSignal` on `start()`.
## Usage: Basic Example
```ts
import { Libp2pTransport } from "@suaveplan/network-transport-libp2p";
const transport = new Libp2pTransport({
libp2pFactory: myLibp2pFactory,
bootstrap: ["/dns4/bootstrap.example/tcp/443/wss/p2p/Qm…"],
});
await transport.start();
transport.onMessage((peerId, envelope) => {
console.log(`recv ${envelope.id} from ${peerId}`);
});
await transport.connect("Qm-remote", "/dns4/remote/tcp/443/wss/p2p/Qm-remote");
await transport.send("Qm-remote", { id: "m1", type: "ping", payload: {} });
await transport.close();
```
## Usage: Advanced Example
```ts
import {
Libp2pTransport,
NET_LIBP2P_CODES,
NetLibp2pError,
} from "@suaveplan/network-transport-libp2p";
const transport = new Libp2pTransport({
libp2pFactory,
bootstrap: bootstrapList,
subTransport: "webrtc",
addresses: { listen: ["/webrtc"] },
protocolId: "/my-app/2.0.0",
maxConnections: 50,
dialTimeoutMs: 5_000,
maxFrameSize: 256 * 1024,
messageTimeoutMs: 3_000,
});
const startController = new AbortController();
setTimeout(() => startController.abort(), 15_000);
try {
await transport.start(startController.signal);
} catch (err) {
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.BOOTSTRAP_FAILED) {
// surface to user / retry / etc.
}
throw err;
}
const off = transport.onPeerEvent((peer, event) => {
metrics.gauge("peers", transport.getConnectedPeers().length, { peer, event });
});
// later: off();
```
## API Reference
- `new Libp2pTransport(config: Libp2pConfig)` — throws `INVALID_OPTIONS` if `libp2pFactory` missing or `bootstrap` empty.
- `id: "libp2p"` (readonly).
- `subTransport: Libp2pSubTransport` (readonly).
- `protocolId: string` (readonly).
- `start(signal?: AbortSignal): Promise<void>` — idempotent; throws `DISPOSED` after `close()`.
- `connect(peerId, address): Promise<void>` — throws `NOT_RUNNING` before `start()`, `DISPOSED` after `close()`, `DIAL_FAILED` on dial failure.
- `disconnect(peerId): Promise<void>` — idempotent.
- `send(peerId, envelope): Promise<void>` — throws `PEER_UNKNOWN` for unknown peer, `STREAM_FAILED` on protocol failure.
- `onMessage(handler): () => void` — returns disposer.
- `onPeerEvent(handler): () => void` — returns disposer.
- `getConnectedPeers(): readonly PeerId[]` — snapshot.
- `close(): Promise<void>` — idempotent.
## Implementation Notes
The transport stores peer ids as plain strings to keep `getConnectedPeers()` O(1) on the hot path. The `peer:connect` / `peer:disconnect` event listeners use ordinary functions held in instance fields so `close()` can pass exact references to `removeEventListener` — anonymous arrow handlers would silently leak. Sub-transport defaults to `websockets` because it is the universally-supported wire on both server and browser; selecting `webrtc` is a deliberate browser-tier opt-in. All error translation runs at the boundary; the internal `connection` / `streams` / `send` modules already produce typed errors, so the transport class itself contains essentially zero error-translation logic.

View File

@@ -0,0 +1,91 @@
# types
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the universal structural types consumed by every other module in `@suaveplan/network-transport-libp2p`. The package never imports the concrete `libp2p` package at module-eval time — instead, callers supply a `Libp2pFactory` and the package operates on structural `Libp2pNodeLike` / `Libp2pStreamLike` / `Libp2pPeerIdLike` interfaces. This keeps the package portable across browsers and Node, makes tests trivially injectable, and limits the bundle surface to the abstractions the transport actually exercises.
## Features
- `Libp2pSubTransport` discriminated union (`websockets` | `webrtc`).
- `Libp2pNodeLike` structural interface mirroring the subset of `libp2p`'s `Libp2p` instance that this package uses (`start`/`stop`/`dial`/`dialProtocol`/`handle`/`unhandle`/`getPeers`/`addEventListener`/`removeEventListener`).
- `Libp2pStreamLike` structural duplex stream with async-iterable `source` and async `sink`.
- `Libp2pPeerIdLike` opaque peer-id with stable `toString()`.
- `Libp2pStreamHandler` callback signature accepted by `handle(protocolId, handler)`.
- `Libp2pFactory` async factory function — sole production wiring boundary.
- `Libp2pFactoryConfig` configuration passed from the transport to the factory.
- `Libp2pConfig` public boundary type validated by `Libp2pConfigSchema`.
- `MessageHandler` / `PeerEventHandler` matching the `Transport` contract.
## Usage: Basic Example
```ts
import type {
Libp2pConfig,
Libp2pFactory,
} from "@suaveplan/network-transport-libp2p";
const factory: Libp2pFactory = async (cfg) => {
const { createLibp2p } = await import("libp2p");
return createLibp2p({
addresses: { listen: cfg.addresses.listen },
// ...wire bootstrap, identify, ping, etc.
}) as unknown as import("@suaveplan/network-transport-libp2p").Libp2pNodeLike;
};
const config: Libp2pConfig = {
libp2pFactory: factory,
bootstrap: ["/dns4/bootstrap.example/tcp/443/wss/p2p/Qm…"],
subTransport: "websockets",
};
```
## Usage: Advanced Example
```ts
import type {
Libp2pConfig,
Libp2pNodeLike,
} from "@suaveplan/network-transport-libp2p";
function wrapForObservability(
next: Libp2pNodeLike,
metricsHook: (event: string) => void
): Libp2pNodeLike {
return new Proxy(next, {
get(target, prop, recv) {
metricsHook(String(prop));
return Reflect.get(target, prop, recv);
},
});
}
const config: Libp2pConfig = {
libp2pFactory: async (cfg) => wrapForObservability(await realFactory(cfg), () => {}),
bootstrap: ["/dns4/example/tcp/443/wss/p2p/Qm…"],
subTransport: "webrtc",
addresses: { listen: ["/webrtc"] },
maxConnections: 50,
dialTimeoutMs: 5_000,
maxFrameSize: 256 * 1024,
};
```
## API Reference
- `Libp2pSubTransport`: `"websockets" | "webrtc"`.
- `Libp2pNodeLike`: structural libp2p node surface.
- `Libp2pStreamLike`: structural duplex stream.
- `Libp2pPeerIdLike`: structural peer-id.
- `Libp2pStreamHandler`: `(args: { stream; connection }) => void`.
- `Libp2pFactory`: `(config: Libp2pFactoryConfig) => Promise<Libp2pNodeLike>`.
- `Libp2pFactoryConfig`: subset of `Libp2pConfig` re-shaped for the factory call.
- `Libp2pConfig`: validated public config; `libp2pFactory` and non-empty `bootstrap` are mandatory.
- `MessageHandler`: `(peerId, envelope) => void`.
- `PeerEventHandler`: `(peerId, "connected" | "disconnected") => void`.
## Implementation Notes
The structural-type pattern is the single most load-bearing design choice in the package. By never importing `libp2p` at module-eval, we avoid pulling in transitive Node-only dependencies into browser bundles and give tests a clean injection seam (`FakeLibp2pNode`). Tradeoff: callers must take responsibility for wiring `createLibp2p` correctly — including Noise, Yamux, identify, ping, and bootstrap services. The Wave-7 demo package documents the canonical wiring.