# p2p core lib
App-agnostic building blocks for serverless E2E-encrypted p2p apps with a
replicated, encrypted log. The chat app under `../app/` is one consumer; new
apps copy or import this directory unchanged. Protocol and crypto:
`docs/PROTOCOL.md`. Relay side: `docs/RELAY.md`.
Two layers, so light apps stay light:
- **zero-dependency core** — importable without the heavy stack:
`crypto.ts`, `identity.ts`, `protocol.ts`, `turn.ts`, `selftest.ts`
- **network + log** — pulls in libp2p / Helia / OrbitDB (~0.5 MB gzip):
`net.ts`, `log.ts`, `encryption.ts`, `orbit-identity.ts`, `media.ts`,
`descriptor.ts`
| Module | Job |
|---|---|
| `crypto.ts` | HKDF room derivations (`RoomCrypto`), session `Sealer` (structural nonce safety), room-wide `AtRestCipher` (random IVs) for the log |
| `protocol.ts` | shared types: connection states, signal payloads, log operations |
| `net.ts` | the libp2p node: relay bootstrap, pubsub peer discovery, WebRTC upgrades, sealed presence/roster, sealed broadcast + directed frame streams, redial/wake/ping resilience |
| `log.ts` | `RoomLog`: the OrbitDB events DB — deterministic address from the secret, encrypted entries, entry emission incl. branch catch-up, encrypted image blobs (unixfs/bitswap) with pin pruning |
| `encryption.ts` | OrbitDB encryption hooks backed by `RoomCrypto` (decrypt-throws = access control) |
| `access.ts` | hardened IPFS access controller: binds `entry.key` to the referenced identity (blocks member-to-member authorship forgery), address-identical to vanilla |
| `orbit-identity.ts` | OrbitDB identity provider chaining log entries to the persistent Ed25519 identity |
| `media.ts` | call media: per-pair media-only RTCPeerConnections, perfect negotiation, transceiver reuse, sealed SDP over a libp2p stream |
| `turn.ts` | ephemeral TURN credential fetch + cache → `RTCConfiguration` |
| `identity.ts` | persistent Ed25519 identity from a password-manager-sized seed: session binding assertions, raw signing, emoji/hex fingerprints |
| `descriptor.ts` | best-effort `/.well-known/ap2p` fetch so the relay operator can rotate keys without a client release |
| `selftest.ts` | dev-mode crypto self-test (incl. cross-instance at-rest roundtrips + dbName determinism) |
## Recipe: a new app
```ts
import { newRoomSecret, RoomCrypto } from "./lib/crypto";
import { resolveRelayConfig } from "./lib/descriptor";
import { openHelia, RoomLog } from "./lib/log";
import { Net, newSessionKey } from "./lib/net";
import { IceConfig } from "./lib/turn";
const APP_SALT = "myapp.example.org/v1"; // ← namespaces ALL crypto derivations
const NS = "myapp"; // ← namespaces ALL persistent storage
// (localStorage keys, IDB names) so
// two apps can share an origin
let secret = location.hash.slice(1);
if (!/^[A-Za-z0-9_-]{43}$/.test(secret)) {
secret = newRoomSecret();
history.replaceState(null, "", `#${secret}`);
}
const session = await newSessionKey(); // fresh network id per load
const rc = await RoomCrypto.create(secret, APP_SALT, session.peerId);
const cfg = await resolveRelayConfig({
relayMultiaddr: "/dns4/signal.ardegazu.ro/tcp/443/tls/ws/p2p/12D3KooW…", // any willing relay
turnCredsUrl: "https://signal.ardegazu.ro/turn-credentials",
discoveryTopic: "_peer-discovery._p2p._pubsub",
});
const ice = new IceConfig(cfg.turnCredsUrl);
const net = await Net.create({
privateKey: session.privateKey, crypto: rc, ice,
relayMultiaddr: cfg.relayMultiaddr, discoveryTopic: cfg.discoveryTopic,
myName: () => "anon",
events: {
peerState: () => {}, peerGone: () => {}, peerReady: () => {},
message: (from, payload) => console.log("live:", from, payload),
binary: () => {}, status: () => {},
},
});
// ephemeral traffic: net.broadcast({...}) / net.sendTo(peer, {...}) — sealed E2E
// durable, replicated state: the encrypted log (optional — skip for live-only apps)
const helia = await openHelia(net.libp2p, { blocks: `${NS}-blocks`, data: `${NS}-data` });
const log = await RoomLog.open(helia, rc, /* identity */ null, {
// wire BEFORE replication starts — a late-attached handler loses entries
onEntry: (e) => console.log("log:", e.hash, e.from, e.op),
onError: () => {}, // non-members' undecryptable pushes — expected noise
});
await log.loadTail(500); // replay this device's copy
await log.append({ t: "chat", ts: Date.now(), name: "anon", text: "hello, replicated world" });
```
Rules the lib enforces for you:
- the room secret stays in the URL fragment; everything the network sees is a
one-way derivation of it
- every payload is AES-GCM sealed — live traffic per sender per session
(structurally nonce-safe), the log under room-wide keys (random IVs);
anything that fails to decrypt is dropped, and decryptability IS membership
- every member derives the same log address; non-members' writes are rejected
at the crypto boundary
- the relay only ever carries encrypted handshakes; room traffic runs
member⇄member, and relay loss never interrupts established connections
Keep rooms ≤ ~12 peers — it's a full mesh (O(n²) pairs, each broadcast uploads
n−1 times).