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 🙏

© 2026 – Pkg Stats / Ryan Hefner

freeswitch-esl-node

v1.0.0

Published

Dependency-free FreeSWITCH Event Socket (ESL) client for Node.js. TypeScript types, correct Content-Length framing, bgapi, events, auto-reconnect, ESM and CommonJS.

Readme

freeswitch-esl-node

A small, dependency-free FreeSWITCH Event Socket client for Node.js, inbound mode.

ESL is a header block plus an optional body over TCP, so this package hand-rolls it against node:net. Nothing else is pulled into your tree.

npm install freeswitch-esl-node
  • Zero runtime dependencies
  • TypeScript types included, written in TypeScript
  • Ships both ESM and CommonJS
  • Waits for the full declared Content-Length before handing you a body
  • bgapi resolves with the job result, correlated by Job-UUID
  • Events kept out of command replies, and command replies kept out of events
  • Optional auto-reconnect that re-authenticates and re-subscribes
  • Node 18 or newer

Quick start

import { EslClient } from "freeswitch-esl-node";

const esl = new EslClient({
  host: "127.0.0.1",
  port: 8021,
  password: "ClueCon",
});

await esl.connect();

console.log(await esl.api("status"));
console.log(await esl.api("sofia status profile internal"));

esl.close();

CommonJS works the same way:

const { EslClient } = require("freeswitch-esl-node");

FreeSWITCH side setup

The event socket is on by default, listening on 127.0.0.1:8021 with the password ClueCon. Change it in autoload_configs/event_socket.conf.xml:

<configuration name="event_socket.conf" description="Socket Client">
  <settings>
    <param name="listen-ip" value="127.0.0.1"/>
    <param name="listen-port" value="8021"/>
    <param name="password" value="something-that-is-not-ClueCon"/>
  </settings>
</configuration>

Then fs_cli -x "reload mod_event_socket". Keep listen-ip on localhost and reach it over a tunnel or a private network. Anyone who can open that port owns the switch.

Why the framing matters

The obvious way to write an ESL client is to emit a frame as soon as you see the blank line ending the headers. That works right up until you run show channels on a busy switch, the body spans several TCP segments, and you get a confidently truncated answer with no error at all.

Twelve channels looks exactly like twelve channels whether or not there were really ninety.

This client reads Content-Length and waits for the whole declared body, counted in bytes rather than characters so multibyte caller IDs do not shift the boundary. There is a test that feeds a 400-row response through in 500-byte chunks.

Events

const esl = new EslClient({
  host,
  password,
  subscribe: ["CHANNEL_CREATE", "CHANNEL_ANSWER", "CHANNEL_HANGUP_COMPLETE"],
});

esl.on("CHANNEL_ANSWER", (e) => console.log("answered", e["Unique-ID"], e["Caller-Destination-Number"]));
esl.on("event", (e) => metrics.count(e["Event-Name"]));

await esl.connect();

Use subscribe: "ALL" for the firehose, or call esl.subscribe([...]) later. Calling it again adds to the subscription rather than replacing it, matching FreeSWITCH's own behaviour.

Header values are percent-decoded for you. A malformed sequence is left as-is instead of throwing, because a literal % in a caller name should not take down your parser.

filter() maps to the ESL filter command if you want the switch to do the narrowing:

await esl.filter("Event-Name", "CHANNEL_ANSWER");

Background jobs

const uuid = await esl.bgapi("originate user/1001 &echo");

bgapi returns a promise that settles when the matching BACKGROUND_JOB event arrives, correlated by a Job-UUID this client generates. It subscribes to BACKGROUND_JOB the first time you call it. That event is consumed rather than passed on, so it will not show up in your own event handlers.

Use it for anything slow. api() blocks the socket until the switch answers, which on a long originate means nothing else gets through.

Staying connected

const esl = new EslClient({
  host,
  password,
  subscribe: ["CHANNEL_HANGUP_COMPLETE"],
  reconnect: { delayMs: 1000, maxDelayMs: 30_000, retries: Infinity },
});

esl.on("reconnecting", (attempt, delay) => console.warn(`retry ${attempt} in ${delay}ms`));
esl.on("reconnected", () => console.log("back, and re-subscribed"));
esl.on("error", (err) => console.error(err.message));

Reconnect is off unless you ask for it. On success the client re-authenticates and replays your subscribe option, so you do not silently end up connected but deaf.

Any command still in flight when the connection drops rejects immediately rather than hanging until its timeout.

API

new EslClient(options)

| Option | Default | Meaning | | --- | --- | --- | | host | required | FreeSWITCH host | | port | 8021 | Event socket port | | password | required | Event socket password | | timeoutMs | 10000 | Connect timeout, and default command timeout | | subscribe | none | Event names to subscribe to on connect, or "ALL" | | reconnect | off | true, or { retries, delayMs, maxDelayMs } |

Methods

  • connect() opens the socket and authenticates.
  • api(command, timeoutMs?) runs an API command and resolves with its output. A -ERR reply rejects.
  • bgapi(command, timeoutMs?) runs it in the background and resolves with the job result.
  • subscribe(events) subscribes to more events.
  • filter(header, value) adds a server-side event filter.
  • sendRecv(command, headers?, timeoutMs?) sends a raw ESL command and resolves with the reply frame.
  • close() disconnects and cancels any reconnect.

Events

connect, close, error, reconnecting, reconnected, disconnectNotice, frame (every decoded frame), event (FreeSWITCH events only), plus one channel per event name.

Helpers

  • parseHeaders(raw) parses a header block, percent-decoding values.
  • parseEventPlain(body) parses an event, including its own nested body.
  • serializeCommand(command, headers?) builds a command, throwing on line breaks.
  • decodeValue(raw) percent-decodes one value, tolerating malformed input.
  • EslError the error class everything here throws.

Scope

Inbound mode only, which is what you want for monitoring and control. Outbound mode, where FreeSWITCH connects to you from the dialplan, is not implemented. Open an issue if you need it.

Commands are passed through as written. Anything reaching api() runs with full event socket privileges, so if you expose it to users, check the command against an allow list first.

Related

  • pbx-mcp, a Model Context Protocol server that gives AI assistants a read-only window into Asterisk and FreeSWITCH. This client came out of building it, and the truncation bug above is one we actually shipped and then fixed.
  • asterisk-ami-node, the same idea for the Asterisk Manager Interface.
  • ICTCore, the open source telephony framework behind our products.

About

Built and maintained by Tahir Almas at ICT Innovations, who have been shipping open source telephony since 2005.

Issues and pull requests are welcome. Bug reports with the actual error string are the most useful thing you can send.

License

MIT