# p2p core lib
App-agnostic building blocks for serverless E2E-encrypted mesh apps. The chat
app under `../app/` is one consumer; new apps copy or import this directory
unchanged. Wire protocol and crypto: `docs/PROTOCOL.md`. Server side:
`docs/SIGNAL-SERVER.md`.
| Module | Job |
|---|---|
| `crypto.ts` | HKDF room derivations (`RoomCrypto`), AES-GCM `Sealer` with structural nonce safety, JSON + binary envelopes |
| `protocol.ts` | wire types (signaling frames, signal payloads) |
| `signaling.ts` | WS client: auto-reconnect w/ backoff, ping watchdog, iOS wake handling |
| `mesh.ts` | full-mesh RTCPeerConnections: perfect negotiation, negotiated datachannels, ICE-restart recovery, encrypted broadcast (JSON + chunk-friendly binary w/ backpressure) |
| `turn.ts` | ephemeral TURN credential fetch + cache → `RTCConfiguration` |
| `identity.ts` | persistent Ed25519 identity from a password-manager-sized seed: session binding assertions + emoji/hex fingerprints (PROTOCOL.md §7b) |
| `selftest.ts` | dev-mode crypto self-test |
## Recipe: a new app in ~40 lines
```ts
import { newRoomSecret, RoomCrypto } from "./lib/crypto";
import { Signaling } from "./lib/signaling";
import { Mesh } from "./lib/mesh";
import { IceConfig } from "./lib/turn";
const APP_SALT = "myapp.ardegazu.ro/v1"; // ← the ONLY app-specific bit
const SIGNAL_URL = "wss://signal.ardegazu.ro/ws"; // shared relay is fine
let secret = location.hash.slice(1);
if (!/^[A-Za-z0-9_-]{43}$/.test(secret)) {
secret = newRoomSecret();
history.replaceState(null, "", `#${secret}`);
}
const rc = await RoomCrypto.create(secret, APP_SALT);
const ice = new IceConfig(SIGNAL_URL.replace(/^ws/, "http").replace(/\/ws$/, "/turn-credentials"));
const signaling = new Signaling(SIGNAL_URL, rc.roomId, rc.peerId, {
peers: (l) => mesh.onPeers(l),
peerJoined: (p) => mesh.onPeerJoined(p),
peerLeft: (p) => mesh.onPeerLeft(p),
signal: (f, e) => void mesh.onSignal(f, e),
status: () => {}, error: () => {},
});
const mesh = new Mesh({
crypto: rc, signaling, ice, myName: () => "anon",
events: {
message: (from, payload) => console.log(from, payload), // your app logic
binary: () => {}, peerState: () => {}, peerGone: () => {}, channelOpen: () => {},
},
});
signaling.connect();
// later: mesh.broadcast({ anything: "you like" }) — sealed per-sender, E2E
```
Rules the lib enforces for you:
- room secret stays in the URL fragment; the relay sees only a one-way roomId
- every payload (signaling + data) is AES-GCM sealed per sender per session;
nonce reuse is impossible by construction (`Sealer` owns the IV counter)
- joiner-initiates + perfect negotiation ⇒ no glare, no duplicate channels
- signaling outages never interrupt open datachannels
Keep rooms ≤ ~12 peers — it's a full mesh (O(n²) pairs, each broadcast uploads
n−1 times).