Out-of-tree port of JSS src/webrtc/index.js.
plugins: [{ module: 'webrtc/plugin.js', prefix: '/webrtc',
config: { maxMessageSize: 65536, maxOffersPerAnnounce: 10,
maxResourcesPerPeer: 50 } }]One WebSocket endpoint at the mount prefix, speaking core's exact wire
protocol (core mounts at /.webrtc; here the prefix decides). Three
dialects on the same socket:
- Identity-based (authenticated): connect with
Authorization: Beareror?token=(browser WebSocket can't set headers), get a{ type: "peers", you, peerId, peers }welcome, then relayoffer/answer/candidate/hangupto a peer by WebID (or did:nostr DID). Presence viapeer-joined/peer-left; reconnects replace the old socket. - Content-addressed (anonymous ok):
announcea hex resource hash — the "room" — with SDP offers; the server relays offers to peers already in the room,answerroutes back by peer id,leaveexits. Rooms are isolated; membership is cleaned up on disconnect. - WebTorrent tracker dialect:
{ action: "announce", info_hash, peer_id, offers }with 20-byte binary or hex ids, for stock WebTorrent clients.
The server only introduces peers — media and data flow directly between
them. Full parity with core, plus: messages arriving while auth is still
resolving are buffered and replayed in order (core can drop an eager
client's first message), deactivate() closes every socket rather than
only identity-registered ones, and the size limits are configurable.
- Zero vendored code — the first wall-free port.
api.auth.getAgentreplaced core's internalsrc/auth/token.jsone-for-one, and it verified Bearer tokens on a raw WebSocket upgrade request unchanged. Everything else wasapi.ws.route+api.prefix+api.config. api.ws.routehandlers can be async and the loader catches rejections (logs, terminates the one socket) — exactly what an auth-then-serve WebSocket needs. The handler also gets the full Fastify request:request.queryfor the?token=fallback, mutablerequest.headersto shim that token in as a Bearer header before callinggetAgent. That shim works but is awkward — candidate seam:getAgent(request, { queryToken: true })or native?token=support, so browser WebSocket auth isn't a copy-paste ritual across plugins.- Timing trap found by the port: the welcome frame can coalesce with
the 101 handshake.
getAgenton a Bearer token resolves without I/O, so the plugin'speerswelcome can land in the same TCP segment as the upgrade response; node'swsclient then emitsmessagesynchronously right afteropen, and any listener attached afterawait opensilently misses it. Core never noticed because its auth path crosses more event-loop ticks. Not a plugin-api gap — a protocol footgun; the test client buffers from socket construction (browsers queue events as tasks and are unaffected). - The port fixes core's own auth race: core attaches its message
handler only after
awaiting auth, so a client that speaks immediately afteropen(legal for the anonymous tracker dialect) can lose messages. Here a buffering listener goes on first, then handlers swap synchronously and the backlog replays in order. - Tests authenticate with zero internals:
POST /.podsreturns{ webId, token }over plain HTTP, so real Bearer credentials come from the public surface — no reaching into the IdP or token modules. - Parity gaps: none. All three dialects, all message types, all error
paths. Only differences are the mount path (prefix vs hardcoded
/.webrtc) and the deliberate improvements above.peer-joined/peer-leftkeep core'swebIdfield name on the wire even though the value may be a did:nostr DID, so existing clients keep working.