npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@ajna-inc/webrtc

v0.2.1

Published

DIDComm v2 signaling protocol for WebRTC P2P calls with full NAT/firewall traversal support.

Readme

@ajna-inc/webrtc

DIDComm v2 signaling protocol for WebRTC P2P calls with full NAT/firewall traversal support.

  • PIURI: https://didcomm.org/webrtc/1.0
  • Messages: propose, offer, answer, ice, renegotiate, end
  • Advertises support via Discover Features; works with mediators and message pickup.

Features

  • Module-level ICE server defaults - Configure STUN/TURN servers once
  • ICE policy control - all, relay-preferred, or relay-only for strict firewall environments
  • ICE restart support - Recover from failed connections
  • Trickle ICE - Efficient candidate exchange
  • Propose message - Share ICE servers before SDP exchange

Install

This package is part of the monorepo. Add the module to your Agent options:

import { WebRTCModule } from '@ajna-inc/webrtc'

const agent = new Agent({
  config: { /* ... */ },
  dependencies: agentDependencies,
  modules: {
    webrtc: new WebRTCModule({
      // Configure default ICE servers (optional - has sensible defaults)
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { urls: 'turn:turn.example.org:3478', username: 'user', credential: 'pass' },
      ],
      // Default ICE policy: 'all' | 'relay-preferred' | 'relay-only'
      defaultPolicy: 'relay-preferred',
      // Enable trickle ICE by default
      defaultTrickle: true,
    }),
  },
})

NAT/Firewall Traversal

For peers behind restrictive NAT or firewalls, use TURN servers:

const agent = new Agent({
  modules: {
    webrtc: new WebRTCModule({
      iceServers: [
        // STUN for simple NAT
        { urls: 'stun:stun.l.google.com:19302' },
        // TURN for symmetric NAT and firewalls
        { urls: 'turn:turn.yourserver.com:3478', username: 'user', credential: 'pass' },
        // TURNS (TLS) for stricter firewalls
        { urls: 'turns:turn.yourserver.com:5349', username: 'user', credential: 'pass' },
      ],
      // Force relay-only for maximum compatibility (hides IP addresses too)
      defaultPolicy: 'relay-only',
    }),
  },
})

Frontend usage (browser)

Below is a setup for P2P WebRTC using Credo TS for signaling.

// Get configured ICE servers from module
const iceServers = agent.modules.webrtc.getDefaultIceServers()

// Subscribe to inbound signaling
agent.events.on('WebRTCEvents.IncomingPropose', async ({ payload }) => {
  const { thid, iceServers, policy, media } = payload
  // Caller is proposing a call - you can accept by sending an offer
  // The iceServers from propose can be used to configure RTCPeerConnection
  console.log('Incoming call proposal:', { media, iceServers })
})

agent.events.on('WebRTCEvents.IncomingOffer', async ({ payload }) => {
  const { thid, sdp, iceServers, policy, context } = payload

  // Use ICE servers from offer (or fall back to module defaults)
  const pc = new RTCPeerConnection({
    iceServers: iceServers ?? agent.modules.webrtc.getDefaultIceServers(),
    iceTransportPolicy: policy === 'relay-only' ? 'relay' : 'all',
  })
  peers.set(thid, pc)

  await pc.setRemoteDescription({ type: 'offer', sdp })
  const answer = await pc.createAnswer()
  await pc.setLocalDescription(answer)

  await agent.modules.webrtc.acceptCall({
    connectionId: context.connection!.id,
    threadId: thid,
    sdp: answer.sdp!,
  })
})

agent.events.on('WebRTCEvents.IncomingAnswer', async ({ payload }) => {
  const { thid, sdp, iceServers } = payload
  const pc = peers.get(thid)
  if (!pc) return

  // If callee provided additional ICE servers, you might want to restart ICE
  await pc.setRemoteDescription({ type: 'answer', sdp })
})

agent.events.on('WebRTCEvents.IncomingIce', async ({ payload }) => {
  const { thid, candidate, endOfCandidates } = payload
  const pc = peers.get(thid)
  if (!pc) return
  if (endOfCandidates) await pc.addIceCandidate(null)
  else await pc.addIceCandidate(candidate as RTCIceCandidateInit)
})

// Handle renegotiation requests (e.g., ICE restart)
agent.events.on('WebRTCEvents.RenegotiateRequested', async ({ payload }) => {
  const { thid, iceRestart, iceServers, reason } = payload
  const pc = peers.get(thid)
  if (!pc) return

  if (iceRestart) {
    // Perform ICE restart
    const offer = await pc.createOffer({ iceRestart: true })
    await pc.setLocalDescription(offer)
    // Send new offer...
  }
})

// Start call (caller)
async function startCall(connectionId: string, localStream: MediaStream) {
  const thid = crypto.randomUUID()

  // Use module's default ICE servers
  const iceServers = agent.modules.webrtc.getDefaultIceServers()

  const pc = new RTCPeerConnection({ iceServers })
  peers.set(thid, pc)

  // Handle ICE connection failures
  pc.oniceconnectionstatechange = () => {
    if (pc.iceConnectionState === 'failed') {
      // Request ICE restart
      agent.modules.webrtc.restartIce({ connectionId, threadId: thid })
    }
  }

  // Render remote stream
  pc.ontrack = e => (remoteVideo.srcObject = e.streams[0])

  // Send ICE via DIDComm
  pc.onicecandidate = e => agent.modules.webrtc.sendIce({
    connectionId,
    threadId: thid,
    candidate: e.candidate ?? undefined,
    endOfCandidates: e.candidate == null,
  })

  // Add local tracks
  for (const track of localStream.getTracks()) pc.addTrack(track, localStream)

  const offer = await pc.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true })
  await pc.setLocalDescription(offer)

  // Start call - ICE servers are automatically included from module config
  await agent.modules.webrtc.startCall({
    connectionId,
    threadId: thid,
    sdp: offer.sdp!,
    // Optionally override policy for this specific call
    policy: 'relay-preferred',
  })
}

// Request ICE restart when connection fails
async function handleConnectionFailure(connectionId: string, thid: string) {
  await agent.modules.webrtc.restartIce({
    connectionId,
    threadId: thid,
    // Optionally provide new ICE servers
    iceServers: [{ urls: 'turn:backup-turn.example.org:3478', username: 'u', credential: 'p' }],
  })
}

API Reference

WebRTCApi Methods

| Method | Description | |--------|-------------| | proposeCall() | Send a call proposal with ICE servers before offer | | startCall() | Send SDP offer with ICE servers | | acceptCall() | Send SDP answer | | sendIce() | Send ICE candidate | | renegotiate() | Request renegotiation (track changes, codec changes) | | restartIce() | Request ICE restart (connection recovery) | | endCall() | End the call | | getDefaultIceServers() | Get configured default ICE servers |

Events

| Event | Description | |-------|-------------| | IncomingPropose | Call proposal received (includes ICE servers) | | IncomingOffer | SDP offer received | | IncomingAnswer | SDP answer received | | IncomingIce | ICE candidate received | | RenegotiateRequested | Renegotiation requested (e.g., ICE restart) | | CallEnded | Call ended |

Notes

  • This module moves only signaling over DIDComm; media flows via WebRTC/DTLS-SRTP.
  • For NAT traversal, configure TURN servers in the module config or per-call.
  • Use relay-only policy for maximum firewall compatibility (also hides IP addresses).
  • Works with mediators and message pickup (offline delivery), as with any DIDComm message.