This document provides guidance for AI agents working on the Mau TypeScript implementation.
typescript/
├── src/ # Source code
│ ├── account.ts # Account/identity management
│ ├── client.ts # HTTP client for peer sync
│ ├── server.ts # HTTP server for serving files
│ ├── file.ts # File operations with encryption
│ ├── index.ts # Public API exports
│ ├── crypto/ # PGP encryption/signing
│ ├── network/ # P2P networking (WebRTC, resolvers)
│ ├── storage/ # Storage backends (filesystem, IndexedDB)
│ └── types/ # TypeScript type definitions
├── examples/ # Usage examples
├── dist/ # Compiled JavaScript (git-ignored)
├── coverage/ # Test coverage reports (git-ignored)
├── README.md # User documentation
├── BROWSER.md # Browser testing guide
└── package.json # Dependencies and scripts
Test files: `*.test.ts` alongside source files
BrowserStorage)fake-indexeddb, @roamhq/wrtc)"strict": true in tsconfig.json)src/types/index.tsaccount.ts (40%), client.ts (47%), server.ts (48%)dht.ts (13%), signaling.ts (20%), webrtc-server.ts (15%)1npm run build # TypeScript → dist/
2npm run build:browser # Vite bundle for browsers
1npm test # Run all tests with coverage
2npm test -- --watch # Watch mode
3npm test src/file.test.ts # Run specific test file
4npm test -- --no-coverage # Faster runs without coverage
1npm run lint # ESLint check
2npm run format # Prettier format
1npm run docs # Generate HTML docs → docs/
2npm run docs:serve # Generate + serve at http://localhost:8000
1# Browser testing
2npm run dev # Start dev server at http://localhost:5173
3npm run preview # Preview production build
4
5# Node.js testing
6node test-integration.mjs
7
8# Automated browser testing (Playwright)
9npx playwright test
Test Files:
*.test.ts - Jest unit/integration tests (run in Node.js with polyfills)fake-indexeddb and @roamhq/wrtc to simulate browser APIsThe codebase is organized into these key modules:
account.ts, file.ts, client.ts, server.ts, index.tscrypto/pgp.ts, crypto/index.ts - OpenPGP operationsnetwork/webrtc.ts, network/webrtc-server.ts, network/resolvers.ts, network/dht.ts, network/signaling.tsstorage/browser.ts, storage/index.ts (filesystem storage removed)types/index.ts - TypeScript type definitions 1interface Storage {
2 exists(path: string): Promise<boolean>;
3 readFile(path: string): Promise<Uint8Array>;
4 writeFile(path: string, data: Uint8Array): Promise<void>;
5 readText(path: string): Promise<string>;
6 writeText(path: string, text: string): Promise<void>;
7 readDir(path: string): Promise<string[]>;
8 mkdir(path: string): Promise<void>;
9 remove(path: string): Promise<void>;
10 stat(path: string): Promise<{ size: number; isDirectory: boolean; modifiedTime?: number }>;
11 join(...parts: string[]): string;
12}
Implementation:
1type FingerprintResolver = (
2 fingerprint: Fingerprint,
3 timeout?: number
4) => Promise<string | null>;
Available resolvers:
/kad/find_peer endpoint (HTTP-based)Note: DNS and mDNS resolvers were removed as they require Node.js-specific UDP socket access.
Write:
data → sign (with private key) → encrypt (to public keys) → PGP armor → storage
Read:
storage → PGP armor → decrypt (with private key) → verify (with public keys) → data
network/webrtc.ts): Initiates connections, creates offersnetwork/webrtc-server.ts): Accepts connections, handles answersnetwork/signaling.ts): Coordinates peer connection establishmentNote: WebRTC implementation uses native browser APIs. In Node.js tests, @roamhq/wrtc provides polyfill.
1// Use factory methods that accept storage
2const account = await Account.create(storage, rootDir, options);
3const file = File.create(account, storage, 'filename.json');
4const client = Client.create(account, storage, peer);
Promise.all() for parallel operationsPromise.race() for timeouts 1// Custom error classes with codes
2throw new PeerNotFoundError(); // code: 'PEER_NOT_FOUND'
3throw new InvalidFileNameError('reason'); // code: 'INVALID_FILE_NAME'
4
5// Catch specific errors
6try {
7 await client.sync();
8} catch (err) {
9 if (err instanceof PeerNotFoundError) {
10 // Handle peer not found
11 }
12}
1describe('FeatureName', () => {
2 let storage: BrowserStorage;
3 let account: Account;
4
5 beforeEach(async () => {
6 storage = await BrowserStorage.create();
7 account = await Account.create(storage, TEST_DIR, options);
8 });
9
10 afterEach(async () => {
11 try {
12 await storage.remove(TEST_DIR);
13 } catch {
14 // Ignore cleanup errors
15 }
16 });
17
18 it('should do something', async () => {
19 // Test implementation
20 });
21});
afterEach using storage.remove()1// Use Jest mocks sparingly
2const mockResolver = jest.fn().mockResolvedValue('peer:8080');
.js extensions in imports)dist/ directory with declaration files (.d.ts)Important: All imports must use .js extensions even for .ts files:
1// ✅ Correct
2import { Account } from './account.js';
3
4// ❌ Wrong
5import { Account } from './account';
src/index.ts) ¶Core Classes:
Account, Client, Server, File - Main user-facing classesBrowserStorage, createStorage - Storage backendNetworking:
WebRTCClient, WebRTCServer - WebRTC P2P communicationLocalSignalingServer, WebSocketSignaling, HTTPSignaling, SignaledConnection - Signaling mechanismsstaticResolver, dhtResolver, combinedResolver, retryResolver - Peer discoveryKademliaDHT - Distributed hash table for peer discoveryUtilities:
validateFileName, normalizeFingerprint, formatFingerprint - Crypto utilitiescreateAccount, loadAccount - Convenience functionsTypes & Errors:
MauError, PeerNotFoundError, etc.)NOT Exported (Internal Constants):
MAU_DIR_NAME, ACCOUNT_KEY_FILENAME, SYNC_STATE_FILENAME - Internal file structureFILE_PERM, DIR_PERM - Internal permissionsHTTP_TIMEOUT_MS, SERVER_RESULT_LIMIT - Configure via ClientConfig/ServerConfig insteadDHT_B, DHT_K, DHT_ALPHA, etc. - Internal DHT implementation detailsImplementation Details:
generateKeyPair, signAndEncrypt, decryptAndVerify) - wrapped by Account classcrypto/pgp.tsexport * wildcards in src/index.ts@internal tag or leading underscore for private methodsExample:
1// src/my-feature.ts
2
3/**
4 * @internal
5 * Internal helper function - not exported from index.ts
6 */
7export function _internalHelper() { }
8
9/**
10 * Public API function - exported from index.ts
11 * @param data Input data to process
12 */
13export function publicFeature(data: string) {
14 return _internalHelper();
15}
Account, File, WebRTCClient)createAccount, writeJSON)SERVER_RESULT_LIMIT, DHT_K)_ (underscore) or use TypeScript private 1// 1. Imports (grouped: types, libraries, internal)
2import type { Storage } from './types/index.js';
3import * as fs from 'fs/promises';
4import { Account } from './account.js';
5
6// 2. Type definitions
7interface InternalType {
8 // ...
9}
10
11// 3. Constants
12const DEFAULT_TIMEOUT = 30000;
13
14// 4. Main class/functions
15export class Thing {
16 // ...
17}
18
19// 5. Helper functions (private)
20function internalHelper() {
21 // ...
22}
1// ✅ Good: Parallel operations
2const [file1, file2] = await Promise.all([
3 storage.readFile('file1.txt'),
4 storage.readFile('file2.txt'),
5]);
6
7// ❌ Bad: Sequential when parallel is possible
8const file1 = await storage.readFile('file1.txt');
9const file2 = await storage.readFile('file2.txt');
10
11// ✅ Good: Timeout handling
12await Promise.race([
13 operation(),
14 new Promise((_, reject) =>
15 setTimeout(() => reject(new Error('Timeout')), 5000)
16 ),
17]);
src/types/index.tssrc/index.ts if public APIAlways parallelize independent async operations:
1// ✅ Fast: Parallel execution
2const [account, config] = await Promise.all([
3 loadAccount(dir, passphrase),
4 loadConfig(configPath),
5]);
6
7// ❌ Slow: Sequential execution
8const account = await loadAccount(dir, passphrase);
9const config = await loadConfig(configPath); // Waits unnecessarily
Network operations use p-retry for resilience:
1import pRetry from 'p-retry';
2
3await pRetry(
4 async () => {
5 const response = await fetch(url);
6 if (!response.ok) throw new Error('HTTP error');
7 return response;
8 },
9 { retries: 3, minTimeout: 1000 }
10);
Uint8Array) after processing1// ✅ Correct: Always use BrowserStorage
2const storage = await createStorage(); // Creates BrowserStorage with IndexedDB
3
4// ✅ Or create directly
5const storage = await BrowserStorage.create();
Browser-Only Architecture:
This package is designed exclusively for browsers:
1// ✅ Available: Browser-compatible peer discovery
2const staticRes = staticResolver(knownPeers);
3const dhtRes = dhtResolver(['bootstrap1:443']); // Uses fetch()
4
5// ✅ Combined resolver works in browsers
6const resolver = combinedResolver([
7 staticResolver(knownPeers),
8 dhtResolver(['bootstrap1:443']),
9]);
Why this is browser-only:
1// ❌ Wrong: Async constructor
2class Thing {
3 constructor() {
4 this.init(); // Can't await in constructor
5 }
6}
7
8// ✅ Right: Factory method
9class Thing {
10 private constructor() {}
11
12 static async create(): Promise<Thing> {
13 const instance = new Thing();
14 await instance.init();
15 return instance;
16 }
17}
1// ❌ Wrong: Storing unencrypted private keys
2localStorage.setItem('privateKey', privateKeyArmor);
3
4// ✅ Right: Always encrypted with passphrase
5await account.save(passphrase); // Encrypts before storage
1// ❌ Wrong: Hardcoded separators
2const path = rootDir + '/' + filename;
3
4// ✅ Right: Use storage.join()
5const path = storage.join(rootDir, filename);
1// ❌ Wrong: Forget to close connections
2const client = new WebRTCClient(...);
3await client.connect();
4// ... use client ...
5
6// ✅ Right: Always close
7try {
8 const client = new WebRTCClient(...);
9 await client.connect();
10 // ... use client ...
11} finally {
12 client.close();
13}
1// Set environment variable
2DEBUG=mau:* npm test
3
4// Or in code
5localStorage.setItem('debug', 'mau:*'); // Browser
6process.env.DEBUG = 'mau:*'; // Node.js
1// Verify signatures are working
2const file = File.create(account, storage, 'test.json');
3await file.writeJSON({ test: true });
4const data = await file.read(); // Should not throw
5
6// Check signature manually
7const publicKeys = account.getAllPublicKeys();
8const { verified } = await decryptAndVerify(armor, privateKey, publicKeys);
9console.log('Signature verified:', verified);
1// Log ICE candidates
2client.on('icecandidate', (candidate) => {
3 console.log('ICE candidate:', candidate);
4});
5
6// Check connection state
7console.log('Connection state:', client.connectionState);
8console.log('Data channel state:', client.dataChannelState);
1// Browser: Check IndexedDB
2// Open DevTools → Application → IndexedDB → mau-storage → files
3
4// Node.js: Check filesystem
5console.log(await storage.readDir(rootDir));
“Cannot find module” errors:
.js extensions (ES modules requirement)type: "module" is set in package.jsonWebRTC connection failures:
DEBUG=mau:* npm testTest timeouts:
jest.setTimeout(30000)--detectOpenHandles to find leaksBrowser IndexedDB errors:
Before submitting PR:
npm test)npm run lint)npm run format)npm run build)npm run build:browser)npm run docs)crypto.getRandomValues)GitHub Actions workflows handle:
Local CI simulation:
1npm run lint && npm test && npm run build && npm run build:browser
../docs/ directory../ (reference implementation)npm test -- --verbose for detailed outputDEBUG=mau:* environment variableRemember: This implementation must work in both browser and Node.js. Test both environments before submitting changes.
| Task | Command |
|---|---|
| Install dependencies | npm install |
| Run tests | npm test |
| Run tests (watch) | npm test -- --watch |
| Check coverage | npm test -- --coverage |
| Build for Node.js | npm run build |
| Build for browser | npm run build:browser |
| Start dev server | npm run dev |
| Lint code | npm run lint |
| Format code | npm run format |
| Generate docs | npm run docs |
| Serve docs | npm run docs:serve |