ghostmesh
v1.0.2
Published
GhostMesh is a WebRTC P2P messaging library with WebSocket tracker signaling, relay routing and onion-style multi-hop delivery
Maintainers
Readme
GhostMesh
GhostMesh is a WebRTC P2P messaging and file-transfer library that uses WebTorrent-style WebSocket trackers as the signaling layer.
The main class is GhostMesh. A P2PT alias is still exported for backward compatibility.
It works in:
- browsers
- Node.js
- TypeScript projects
Overview
GhostMesh helps you:
- discover peers through WebSocket trackers
- exchange direct messages and JSON payloads
- send large messages with automatic chunking
- send files with progress tracking through
FileSession - stream media progressively in the browser
- configure STUN and TURN servers with
setOptions() - use relay or lightweight onion-style multi-hop routing
- expose a hidden master service through entry peers with encrypted request/response payloads
Installation
npm
npm install ghostmeshNode.js WebRTC support
If you want to run GhostMesh in Node.js, also install the optional WebRTC package:
npm install ghostmesh @roamhq/wrtcGhostMesh loads @roamhq/wrtc automatically when start() runs in Node.js.
CDN
jsDelivr from GitHub
<script src="https://cdn.jsdelivr.net/gh/kalmonv/GhostMesh/dist/ghostmesh.iife.js"></script>jsDelivr from npm
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ghostmesh.iife.js"></script>When using the IIFE build in the browser, the constructor is exposed directly as window.GhostMesh:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ghostmesh.iife.js"></script>
<script>
const gmesh = new GhostMesh(['wss://tracker.openwebtorrent.com'], 'my-app')
</script>Live Demo
Try the interactive multi-peer chat and file transfer demo on CodePen:
More Resources
Additional documentation:
JavaScript examples:
- server example - hidden-service server example
- client example - hidden-service client example
- open chat example - open chat example
Quick Start
import GhostMesh from 'ghostmesh'
const trackers = [
'wss://tracker.openwebtorrent.com',
'wss://tracker.btorrent.xyz',
'wss://tracker.webtorrent.dev'
]
const gmesh = new GhostMesh(trackers, 'my-app')
gmesh.on('peerconnect', async (peer) => {
console.log('peer connected', peer.id)
const [responsePeer, response] = await gmesh.send(peer, {
hello: 'world'
})
console.log('response from', responsePeer.id, response)
})
gmesh.on('msg', async (peer, msg) => {
console.log('message from', peer.id, msg)
await peer.respond({ ok: true })
})
await gmesh.start()Core API
Constructor
const gmesh = new GhostMesh(trackers, identifier)trackersArray of WebSocket tracker URLs.identifierShared identifier used to discover peers on the same network.
Main events
peerconnectFired when a peer becomes available.msgFired when a full message is received.dataFired for raw incoming chunks.peercloseFired when a peer disconnects.trackerconnectFired when a tracker connection succeeds.trackerwarningFired when a tracker warns or fails.onionmsgFired when a routed multi-hop message reaches its final target.fileFired when a file transfer starts and gives you aFileSession.
Messaging
Send a string
await gmesh.send(peer, 'hello')Send JSON
await gmesh.send(peer, {
type: 'chat',
text: 'hello'
})Respond to a message
gmesh.on('msg', async (peer, msg) => {
console.log(msg)
await peer.respond({ ok: true })
})Large messages
GhostMesh automatically chunks and reassembles large payloads, so send() works for regular strings and JSON objects without extra setup.
File Transfers
Send a file
const session = await gmesh.sendFile(peer, file, {
chunkSize: 12 * 1024,
metadata: {
purpose: 'video'
}
})
console.log(session.transferId)
console.log(session.status)
console.log(session.progress)
session.on('progress', current => {
console.log(current.receivedBytes, current.progress)
})
session.on('complete', current => {
console.log('sent', current.transferId)
})sendFile() returns immediately with a live FileSession, so you can monitor and control the transfer while it is still running.
Receive a file
gmesh.on('file', async (peer, file) => {
console.log('incoming file from', peer.id, file.name)
file.on('progress', session => {
console.log(session.receivedBytes, session.progress)
})
file.on('complete', async session => {
const blob = await session.blob()
console.log('ready', blob.size)
})
})FileSession
FileSession is the object returned by sendFile() and also the object emitted by the file event.
Properties
transferIdnamemimeTypesizechunkSizetotalChunksreceivedBytesmetadatapeerIddirectionstatusprogresscanceled
Methods
blob()Resolves to the finalBlob.readRange(start, end)Reads a byte range from the local transfer cache.stream(options?)Returns aReadableStream<Uint8Array>.streamTo(element)Streams to<video>,<audio>or<img>in the browser when possible.waitUntilComplete()Resolves when the transfer completes.pause()Pauses an outgoing transfer.resume()Resumes a paused outgoing transfer.cancel(reason?)Cancels the transfer and notifies the remote peer.destroy()Alias forcancel('Transfer destroyed').
Session events
progresscompleteerrorpauseresumecancel
Media Streaming
GhostMesh includes a progressive file-transfer layer on top of the message transport.
Current protocol flow:
- sender emits
file-offer - sender emits ordered
file-chunkpackets - receiver may request targeted byte ranges with
file-range-request - sender emits
file-end - receiver works with a
FileSession
Browser video playback
gmesh.on('file', async (_peer, file) => {
const video = document.querySelector('video')
if (video) {
await file.streamTo(video)
}
})The browser demo uses FileSession.streamTo(...) to start compatible videos before the entire file is downloaded. If MediaSource is not usable for that file, GhostMesh falls back to progressive blob-based playback.
Current trade-offs
- transfers are optimized for simplicity, not peak throughput
- chunks are serialized as base64 inside the current protocol
- the main transfer path is still sequential
- incoming browser transfers can use
IndexedDBas a cache-backed store for larger files - range-based playback support is still lightweight compared to a system like WebTorrent
Transport Options
Use setOptions() before start() to configure WebRTC and default routing behavior.
const gmesh = new GhostMesh(trackers, 'my-app')
gmesh.setOptions({
timeout: 60_000,
iceTransportPolicy: 'relay',
iceServers: [
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'pass'
}
],
onion: {
through: [relayPeerId]
}
})
await gmesh.start()Supported options
timeoutDefault response timeout in milliseconds. Default:60000.iceServersPassed toRTCPeerConnection.iceTransportPolicyUseful when you want to force relay mode.onionDefault routed delivery strategy forsend()andsendFile().OnionAccepted as a compatibility alias.identityOptional local role and RSA key material used by hidden services.hiddenServiceHidden-master routing, entry peer selection, response shaping and master public key settings.
Privacy note
Using iceTransportPolicy: 'relay' reduces direct IP exposure to peers, but it does not make WebRTC anonymous. Your TURN server still sees relay activity and metadata.
Hidden Master Services
GhostMesh now includes a lightweight hidden-service layer on top of the existing onion routing.
The goal is:
- clients only know one entry peer
- entry peers know how to reach the hidden master
- the request body is encrypted with the master's public key
- the response body is encrypted for the requesting client
- entry peers can simulate a minimum route depth with delayed responses when the mesh is small
Hidden service options
gmesh.setOptions({
identity: {
role: 'client',
publicKey: CLIENT_PUBLIC_KEY_PEM
},
hiddenService: {
serviceName: 'directory',
entryPeers: ['entry-peer-id-1', 'entry-peer-id-2'],
masterPublicKey: MASTER_PUBLIC_KEY_PEM,
revealServer: false,
minHops: 3,
responseDelayMs: [1000, 5000],
fixedPacketBytes: 4096
}
})Client request
const response = await gmesh.requestHiddenService('directory', {
action: 'lookup',
username: 'alice'
})
console.log(response)If the client does not provide identity.publicKey and identity.privateKey, requestHiddenService() creates a temporary RSA keypair for that request and uses it for the encrypted response.
Entry peer setup
Entry peers forward hidden-service requests without being able to read the encrypted body.
gmesh.setOptions({
identity: {
role: 'entry'
},
hiddenService: {
role: 'entry',
services: {
directory: MASTER_PEER_ID
},
minHops: 3,
responseDelayMs: [1000, 5000],
fixedPacketBytes: 4096
}
})When the route to the master is shorter than minHops, the entry peer keeps the response for a random delay inside responseDelayMs so a short path looks less obvious.
Optional server discovery
By default, a hidden-service server does not reveal itself to discovery probes.
If you want a server to prove that it is the configured master, enable:
gmesh.setOptions({
identity: {
role: 'master',
privateKey: MASTER_PRIVATE_KEY_PEM
},
hiddenService: {
role: 'master',
serviceName: 'directory',
revealServer: true
}
})Then a peer can check:
const isServer = await peer.isServer()
console.log(isServer)peer.isServer() only returns true when:
hiddenService.masterPublicKeyis configured on the caller- the remote peer has
revealServer: true - the remote peer proves possession of the matching private key
If revealServer is left as false (the default), peer.isServer() returns false, while the hidden-service server can still answer requests without announcing itself.
Master setup
The master only needs its private key and a handler for the service name.
gmesh.setOptions({
identity: {
role: 'master',
privateKey: MASTER_PRIVATE_KEY_PEM
},
hiddenService: {
role: 'master',
serviceName: 'directory',
fixedPacketBytes: 4096
}
})
gmesh.handleHiddenService('directory', async (payload, context) => {
console.log('hidden request', context.requestId, payload)
return {
ok: true,
peers: ['alice', 'bob']
}
})Key format
identity.publicKey, identity.privateKey and hiddenService.masterPublicKey accept:
- PEM strings for RSA-OAEP keys
- already-imported
CryptoKeyinstances
GhostMesh uses Web Crypto to encrypt the hidden-service body with AES-GCM and wraps the AES key with RSA-OAEP.
Relay and Onion Routing
GhostMesh includes a lightweight onion-style routing base. This is best thought of as relay-based obfuscation, not strong anonymity.
For most apps, the recommended approach is:
- configure routing defaults once with
setOptions() - call
send()andsendFile()normally
Automatic routing with send()
gmesh.setOptions({
timeout: 60_000,
onion: {
hops: 2
}
})
const [peer, response] = await gmesh.send(targetPeer, {
type: 'chat',
text: 'hello through the default route'
})Explicit relay with sendOnion()
gmesh.sendOnion(targetPeerId, {
type: 'chat',
text: 'hello through relay'
}, {
through: [relayPeerId]
})Receive routed messages
gmesh.on('onionmsg', (peer, msg, route) => {
console.log('delivered by', peer.id)
console.log('circuit', route.circuitId)
console.log('payload', msg)
})Current limitations
- intermediate peers can still inspect payloads
- there is no layered encryption yet
- timing analysis is still possible
How Peer Discovery Works
GhostMesh uses a shared identifier to connect peers interested in the same application or room.
Flow:
- you create a
GhostMeshinstance with an identifier - GhostMesh converts that identifier into an info hash
- that info hash is announced to WebSocket trackers
- trackers return peers using the same identifier
- peers establish WebRTC data channels
Example:
const gmesh = new GhostMesh(trackers, 'my-chat-room')Demo
This repository includes a browser demo in:
The demo supports:
- direct messaging
- relay mode through a chosen peer
- onion-style mode with configurable hops
- file uploads and downloads
- progressive media preview
Development
Start the demo
npm run devStart local trackers
npm run trackersRun tests
npm testBuild
npm run buildBuild output:
dist/ghostmesh.jsdist/ghostmesh.cjsdist/ghostmesh.iife.jsdist/ghostmesh.d.ts
Publishing and jsDelivr
If you want jsDelivr to serve files directly from your GitHub repository, commit and push dist/.
Typical release flow:
npm run build
git add dist README.md package.json
git commit -m "build: update dist"
git pushAfter that, jsDelivr can serve:
<script src="https://cdn.jsdelivr.net/gh/kalmonv/GhostMesh/dist/ghostmesh.iife.js"></script>More Docs
More API detail is available in:
