sueta / client / src / lib / README.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 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).