feat: add WebRTC signaling server plugin#208
Conversation
Lightweight signaling server for WebRTC peer-to-peer connections. Relays SDP offers/answers and ICE candidates between authenticated users. The actual media/data flows directly between peers. - New plugin: src/webrtc/index.js (~110 lines) - CLI: --webrtc, --webrtc-path - Env: JSS_WEBRTC, JSS_WEBRTC_PATH - WebSocket endpoint at /.webrtc (default) - WebID-based authentication required - Peer presence notifications (join/leave) Fixes #207
- 6 tests covering auth, peer presence, full signaling relay, error handling - Skip WAC and dotfile checks for /.webrtc path - Fix reconnection race: old socket close handler no longer deletes the replacement socket from the peers map
There was a problem hiding this comment.
Pull request overview
Adds an optional WebRTC signaling plugin to JSS, exposing a WebSocket endpoint for authenticated peers to exchange SDP/ICE and receive join/leave presence events.
Changes:
- Introduces
src/webrtc/index.jsFastify plugin implementing signaling + peer presence. - Adds CLI/config/env support for
--webrtcand--webrtc-path. - Documents the new feature and adds a basic signaling lifecycle test.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/webrtc/index.js |
New WebRTC signaling WebSocket route + in-memory peer registry. |
src/server.js |
Wires plugin behind webrtc flag; adjusts dotfile allowlist and auth bypass. |
src/config.js |
Adds defaults + env var mapping for WebRTC settings. |
bin/jss.js |
Adds CLI options and prints WebRTC status on startup. |
README.md |
Documents WebRTC signaling usage and protocol. |
test/webrtc.test.js |
Adds coverage for auth, presence, and message relay behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| // Connected peers: webId → WebSocket | ||
| const peers = new Map(); | ||
|
|
| export async function webrtcPlugin(fastify, options = {}) { | ||
| const path = options.path || '/.webrtc'; | ||
|
|
||
| await fastify.register(websocket); |
| socket.on('message', (data) => { | ||
| let msg; | ||
| try { | ||
| msg = JSON.parse(data.toString()); | ||
| } catch { | ||
| socket.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' })); | ||
| return; | ||
| } | ||
|
|
||
| if (!msg.to || !msg.type) { | ||
| socket.send(JSON.stringify({ type: 'error', message: 'Missing "to" or "type" field' })); | ||
| return; | ||
| } | ||
|
|
||
| const target = peers.get(msg.to); | ||
| if (!target || target.readyState !== 1) { | ||
| socket.send(JSON.stringify({ type: 'error', message: 'Peer not online', peer: msg.to })); | ||
| return; |
| // This prevents exposure of .git/, .env, .htpasswd, etc. | ||
| // Git protocol requests bypass this check when git is enabled | ||
| const ALLOWED_DOTFILES = ['.well-known', '.acl', '.meta', '.pods', '.notifications', '.account']; | ||
| const ALLOWED_DOTFILES = ['.well-known', '.acl', '.meta', '.pods', '.notifications', '.account', '.webrtc']; |
| const timer = setTimeout(() => reject(new Error(`Timeout waiting for "${type}"`)), timeout); | ||
| ws.on('message', function handler(data) { | ||
| const msg = JSON.parse(data.toString()); | ||
| if (msg.type === type) { | ||
| clearTimeout(timer); | ||
| ws.removeListener('message', handler); | ||
| resolve(msg); | ||
| } | ||
| }); |
- Move peers Map inside plugin (instance-scoped), add onClose cleanup hook - Guard @fastify/websocket registration to avoid duplicates - Allowlist signaling types (offer/answer/candidate/hangup), enforce 64KB max - Only add dotfile exception when webrtcEnabled, derive from webrtcPath - Fix test waitForMessage to clean up listeners on timeout/close
There was a problem hiding this comment.
Pull request overview
Adds an optional WebRTC signaling feature to JSS, exposing a WebSocket endpoint for relaying SDP/ICE messages between authenticated peers.
Changes:
- Introduces a Fastify WebRTC signaling plugin with peer presence notifications and relay logic.
- Wires new
webrtc/webrtcPathconfiguration through CLI flags, config defaults, env vars, and server registration. - Adds README documentation and a new automated test suite for the signaling behavior.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/webrtc/index.js |
New WebRTC signaling WebSocket plugin (auth + relay + presence). |
src/server.js |
Registers the plugin when enabled; updates dotfile allowlist and WAC-skip routing logic. |
src/config.js |
Adds webrtc defaults and env var mapping. |
bin/jss.js |
Adds CLI flags and passes WebRTC config into server startup. |
test/webrtc.test.js |
Adds tests for auth, presence notifications, relay lifecycle, and errors. |
README.md |
Documents the new WebRTC feature and protocol. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| socket.on('message', (data) => { | ||
| // Enforce max message size | ||
| if (data.length > MAX_MESSAGE_SIZE) { | ||
| socket.send(JSON.stringify({ type: 'error', message: 'Message too large' })); | ||
| return; | ||
| } |
| // Relay the message, replacing 'to' with 'from' | ||
| const relay = { ...msg, from: webId }; | ||
| delete relay.to; | ||
| target.send(JSON.stringify(relay)); |
| if (webrtcEnabled) ALLOWED_DOTFILES.push(webrtcPath.split('/').pop()); | ||
| fastify.addHook('onRequest', async (request, reply) => { |
| request.url.startsWith('/storage/') || | ||
| (payEnabled && isPayRequest(request.url)) || | ||
| (mongoEnabled && (request.url === '/db' || request.url.startsWith('/db/'))) || | ||
| (webrtcEnabled && request.url.startsWith(webrtcPath)) || |
- Use Buffer.byteLength for reliable message size enforcement - Whitelist relay payload fields to prevent prototype pollution - Exact path match for dotfile and WAC bypass (no prefix leakage) - Dotfile exception only via early-return hook, not ALLOWED_DOTFILES array
There was a problem hiding this comment.
Pull request overview
Adds an optional WebRTC signaling WebSocket plugin to JSS, enabling WebID-authenticated peers to exchange SDP/ICE messages via a lightweight relay endpoint.
Changes:
- Introduces a new
webrtcPlugin(/.webrtcby default) to relay offers/answers/candidates and emit peer presence events. - Wires WebRTC enablement and path through server config, CLI flags, and environment variables.
- Adds automated tests plus README documentation for the new signaling endpoint.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/webrtc/index.js |
New Fastify WebSocket signaling plugin with peer tracking and message relay. |
src/server.js |
Registers the plugin behind a feature flag and allows the dotfile-like endpoint through existing guards. |
src/config.js |
Adds webrtc / webrtcPath defaults and env var mappings. |
bin/jss.js |
Adds CLI flags and prints WebRTC status on startup. |
test/webrtc.test.js |
New end-to-end WebSocket tests for auth, signaling relay, and presence events. |
README.md |
Documents new CLI flags and the WebRTC signaling protocol. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| * → { type: "offer", to: "<webid>", sdp: "..." } | ||
| * → { type: "answer", to: "<webid>", sdp: "..." } | ||
| * → { type: "candidate", to: "<webid>", candidate: {...} } | ||
| * → { type: "hangup", to: "<webid>" } | ||
| * ← { type: "offer", from: "<webid>", sdp: "..." } | ||
| * ← { type: "answer", from: "<webid>", sdp: "..." } | ||
| * ← { type: "candidate", from: "<webid>", candidate: {...} } | ||
| * ← { type: "hangup", from: "<webid>" } | ||
| * ← { type: "error", message: "..." } | ||
| * ← { type: "peers", peers: ["<webid>", ...] } |
| // Register this peer (close old connection if reconnecting) | ||
| const existing = peers.get(webId); | ||
| if (existing) { | ||
| peers.delete(webId); | ||
| existing.close(); | ||
| } | ||
| peers.set(webId, socket); | ||
| socket.webId = webId; | ||
|
|
||
| // Notify the peer of their identity and online peers | ||
| socket.send(JSON.stringify({ | ||
| type: 'peers', | ||
| you: webId, | ||
| peers: [...peers.keys()].filter(id => id !== webId) | ||
| })); | ||
|
|
||
| // Notify other peers that someone came online | ||
| broadcast(webId, { type: 'peer-joined', webId }); | ||
|
|
| socket.on('error', () => { | ||
| if (peers.get(webId) === socket) { | ||
| peers.delete(webId); |
|
|
||
| ## WebRTC Signaling | ||
|
|
||
| Peer-to-peer communication via WebRTC, using JSS as the signaling server. Once peers are connected, all media and data flows directly between them. |
| /** Connect an authenticated WebSocket for a pod user, waits for open */ | ||
| function connectPeer(podName) { | ||
| const token = getPodToken(podName); | ||
| const ws = new WebSocket(wsUrl, { | ||
| headers: { 'Authorization': `Bearer ${token}` } | ||
| }); | ||
| return ws; |
- Document peer-joined/peer-left/you in protocol comment - Only broadcast peer-joined for new connections, not reconnects - Let error handler defer cleanup to close handler - Grammar: "flows" → "flow" - Fix misleading test docstrings
There was a problem hiding this comment.
Pull request overview
Adds an optional WebRTC signaling server feature to JSS, exposing a WebSocket endpoint that relays SDP/ICE messages between authenticated peers and integrates it into the CLI/config/docs.
Changes:
- Introduces a new Fastify WebRTC signaling plugin (
/.webrtcby default) with peer join/leave notifications and message relaying. - Wires the feature into server startup, config/env mapping, and CLI flags (
--webrtc,--webrtc-path). - Adds a new test suite validating authentication, presence, and basic signaling relay behavior.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| test/webrtc.test.js | Adds end-to-end tests for the new WebRTC WebSocket signaling endpoint. |
| src/webrtc/index.js | Implements the WebRTC signaling server plugin (auth, peer tracking, relay). |
| src/server.js | Registers the plugin when enabled and adjusts auth/dotfile bypass logic for the endpoint. |
| src/config.js | Adds webrtc / webrtcPath defaults and env var mapping. |
| bin/jss.js | Adds CLI flags and passes config through to server creation. |
| README.md | Documents the new CLI flags and introduces a WebRTC signaling section. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| if (typeof msg.sdp === 'string') relay.sdp = msg.sdp; | ||
| if (msg.candidate != null && typeof msg.candidate === 'object' && !Array.isArray(msg.candidate)) { | ||
| relay.candidate = msg.candidate; | ||
| } |
| // Enforce max message size (Buffer.byteLength for reliable byte count) | ||
| const raw = Buffer.isBuffer(data) ? data : Buffer.from(data); | ||
| if (raw.byteLength > MAX_MESSAGE_SIZE) { | ||
| socket.send(JSON.stringify({ type: 'error', message: 'Message too large' })); | ||
| return; | ||
| } |
| export JSS_MONGO=true | ||
| export JSS_MONGO_URL=mongodb://localhost:27017 | ||
| export JSS_MONGO_DATABASE=solid | ||
| export JSS_WEBRTC=true |
| // Only relay known signaling types | ||
| if (!ALLOWED_TYPES.has(msg.type)) { | ||
| socket.send(JSON.stringify({ type: 'error', message: `Unknown type "${msg.type}"` })); | ||
| return; | ||
| } |
| if (msg.candidate != null && typeof msg.candidate === 'object' && !Array.isArray(msg.candidate)) { | ||
| relay.candidate = msg.candidate; | ||
| } | ||
| target.send(JSON.stringify(relay)); |
| // Allow WebRTC signaling endpoint through when enabled | ||
| const urlNoQuery = request.url.split('?')[0]; | ||
| if (webrtcEnabled && urlNoQuery === webrtcPath) { | ||
| return; | ||
| } |
| const data = JSON.stringify(msg); | ||
| for (const [id, socket] of peers) { | ||
| if (id !== senderWebId && socket.readyState === 1) { | ||
| socket.send(data); |
| if (!msg.to || !msg.type) { | ||
| socket.send(JSON.stringify({ type: 'error', message: 'Missing "to" or "type" field' })); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Pull request overview
Adds an optional WebRTC signaling server to JSS, enabling authenticated peers to exchange SDP/ICE over a dedicated WebSocket endpoint for establishing peer-to-peer connections.
Changes:
- Introduces a new
webrtcPluginimplementing authenticated signaling relay and peer presence over WebSocket. - Wires the feature into server startup, config/env support, and CLI flags (
--webrtc,--webrtc-path). - Adds automated tests plus README documentation for the new signaling protocol.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/webrtc.test.js | Adds end-to-end tests for auth, presence, relay, and error cases. |
| src/webrtc/index.js | Implements the signaling WebSocket route, peer tracking, and message relay. |
| src/server.js | Registers the plugin when enabled and bypasses dotfile/WAC checks for the signaling endpoint. |
| src/config.js | Adds defaults and environment-variable mapping for WebRTC feature flags. |
| bin/jss.js | Exposes CLI options and passes config through to createServer(). |
| README.md | Documents CLI usage, environment variable, and the signaling message format. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| // Error handler: close event will follow and handle cleanup | ||
| socket.on('error', () => {}); | ||
| }); |
| // WebRTC signaling is OFF by default | ||
| const webrtcEnabled = options.webrtc ?? false; | ||
| const webrtcPath = options.webrtcPath ?? '/.webrtc'; | ||
| // ActivityPub federation is OFF by default |
| /** Collect messages from a WebSocket for a duration */ | ||
| function collectMessages(ws, duration = 500) { | ||
| return new Promise((resolve) => { | ||
| const msgs = []; | ||
| const handler = (data) => msgs.push(JSON.parse(data.toString())); | ||
| ws.on('message', handler); | ||
| setTimeout(() => { | ||
| ws.removeListener('message', handler); | ||
| resolve(msgs); | ||
| }, duration); | ||
| }); | ||
| } |
Summary
--webrtc,--webrtc-path/.webrtc(default)How it works
JSS acts as the matchmaker — once peers exchange SDP/ICE via the signaling server, all media and data flows directly between them. No new dependencies.
Test plan
Fixes #207