TypeScript/JavaScript implementation of the Mau P2P social network protocol.
Browser-first P2P social networking with IndexedDB storage and WebRTC communication.
⚠️ HTTP Client: The HTTP client does not yet implement mTLS authentication. For authenticated P2P connections, use the WebRTC client which implements PGP-based challenge-response authentication.
This package is designed for browser environments:
Tests run in Node.js using polyfills (fake-indexeddb, @roamhq/wrtc) but the production code is browser-only.
1npm install @mau-network/mau
1import { createAccount, File } from '@mau-network/mau';
2
3// Create account (uses IndexedDB automatically)
4const account = await createAccount(
5 'mau-data', // Root path in IndexedDB
6 'Alice',
7 'alice@mau.network',
8 'strong-passphrase'
9);
10
11console.log('Account fingerprint:', account.getFingerprint());
12console.log('Public key:', account.getPublicKey());
13
14// Write a post
15const file = await account.createFile('hello.json');
16await file.writeJSON({
17 '@type': 'SocialMediaPosting',
18 headline: 'Hello, Mau!',
19 articleBody: 'My first post on Mau',
20 datePublished: new Date().toISOString(),
21});
22
23// List all files
24const files = await account.listFiles();
25for (const f of files) {
26 console.log('File:', f.getName());
27}
28
29// Add a friend
30const friendPublicKey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\n...';
31const friendFingerprint = await account.addFriend(friendPublicKey);
32
33console.log('Friend added:', friendFingerprint);
});
### Browser P2P with WebRTC
```typescript
import { createAccount, WebRTCServer, WebRTCClient, WebSocketSignaling } from '@mau-network/mau';
// ============== Peer A: Start Server ==============
const aliceAccount = await createAccount('mau-alice', 'Alice', 'alice@mau.network', 'secret');
const aliceFpr = aliceAccount.getFingerprint();
// Start WebRTC server (accepts incoming connections)
const server = new WebRTCServer(aliceAccount, aliceAccount.storage);
// Connect to signaling server
const signaling = new WebSocketSignaling('wss://signaling.mau.network', aliceFpr);
// Handle incoming connection offers
signaling.onMessage(async (message) => {
if (message.type === 'offer') {
const connectionId = `conn-${Date.now()}`;
const answer = await server.acceptConnection(connectionId, message.data);
// Send answer back through signaling
await signaling.send({
from: aliceFpr,
to: message.from,
type: 'answer',
data: answer
});
}
});
console.log('Alice is ready:', aliceFpr);
// ============== Peer B: Connect to Alice ==============
const bobAccount = await createAccount('mau-bob', 'Bob', 'bob@mau.network', 'secret');
const bobFpr = bobAccount.getFingerprint();
// Follow Alice
await bobAccount.addFriend(aliceAccount.getPublicKey());
// Create WebRTC client
const client = new WebRTCClient(bobAccount, bobAccount.storage, aliceFpr);
// Create offer and send via signaling
const offer = await client.createOffer();
await signaling.send({
from: bobFpr,
to: aliceFpr,
type: 'offer',
data: offer
});
// Wait for answer
signaling.onMessage(async (message) => {
if (message.type === 'answer' && message.from === aliceFpr) {
await client.completeConnection(message.data);
// Perform mTLS handshake
const authenticated = await client.performMTLS();
if (!authenticated) throw new Error('Authentication failed');
// Now fetch files over WebRTC!
const fileList = await client.fetchFileList();
console.log('Files from Alice:', fileList);
// Download a file
const data = await client.downloadFile('hello.json');
console.log('Downloaded:', data.length, 'bytes');
}
});
Run a simple HTTP signaling server for WebRTC coordination:
1# Start signaling server
2npx tsx examples/signaling-server.ts 8080
For full examples, see:
examples/browser-example.ts - Complete browser P2P flowexamples/signaling-server.ts - HTTP signaling server 1// Create new account
2const account = await Account.create(storage, rootPath, {
3 name: 'Alice',
4 email: 'alice@mau.network',
5 passphrase: 'strong-passphrase',
6 algorithm: 'ed25519', // or 'rsa'
7});
8
9// Load existing account
10const account = await Account.load(storage, rootPath, 'passphrase');
11
12// Get account info
13account.getFingerprint(); // => '3a7b2c...'
14account.getName(); // => 'Alice'
15account.getEmail(); // => 'alice@mau.network'
16account.getPublicKey(); // => '-----BEGIN PGP PUBLIC KEY BLOCK-----...'
17
18// Manage friends
19await account.addFriend(armoredPublicKey);
20await account.removeFriend(fingerprint);
21account.getFriends(); // => ['fpr1', 'fpr2', ...]
22account.isFriend(fpr); // => true/false
1// Create file reference
2const file = File.create(account, storage, 'my-post.json');
3
4// Write content (encrypted + signed automatically)
5await file.writeText('Hello, world!');
6await file.writeJSON({ message: 'Hello' });
7await file.write(new Uint8Array([1, 2, 3]));
8
9// Read content (decrypts + verifies automatically)
10const text = await file.readText();
11const json = await file.readJSON();
12const data = await file.read();
13
14// File operations
15await file.delete();
16const size = await file.getSize();
17const checksum = await file.getChecksum();
18
19// Versioning
20const versions = await file.getVersions();
21for (const version of versions) {
22 console.log(await version.readText());
23}
24
25// List files
26const files = await File.list(account, storage);
27const friendFiles = await File.listFriend(account, storage, friendFingerprint);
1// Create client
2const client = new Client(account, storage, peerFingerprint, {
3 timeout: 30000,
4 resolvers: [/* custom resolvers */],
5});
6
7// Or use convenience method
8const client = Client.create(account, account.storage, {
9 fingerprint: 'peer-fingerprint',
10 address: '192.168.1.100:8080',
11}, [staticResolver(knownPeers)]);
12
13// Sync files
14const stats = await client.sync();
15// => { downloaded: 5, updated: 2, errors: 0, failedFiles: [] }
16
17// Fetch file list
18const files = await client.fetchFileList();
19const filesAfter = await client.fetchFileList(new Date('2024-01-01'));
20
21// Download specific file
22const data = await client.downloadFile('post.json');
23const version = await client.downloadFileVersion('post.json', 'abc123...');
The Server class provides a framework-agnostic HTTP request handler. It’s primarily used internally by WebRTCServer for HTTP-over-datachannel communication.
1// Create server
2const server = new Server(account, storage, {
3 resultsLimit: 20,
4});
5
6// Handle requests manually (used internally by WebRTCServer)
7const response = await server.handleRequest({
8 method: 'GET',
9 url: '/p2p/3a7b2c.../post.json',
10 path: '/p2p/3a7b2c.../post.json',
11 query: {},
12 headers: {},
13});
For browser-to-browser communication, use WebRTCServer which wraps the Server class.
1// Create WebRTC server (for browser)
2const server = new WebRTCServer(account, storage, {
3 iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
4 allowedPeers: ['allowed-fingerprint-1', 'allowed-fingerprint-2'], // optional whitelist
5});
6
7// Accept incoming connection
8const answer = await server.acceptConnection(connectionId, offer);
9
10// Add ICE candidate
11await server.addIceCandidate(connectionId, candidate);
12
13// Listen for signaling events (ICE candidates)
14server.onSignaling(connectionId, (signal) => {
15 // Send via signaling channel
16 signalingChannel.send(signal);
17});
18
19// Get active connections
20const connections = server.getConnections();
21
22// Close connection
23server.closeConnection(connectionId);
24
25// Stop server
26server.stop();
1// Create WebRTC client
2const client = new WebRTCClient(account, storage, peerFingerprint, {
3 iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
4 timeout: 30000,
5});
6
7// Create offer
8const offer = await client.createOffer();
9
10// Or accept offer and create answer
11const answer = await client.acceptOffer(offer);
12
13// Complete connection with answer
14await client.completeConnection(answer);
15
16// Add ICE candidate
17await client.addIceCandidate(candidate);
18
19// Perform mTLS authentication with retry attempts
20const authenticated = await client.performMTLS({ maxRetries: 3 });
21if (!authenticated) throw new Error('Authentication failed');
22
23// HTTP-style requests over data channel
24const fileList = await client.fetchFileList();
25const fileData = await client.downloadFile('post.json');
26const versionData = await client.downloadFileVersion('post.json', 'hash123');
27
28// Close connection
29client.close();
1// WebSocket signaling (browser)
2import { WebSocketSignaling } from '@mau-network/mau';
3
4const signaling = new WebSocketSignaling('wss://signaling.example.com', fingerprint);
5
6// Send signaling message
7await signaling.send({
8 from: myFingerprint,
9 to: peerFingerprint,
10 type: 'offer',
11 data: offerData,
12});
13
14// Receive messages
15signaling.onMessage((message) => {
16 console.log('Received:', message.type, 'from', message.from);
17});
18
19// Close connection
20signaling.close();
21
22// HTTP signaling (polling-based)
23import { HTTPSignaling } from '@mau-network/mau';
24
25const signaling = new HTTPSignaling('https://signaling.example.com', fingerprint);
26
27// Start polling for messages
28signaling.startPolling();
29
30// Send and receive messages (same interface as WebSocket)
31signaling.onMessage((message) => { /* ... */ });
32await signaling.send(message);
33
34// Stop polling
35signaling.stopPolling();
36
37// Signaled connection helper
38import { SignaledConnection } from '@mau-network/mau';
39
40const connection = new SignaledConnection(signaling, myFingerprint, peerFingerprint);
41
42// Callbacks for connection events
43connection.onOffer(async (offer) => { /* handle offer */ });
44connection.onAnswer(async (answer) => { /* handle answer */ });
45connection.onICECandidate(async (candidate) => { /* handle ICE */ });
46
47// Send connection events
48await connection.sendOffer(offer);
49await connection.sendAnswer(answer);
50await connection.sendICECandidate(candidate);
1// Auto-detect storage backend (uses BrowserStorage with IndexedDB)
2const storage = await createStorage();
3
4// Or create BrowserStorage directly
5import { BrowserStorage } from '@mau-network/mau';
6
7const browserStorage = await BrowserStorage.create();
8
9// Storage interface
10await storage.readFile(path); // => Uint8Array
11await storage.writeFile(path, data); // => void
12await storage.readText(path); // => string
13await storage.writeText(path, text); // => void
14await storage.exists(path); // => boolean
15await storage.mkdir(path); // => void
16await storage.readDir(path); // => string[]
17await storage.remove(path); // => void
18await storage.stat(path); // => { size, isDirectory }
19storage.join('a', 'b', 'c'); // => 'a/b/c'
Mau uses IndexedDB for persistent storage in the browser. The storage abstraction layer provides a file-like interface over IndexedDB’s key-value store.
In browser environments, Mau uses WebRTC data channels instead of traditional HTTP:
┌─────────────┐ ┌─────────────┐
│ Browser A │◄──WebRTC Channel──►│ Browser B │
│ │ │ │
│ WebRTCServer│ │WebRTCClient │
│ (Alice) │ │ (Bob) │
└─────────────┘ └─────────────┘
│ │
└────── Signaling Server ─────────┘
(offer/answer/ICE)
How it works:
Key Components:
Benefits:
All files are encrypted by default to:
When a file is modified, the old version is automatically archived:
hello.json ← Latest version
hello.json.versions/
├── abc123... ← Version 1 (SHA-256 checksum)
└── def456... ← Version 2
This implementation follows the Mau specification:
/p2p/<fpr>, /p2p/<fpr>/<file>, /p2p/<fpr>/<file>.versions/<hash> 1import { staticResolver, dhtResolver, combinedResolver } from '@mau-network/mau';
2
3// Static resolver - hardcoded peer addresses
4const resolver1 = staticResolver({
5 'alice-fingerprint': '192.168.1.100:8080',
6 'bob-fingerprint': 'bob.example.com:8080',
7});
8
9// DHT resolver - discover peers via Kademlia DHT
10const resolver2 = dhtResolver(['bootstrap.mau.social:443']);
11
12// Combined resolver - try multiple strategies
13const resolver3 = combinedResolver([
14 staticResolver(knownPeers),
15 dhtResolver(['bootstrap1:443', 'bootstrap2:443']),
16]);
17
18// Use with client
19const client = Client.create(account, storage, peer, [resolver3]);
These limitations are by design for this browser-focused implementation.
1# Install dependencies
2npm install
3
4# Build
5npm run build
6
7# Run tests
8npm test
9
10# Lint
11npm run lint
12
13# Format
14npm run format
GPL-3.0 - Same as the main Mau project
Contributions welcome! Please open issues and PRs on the main Mau repository.