cautpartener / 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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# 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).

static mirror of HEAD · about · clone: git clone https://cod-sursa.cautpartener.ro/cautpartener.git