Encrypted messenger for AI agents using Signal Protocol (X3DH + Double Ratchet).
murmur/
├── src/
│ ├── index.ts # Main exports
│ ├── crypto/ # Cryptographic primitives
│ │ ├── utils.ts # Base64, random bytes, byte manipulation
│ │ ├── dh.ts # X25519 Diffie-Hellman
│ │ ├── signing.ts # Ed25519 signatures
│ │ ├── kdf.ts # HKDF/HMAC key derivation
│ │ ├── aead.ts # ChaCha20-Poly1305 encryption
│ │ └── index.ts # Crypto exports
│ ├── x3dh/ # X3DH key agreement
│ │ ├── types.ts # Type definitions
│ │ ├── x3dh.ts # Protocol implementation
│ │ └── index.ts # X3DH exports
│ ├── ratchet/ # Double Ratchet implementation
│ │ ├── types.ts # Type definitions
│ │ ├── header.ts # Message header encoding
│ │ ├── state.ts # State serialization
│ │ ├── ratchet.ts # Core algorithm
│ │ └── index.ts # Ratchet exports
│ ├── session/ # High-level Session API
│ │ ├── types.ts # Session type definitions
│ │ ├── session.ts # Session management
│ │ └── index.ts # Session exports
│ └── integration.test.ts # Full protocol tests
├── bin/
│ └── murmur.mjs # CLI entry point
├── package.json
├── tsconfig.json
└── vitest.config.ts
- Strict TypeScript: All types explicit, no
any - ESM only: Use
.jsextensions in imports - Noble crypto: Use @noble/* libraries for all cryptography
- Comprehensive JSDoc: Document all public functions
- Co-located tests:
*.test.tsnext to implementation
- Run tests and typechecks before finishing work.
- Always commit and push changes when done.
- Use Angular-style commit messages (e.g.,
feat(cli): add webhook sync).
- All cryptographic keys are Uint8Array - never strings internally
- Base64 encoding is only for serialization - use encodeBase64/decodeBase64
- State is mutable - ratchetEncrypt/ratchetDecrypt modify state in place
- Errors throw - no null returns for cryptographic failures
yarn test # Run vitest tests
yarn test:watch # Watch mode
yarn typecheck # TypeScript validation
yarn build # Build for distribution@noble/curves: X25519 for Diffie-Hellman@noble/hashes: SHA-256, HMAC, HKDF@noble/ciphers: ChaCha20-Poly1305node:sqlite: SQLite database (Node, experimental)zod: Schema validation
X3DH establishes a shared secret between parties who may not be online simultaneously:
- Bob publishes: Identity key + Signed prekey + One-time prekeys
- Alice fetches: Bob's prekey bundle from server
- Alice computes: Shared secret from multiple DH operations
- Bob computes: Same shared secret using his private keys
The Double Ratchet has two key operations:
-
DH Ratchet (
dhRatchet): Called when receiving a new ratchet public key. Introduces new entropy from Diffie-Hellman. -
Symmetric Ratchet (
kdfCK): Called for each message. Advances chain key to derive message key.
Message keys are one-time use. After decryption, they are deleted. Skipped message keys are stored for out-of-order message handling.
1. Bob: initializeKeyStore() → publish prekey bundle to server
2. Alice: Fetch bundle → x3dhSender() → initializeAlice()
3. Bob: x3dhReceiver() → initializeBob() → consumeOneTimePreKey()
4. Both: ratchetEncrypt/ratchetDecrypt for messaging
5. Both: serializeState for persistence
- Never log or expose secret keys
- Use constant-time comparison for authentication
- Zero secret memory when done (call zeroBytes)
- Validate all inputs before cryptographic operations
Use murmur subcommands to sign in, send messages, sync, and view recent history.
For testing best practices and feedback loop optimization, see:
- Main Guide: GUIDE_FEEDBACK_LOOP.md - Complete feedback loop manifesto
- Subguides: feedback-loop/ - Detailed guides by topic
Key principles:
- LOCAL FEEDBACK ONLY - Never wait for CI
- DON'T MOCK SERVICES YOU OWN - Run PGlite, SQLite :memory:, local servers
- NARROW THE SCOPE - One function, one test file
- KEEP ENVIRONMENT WORKING - Tests pass before and after changes
Quick reference:
- Unit testing:
yarn testornpx vitest - PostgreSQL testing: Use PGlite (no Docker needed)
- Watch mode:
npx vitest --watch - TDD workflow: Red → Green → Refactor