agi-server
v1.0.3
Published
A modern, FastAGI server library for Node.js with full TypeScript support.
Maintainers
Readme
AGI Server
A modern, zero‑dependency FastAGI server and client library for Asterisk, written in TypeScript. It provides a clean event‑based API, full AGI command coverage, per‑route authentication, and strong type safety.
Table of Contents
- Features
- Installation
- Quick Start
- Using with NestJS
- API Reference
- Usage Examples
- Error Handling
- Development
- Compatibility
- License
Features
- 🚀 FastAGI server – Listen for AGI connections from Asterisk
- 📡 Full AGI command set – All standard AGI commands as async methods
- 🔐 Per‑route & global authentication – Flexible auth functions
- 📦 Zero runtime dependencies – Uses only Node.js core modules
- 🧩 TypeScript first – Complete type definitions for events and commands
- 🧵 Command queueing – Serializes AGI commands automatically
- ⏱️ Command timeouts – Prevents hanging requests (30s default)
- 🧹 Graceful shutdown – Proper cleanup of channels and server
Installation
npm install agi-serverQuick Start
import { AGIServer } from 'agi-server';
const server = new AGIServer();
// Optional global authentication
server.auth(async (channel, path, params, headers) => {
const token = headers['x-api-token'];
return token === 'my-secret';
});
// Register a route
server.agi('/welcome', async (channel, params) => {
await channel.answer();
await channel.streamFile('welcome');
await channel.hangup();
});
// Start listening
await server.listen(4573, '0.0.0.0');
console.log('AGI server running on port 4573');Using with NestJS
import { Injectable, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
import { AGIServer } from 'agi-server';
@Injectable()
export class AgiService implements OnApplicationBootstrap, OnApplicationShutdown {
private server: AGIServer;
async onApplicationBootstrap() {
this.server = new AGIServer();
this.server.auth(this.authenticate.bind(this));
this.server.agi('/menu', this.handleMenu.bind(this));
await this.server.listen(4573);
console.log('AGI server started');
}
async onApplicationShutdown() {
await this.server.close();
}
private authenticate(channel, path, params, headers) {
return headers['x-token'] === process.env.AGI_TOKEN;
}
private async handleMenu(channel, params) {
await channel.answer();
const result = await channel.getData('menu-prompt', 10000, 1);
if (result.data === '1') {
await channel.streamFile('option1');
}
await channel.hangup();
}
}API Reference
AGIServer
constructor() Creates a new AGI server instance. Does not start listening until listen() is called.
listen(host?: string, port?: number): Promise<{ host: string; port: number }> Starts the server. Default host '0.0.0.0', default port 4573. Throws if already listening.
close(): Promise
Closes the server and all active channels. Emits 'stopped' when done.
auth(authFn: AuthFunction): void
Sets a global authentication function called before every route.
type AuthFunction = (
channel: AGIChannel,
path: string,
params: Record<string, string | string[]>,
headers: Record<string, string>
) => Promise<boolean> | boolean;agi(path: string, handler: AGIHandler): void
agi(path: string, config: AGIRouteConfig): void
Registers a route. config can include a custom auth function that overrides the global one.
type AGIHandler = (channel: AGIChannel, params: Record<string, string | string[]>) => Promise<void>;
interface AGIRouteConfig {
handler: AGIHandler;
auth?: AuthFunction;
}AGIChannel
All methods return Promise<AGIResponse> where:
interface AGIResponse {
code: number; // 200, 510, etc.
result?: number | null;
data?: string | null;
error?: string;
}Basic commands
| Method | Description |
|--------|-------------|
| answer() | Answers the channel |
| hangup() | Hangs up and destroys the channel |
| verbose(message, level?) | Sends log to Asterisk console (level 1-4) |
| channelStatus(channel?) | Returns status 0-7 |
| noop(message?) | Does nothing (debug) |
Audio playback & recording
| Method | Description |
|--------|-------------|
| streamFile(filename, escapeDigits?, offset?) | Plays a file, listens for DTMF |
| controlStreamFile(filename, escapeDigits, skipms?, ffchar?, rewchar?, pausechar?) | Play with controls |
| recordFile(filename, format, escapeDigits, timeout, offset?, beep?, silence?) | Records audio to file |
DTMF & input
| Method | Description |
|--------|-------------|
| waitForDigit(timeout?) | Waits for a DTMF digit |
| getData(filename, timeout?, maxDigits?) | Plays file and collects digits |
| getOption(filename, escapeDigits, timeout?) | Plays file with timeout |
| sendDTMF(digits) | Sends DTMF digits |
Say commands (text-to-speech)
| Method | Description |
|--------|-------------|
| sayAlpha(text, escapeDigits?) | Says alphabetic string |
| sayDigits(digits, escapeDigits?) | Says digits |
| sayNumber(number, escapeDigits?, gender?) | Says number |
| sayPhonetic(text, escapeDigits?) | Says phonetically |
| sayDate(timestamp, escapeDigits?) | Says date |
| sayTime(timestamp, escapeDigits?) | Says time |
| sayDatetime(timestamp, escapeDigits?, format?, timezone?) | Says date and time |
Variables
| Method | Description |
|--------|-------------|
| getVariable(name) | Gets a channel variable |
| getFullVariable(name, channel?) | Gets variable with channel spec |
| setVariable(name, value) | Sets a channel variable |
Call flow control
| Method | Description |
|--------|-------------|
| setContext(context) | Sets continuation context |
| setExtension(extension) | Sets continuation extension |
| setPriority(priority) | Sets continuation priority |
| gosub(context, extension, priority) | Executes a subroutine |
Other commands
| Method | Description |
|--------|-------------|
| setCallerId(number) | Changes caller ID |
| setAutoHangup(seconds) | Auto‑hangup after seconds |
| setMusic(enabled, musicClass?) | Music on hold |
| sendText(text) | Sends text message |
| sendImage(image) | Sends image |
| databaseGet(family, key) | Gets from Asterisk DB |
| databasePut(family, key, value) | Puts into Asterisk DB |
| databaseDel(family, key) | Deletes from DB |
| databaseDelTree(family, key?) | Deletes DB tree |
| exec(application, options?) | Executes any dialplan application |
| asyncagiBreak() | Interrupts Async AGI (Asterisk 1.8+) |
Events
AGIServer emits these typed events:
| Event | Payload |
|-------|---------|
| listening | (host: string, port: number) => void |
| close | () => void |
| stopped | () => void |
| error | (error: Error) => void |
| connection | (channel: AGIChannel) => void |
| channel_ready | (data: { channel, path, params, headers }) => void |
| channel_end | (channel: AGIChannel) => void |
| channel_error | (data: { channel, error }) => void |
| no_handler | (data: { path, channel }) => void |
| handler_error | (data: { error, path, channel }) => void |
| execute_error | (data: { error, channel }) => void |
| auth_failed | (data: { channel, path, reason }) => void |
| auth_error | (data: { channel, path, error }) => void |
Usage Examples
Authentication (global & per‑route)
// Global auth (all routes)
server.auth(async (channel, path, params, headers) => {
return headers['x-token'] === process.env.GLOBAL_TOKEN;
});
// Route without auth (public)
server.agi('/public', async (channel) => {
await channel.streamFile('public');
});
// Route with custom auth (overrides global)
server.agi('/admin', {
handler: async (channel) => {
await channel.streamFile('admin');
},
auth: async (channel, path, params, headers) => {
return headers['x-admin-token'] === 'super-secret';
}
});Handling DTMF Input
server.agi('/survey', async (channel) => {
await channel.answer();
const result = await channel.getData('survey-prompt', 30000, 1);
if (result.data === '1') {
await channel.streamFile('thanks');
} else {
await channel.streamFile('goodbye');
}
await channel.hangup();
});Audio Playback & Recording
// Play a file
await channel.streamFile('welcome', '#', 0);
// Record a message
await channel.recordFile('recording', 'wav', '#', 60000, 0, true, 2);Using exec() for custom dialplan apps
// Run any Asterisk dialplan application
await channel.exec('MixMonitor', 'recording.wav');
await channel.exec('Dial', 'PJSIP/100,30,Tt');Graceful Shutdown
// In plain Node.js
process.on('SIGTERM', async () => {
console.log('Closing AGI server...');
await server.close();
process.exit(0);
});
// In NestJS (see NestJS example above)Error Handling
The library uses a custom AGIError class with error codes:
| Code | Description |
|------|-------------|
| E_AGI_ALREADY_LISTENING | listen() called twice |
| E_AGI_ROUTE_EXISTS | Duplicate route path |
| E_AGI_AUTH_EXISTS | Global auth already set |
| E_AGI_INVALID_PATH | Invalid route path |
| E_AGI_COMMAND_TIMEOUT | No response from Asterisk in 30 seconds |
| E_AGI_CHANNEL_DESTROYED | Channel already closed |
| E_AGI_SOCKET_END | Socket ended unexpectedly |
| E_AGI_SOCKET_ERROR | Socket error |
try {
await channel.getData('prompt');
} catch (error) {
if (error.code === 'E_AGI_COMMAND_TIMEOUT') {
console.log('Asterisk did not respond');
}
}Development
Build from source
git clone https://github.com/your-username/agi-server.git
cd agi-server
npm install
npm run buildProject structure
src/
├── agi.channel.ts # AGIChannel class (all commands)
├── agi.server.ts # AGIServer class
├── agi.error.ts # Custom error class
├── types.ts # Shared types and interfaces
└── index.ts # Public exportsRunning tests
npm testCompatibility
Node.js: 18.x or higher (uses ES2022 features)
Asterisk: 1.8 and above (all modern versions)
TypeScript: 4.9 or higher
License
MIT
Contributing
Issues and pull requests are welcome. Please ensure your code passes the existing tests and follows the TypeScript style.
Acknowledgements
Asterisk AGI documentation – docs.asterisk.org
Built with Node.js net and events modules.
