@fonoster/amd 0.27.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,86 +1,19 @@
1
1
  amd
2
2
  =================
3
3
 
4
- Standalone Answering Machine Detection sidecar for Fonoster. Runs an ONNX
5
- whisper-tiny classifier over the leading audio of an outbound call and
6
- reports the verdict onto the same `AMDSTATUS`/`AMDCAUSE` Asterisk channel
7
- variables that Asterisk's own native `AMD()` application sets — so nothing
8
- downstream (`apiserver`'s `createCreateVoiceClient.ts` / `mapAsteriskAmd.ts`)
9
- needs to change to consume it.
4
+ Answering-machine detection for Fonoster. An ONNX whisper-tiny classifier reads the first seconds
5
+ of a call's audio and returns a status (HUMAN, MACHINE, VOICEMAIL, IVR or UNKNOWN), a confidence,
6
+ the detector name and the latency.
10
7
 
11
- It runs as its own process/container, decoupled from `apiserver` and
12
- `autopilot`, and can be built, tested, and smoke-tested against a bare
13
- Asterisk box with no other part of the Fonoster stack running.
8
+ AppBridge loads it for the `amd` listener: it attaches a listen-only participant to the room,
9
+ collects the caller's audio (16 kHz L16) and calls `classifyPcm`. The model ships with the package
10
+ in `amd-model/`.
14
11
 
15
- ## How it works
12
+ ```ts
13
+ import { classifyPcm } from "@fonoster/amd";
16
14
 
17
- 1. The dialplan (see `asterisk/config/extensions.conf`'s `AMD_ENGINE` gate)
18
- calls `AGI(agi://amd:4573,${AMD_MODE})`, which blocks the channel.
19
- The argument is the mode feature flag — see below.
20
- 2. Inside that AGI session, this service mints a UUID and runs
21
- `EXEC AudioSocket <uuid>,<host>:<port>`, which blocks until the AudioSocket
22
- side (a second listener in this same process) closes the connection.
23
- 3. The AudioSocket side buffers a few seconds of PCM and classifies it with
24
- the bundled ONNX model — always the classifier's own five-way status
25
- (HUMAN/MACHINE/VOICEMAIL/IVR/UNKNOWN) plus confidence, detector, and
26
- latency; nothing is thrown away yet — then hangs up, which unblocks the
27
- AGI session.
28
- 4. The AGI session maps that raw result to channel variables according to
29
- the requested mode (see below), issues one `SET VARIABLE` per variable
30
- over the same AGI connection, then closes the connection — which is what
31
- tells Asterisk's `AGI()` app to resume the dialplan into
32
- `Stasis(mediacontroller)`, exactly as it does today.
33
-
34
- Every step is fail-open: a missing/corrupt model, a hung inference, a
35
- timeout, or empty audio all resolve to an "unknown" result with a diagnostic
36
- cause (`ML-TIMEOUT`, `ML-ERROR`, `ML-NO-AUDIO`) rather than stalling or
37
- dropping the call.
38
-
39
- ## Modes: compact vs. full
40
-
41
- The mode is a feature flag passed as the AGI call's first dialplan argument
42
- (`agi_arg_1` on the wire) — not an env var, since it's a per-call decision
43
- made in the dialplan, mirroring how `AMD_ENGINE` is already threaded through
44
- as a channel variable.
45
-
46
- - **`compact`** (the default — anything other than the literal `full`,
47
- including no argument at all, falls back to this): collapses the
48
- classifier's five-way status onto the same four-value `AMDSTATUS`
49
- vocabulary Asterisk's native `AMD()` uses (`HUMAN`/`MACHINE`/`NOTSURE`),
50
- with `VOICEMAIL`/`IVR` folded into `MACHINE` and a descriptive `AMDCAUSE`
51
- (e.g. `MACHINE-VOICEMAIL`). Confidence below `AMD_MIN_CONFIDENCE`
52
- is downgraded to `NOTSURE`/`ML-LOW-CONFIDENCE`. Only ever sets
53
- `AMDSTATUS`/`AMDCAUSE` — a consumer built against native AMD() sees no
54
- difference switching engines.
55
- - **`full`**: sets `AMDSTATUS` to the classifier's own status
56
- (`HUMAN`/`MACHINE`/`VOICEMAIL`/`IVR`/`UNKNOWN`, untouched by any
57
- confidence threshold — the consumer applies its own policy), plus
58
- `AMDCONFIDENCE` (0–1), `AMDDETECTOR` (model name), and `AMDLATENCYMS`.
59
-
60
- Set it from the dialplan by giving `AMD_MODE` a value before the `AGI()`
61
- call — apiserver already does this for API-originated calls via
62
- `APISERVER_AMD_MODE` (see below).
63
-
64
- ## Configuration
65
-
66
- | env | default | meaning |
67
- |---|---|---|
68
- | `AMD_AGI_PORT` | `4573` | FastAGI listener port |
69
- | `AMD_AUDIOSOCKET_PORT` | `9092` | AudioSocket listener port |
70
- | `AMD_AUDIOSOCKET_BIND_ADDR` | `0.0.0.0` | AudioSocket listener bind address |
71
- | `AMD_AUDIOSOCKET_ADVERTISE_HOST` | `amd` | host Asterisk is told to connect to for the AudioSocket leg — must be reachable *from* Asterisk |
72
- | `AMD_PROBE_MS` | `3000` | leading audio to gather before classifying |
73
- | `AMD_TIMEOUT_MS` | `4000` | hard deadline for the whole AGI session |
74
- | `AMD_MIN_CONFIDENCE` | `0.8` | compact mode only: verdicts below this are reported as `NOTSURE` |
75
- | `AMD_MODEL_PATH` | *(bundled)* | override the model directory |
76
-
77
- ## Enabling it end-to-end
78
-
79
- Set `APISERVER_AMD_ENABLED=true` and `APISERVER_AMD_ENGINE=ml` on `apiserver`
80
- (next to the existing native-AMD env vars). With `AMD_ENGINE` unset or set to
81
- anything but `ml`, behavior is unchanged from Asterisk's native `AMD()`.
82
- Additionally set `APISERVER_AMD_MODE=full` to opt into the richer variable
83
- set — omit it (or leave it `compact`) to stay native-AMD-compatible.
15
+ const verdict = await classifyPcm(pcm);
16
+ ```
84
17
 
85
18
  ## Regression corpus
86
19
 
@@ -101,58 +34,3 @@ few exist on purpose, to track known confusions (e.g. formal/synthesized IVR
101
34
  scripts reading as MACHINE) rather than hide them.
102
35
  Licensing and attribution for every third-party clip is in
103
36
  `test/fixtures/regression/ATTRIBUTION.md`.
104
-
105
- ## Automated verification
106
-
107
- `test/agiAudiosocket.integration.test.ts` starts the real AGI and AudioSocket
108
- listeners in-process, plays both sides of Asterisk's role over raw TCP (the
109
- FastAGI header handshake with `agi_arg_1` set, `EXEC AudioSocket`, streaming
110
- real fixture PCM frame-by-frame, then reading back whatever `SET VARIABLE`
111
- commands the server issues), and asserts the values that arrive over that
112
- wire match calling the classifier directly on the same audio — for both
113
- compact (default) and full mode, plus a case confirming an unrecognized mode
114
- argument falls back to compact. This is the primary proof that the
115
- AGI+AudioSocket mechanism and the mode flag work end-to-end; run it with:
116
-
117
- ```sh
118
- npm run integration
119
- ```
120
-
121
- ## Isolated smoke test (manual, against real Asterisk)
122
-
123
- This service can also be exercised without `apiserver`, `autopilot`, NATS, or
124
- Postgres — just this container and a bare Asterisk instance. Unlike the
125
- integration test above, this path has not been run against a real Asterisk
126
- box in this repo's CI; treat it as a documented starting point to validate by
127
- hand:
128
-
129
- ```sh
130
- docker compose -f mods/amd/docker-compose.smoke.yaml up --build
131
- ```
132
-
133
- That starts `amd` alongside a `fonoster/asterisk` container using
134
- `mods/amd/smoke/extensions.conf`, a minimal dialplan with one test
135
- extension:
136
-
137
- ```
138
- exten => 7000,1,Answer()
139
- same => n,Wait(1)
140
- same => n,AGI(agi://amd:4573)
141
- same => n,Verbose(1,AMDSTATUS=${AMDSTATUS} AMDCAUSE=${AMDCAUSE})
142
- same => n,Hangup()
143
- ```
144
-
145
- Originate a call into extension `7000` playing a known recorded sample (a
146
- human greeting vs. a voicemail greeting) and check the Asterisk console log
147
- for the `AMDSTATUS`/`AMDCAUSE` line the `Verbose()` step prints, e.g.:
148
-
149
- ```sh
150
- docker compose -f mods/amd/docker-compose.smoke.yaml exec asterisk \
151
- asterisk -rx "channel originate Local/7000@smoke-test application Playback /path/to/sample"
152
- ```
153
-
154
- ## Development
155
-
156
- ```sh
157
- npm run start:dev -w @fonoster/amd
158
- ```
@@ -10,6 +10,11 @@ declare function softmaxTop(logits: ArrayLike<number>): {
10
10
  index: number;
11
11
  confidence: number;
12
12
  };
13
+ /**
14
+ * Releases the loaded ONNX session. Call it before the process exits on purpose (tests): ONNX
15
+ * Runtime's threads abort the process ("mutex lock failed") if they are still alive at exit.
16
+ */
17
+ export declare function releaseAmdModel(): Promise<void>;
13
18
  /**
14
19
  * Classify a buffer of raw slin16 PCM (16 kHz mono) as human / voicemail / IVR /
15
20
  * machine. Loads and caches the ONNX session on first call. Throws on model or
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.DEFAULT_MODEL_DIR = void 0;
46
+ exports.releaseAmdModel = releaseAmdModel;
46
47
  exports.classifyPcm = classifyPcm;
47
48
  exports.mapLabelToStatus = mapLabelToStatus;
48
49
  exports.softmaxTop = softmaxTop;
@@ -114,6 +115,24 @@ function softmaxTop(logits) {
114
115
  return { index, confidence: sum > 0 ? exps[index] / sum : 0 };
115
116
  }
116
117
  let loaded = null;
118
+ /**
119
+ * Releases the loaded ONNX session. Call it before the process exits on purpose (tests): ONNX
120
+ * Runtime's threads abort the process ("mutex lock failed") if they are still alive at exit.
121
+ */
122
+ function releaseAmdModel() {
123
+ return __awaiter(this, void 0, void 0, function* () {
124
+ const entry = loaded;
125
+ loaded = null;
126
+ if (!entry)
127
+ return;
128
+ try {
129
+ yield (yield entry.session).release();
130
+ }
131
+ catch (_a) {
132
+ // never loaded, nothing to release
133
+ }
134
+ });
135
+ }
117
136
  function load(modelDir) {
118
137
  var _a;
119
138
  if (loaded && loaded.dir === modelDir)
package/dist/index.d.ts CHANGED
@@ -16,8 +16,4 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
- export * from "./agi/server";
20
- export * from "./audiosocket/server";
21
19
  export * from "./amd/AmdModel";
22
- export * from "./amd/buildAmdVariables";
23
- export * from "./probe/runProbe";
package/dist/index.js CHANGED
@@ -32,8 +32,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
32
32
  * See the License for the specific language governing permissions and
33
33
  * limitations under the License.
34
34
  */
35
- __exportStar(require("./agi/server"), exports);
36
- __exportStar(require("./audiosocket/server"), exports);
37
35
  __exportStar(require("./amd/AmdModel"), exports);
38
- __exportStar(require("./amd/buildAmdVariables"), exports);
39
- __exportStar(require("./probe/runProbe"), exports);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fonoster/amd",
3
- "version": "0.27.0",
4
- "description": "Standalone answering-machine-detection sidecar for the Fonoster platform",
3
+ "version": "0.31.0",
4
+ "description": "Answering-machine-detection model (ONNX) used by AppBridge",
5
5
  "author": "Pedro Sanders <psanders@fonoster.com>",
6
6
  "homepage": "https://github.com/fonoster/fonoster#readme",
7
7
  "license": "MIT",
@@ -14,20 +14,12 @@
14
14
  "scripts": {
15
15
  "prebuild": "rimraf ./dist tsconfig.tsbuildinfo",
16
16
  "build": "tsc -b tsconfig.json",
17
- "clean": "rimraf ./dist node_modules tsconfig.tsbuildinfo",
18
- "generate:readme": "node ../../.scripts/gen-readme.cjs",
19
- "start:dev": "nodemon --exec tsx --watch src src/server.ts"
17
+ "generate:readme": "fonoster-readme"
20
18
  },
21
19
  "dependencies": {
22
- "@fonoster/common": "^0.27.0",
23
- "@fonoster/logger": "^0.27.0",
24
- "@fonoster/streams": "^0.27.0",
25
- "@fonoster/types": "^0.27.0",
26
- "onnxruntime-node": "1.21.1",
27
- "uuid": "^11.0.3"
28
- },
29
- "devDependencies": {
30
- "@types/uuid": "^10.0.0"
20
+ "@fonoster/common": "^0.31.0",
21
+ "@fonoster/types": "^0.31.0",
22
+ "onnxruntime-node": "1.30.0"
31
23
  },
32
24
  "files": [
33
25
  "dist",
@@ -43,5 +35,5 @@
43
35
  "bugs": {
44
36
  "url": "https://github.com/fonoster/fonoster/issues"
45
37
  },
46
- "gitHead": "1191b34677b51a36000177887ff82303ef2693e1"
38
+ "gitHead": "f0a5102dddeea0f70f659234108805bc10690f2e"
47
39
  }
@@ -1,77 +0,0 @@
1
- /**
2
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
- * http://github.com/fonoster/fonoster
4
- *
5
- * This file is part of Fonoster
6
- *
7
- * Licensed under the MIT License (the "License");
8
- * you may not use this file except in compliance with
9
- * the License. You may obtain a copy of the License at
10
- *
11
- * https://opensource.org/licenses/MIT
12
- *
13
- * Unless required by applicable law or agreed to in writing, software
14
- * distributed under the License is distributed on an "AS IS" BASIS,
15
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- * See the License for the specific language governing permissions and
17
- * limitations under the License.
18
- */
19
- import { EventEmitter } from "events";
20
- import * as net from "net";
21
- /**
22
- * Thrown when the channel hangs up (or the socket closes) while a command is
23
- * in flight, or when a command is attempted on an already-hung-up channel.
24
- */
25
- declare class AgiHangupError extends Error {
26
- constructor(message?: string);
27
- }
28
- /**
29
- * One FastAGI session. Minimal, hand-rolled client for the handful of
30
- * commands this module needs (EXEC, SET VARIABLE) — see AgiServer.ts for why
31
- * this exists instead of a dependency: the only maintained npm package for
32
- * this protocol (`asteriskagi`) shipped a broken `require()` in its latest
33
- * release at the time this was written.
34
- *
35
- * The AGI protocol is a simple line-based request/response: write one
36
- * command line, read back one `200 result=...` line. Asterisk may also send
37
- * an unsolicited `HANGUP` line at any time if the channel hangs up mid-command.
38
- */
39
- declare class AgiChannel extends EventEmitter {
40
- readonly channel: string;
41
- readonly uniqueid: string;
42
- readonly variables: Record<string, string>;
43
- private socket;
44
- private buffer;
45
- private ended;
46
- private pending?;
47
- constructor(props: {
48
- socket: net.Socket;
49
- variables: Record<string, string>;
50
- });
51
- get hungup(): boolean;
52
- /**
53
- * Dialplan arguments passed after the AGI URI, e.g.
54
- * `AGI(agi://amd:4573,${AMD_MODE})` arrives as `agi_arg_1`, read
55
- * here as `args[0]`. Standard AGI behavior, same for FastAGI as for a
56
- * local script.
57
- */
58
- get args(): string[];
59
- private onData;
60
- private resolvePending;
61
- private end;
62
- /** Sends one raw FastAGI command line and waits for its response. */
63
- send(command: string): Promise<void>;
64
- /** Runs a dialplan application via `EXEC`, e.g. `exec("AudioSocket", args)`. */
65
- exec(application: string, args?: string): Promise<void>;
66
- setVariable(name: string, value: string): Promise<void>;
67
- /**
68
- * Ends the AGI session cleanly once this side is done issuing commands.
69
- * This — not a special "done" command — is how Asterisk's AGI() app knows
70
- * to return control to the dialplan: it detects the AGI script closing the
71
- * connection. Without this, a real channel would sit in AGI() forever.
72
- * Distinct from `end()`'s hangup semantics: this is *us* finishing, not
73
- * the far end going away, so it doesn't emit "hangup".
74
- */
75
- close(): void;
76
- }
77
- export { AgiChannel, AgiHangupError };
@@ -1,171 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AgiHangupError = exports.AgiChannel = void 0;
4
- /**
5
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
- * http://github.com/fonoster/fonoster
7
- *
8
- * This file is part of Fonoster
9
- *
10
- * Licensed under the MIT License (the "License");
11
- * you may not use this file except in compliance with
12
- * the License. You may obtain a copy of the License at
13
- *
14
- * https://opensource.org/licenses/MIT
15
- *
16
- * Unless required by applicable law or agreed to in writing, software
17
- * distributed under the License is distributed on an "AS IS" BASIS,
18
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
- * See the License for the specific language governing permissions and
20
- * limitations under the License.
21
- */
22
- const events_1 = require("events");
23
- /**
24
- * Thrown when the channel hangs up (or the socket closes) while a command is
25
- * in flight, or when a command is attempted on an already-hung-up channel.
26
- */
27
- class AgiHangupError extends Error {
28
- constructor(message = "AGI channel hung up") {
29
- super(message);
30
- this.name = "AgiHangupError";
31
- }
32
- }
33
- exports.AgiHangupError = AgiHangupError;
34
- /**
35
- * One FastAGI session. Minimal, hand-rolled client for the handful of
36
- * commands this module needs (EXEC, SET VARIABLE) — see AgiServer.ts for why
37
- * this exists instead of a dependency: the only maintained npm package for
38
- * this protocol (`asteriskagi`) shipped a broken `require()` in its latest
39
- * release at the time this was written.
40
- *
41
- * The AGI protocol is a simple line-based request/response: write one
42
- * command line, read back one `200 result=...` line. Asterisk may also send
43
- * an unsolicited `HANGUP` line at any time if the channel hangs up mid-command.
44
- */
45
- class AgiChannel extends events_1.EventEmitter {
46
- constructor(props) {
47
- var _a, _b;
48
- super();
49
- this.buffer = "";
50
- this.ended = false;
51
- this.onData = (data) => {
52
- if (this.ended)
53
- return;
54
- this.buffer += data.toString();
55
- let newline = this.buffer.indexOf("\n");
56
- while (newline >= 0) {
57
- const line = this.buffer.slice(0, newline).replace(/\r$/, "").trim();
58
- this.buffer = this.buffer.slice(newline + 1);
59
- if (line === "HANGUP") {
60
- this.end();
61
- return;
62
- }
63
- if (line)
64
- this.resolvePending(line);
65
- newline = this.buffer.indexOf("\n");
66
- }
67
- };
68
- this.socket = props.socket;
69
- this.variables = props.variables;
70
- this.channel = (_a = props.variables.channel) !== null && _a !== void 0 ? _a : "";
71
- this.uniqueid = (_b = props.variables.uniqueid) !== null && _b !== void 0 ? _b : "";
72
- this.socket.on("data", this.onData);
73
- this.socket.once("end", () => this.end());
74
- this.socket.once("close", () => this.end());
75
- this.socket.once("error", (err) => this.end(err));
76
- }
77
- get hungup() {
78
- return this.ended;
79
- }
80
- /**
81
- * Dialplan arguments passed after the AGI URI, e.g.
82
- * `AGI(agi://amd:4573,${AMD_MODE})` arrives as `agi_arg_1`, read
83
- * here as `args[0]`. Standard AGI behavior, same for FastAGI as for a
84
- * local script.
85
- */
86
- get args() {
87
- const result = [];
88
- for (let i = 1; this.variables[`arg_${i}`] !== undefined; i++) {
89
- result.push(this.variables[`arg_${i}`]);
90
- }
91
- return result;
92
- }
93
- resolvePending(line) {
94
- const pending = this.pending;
95
- if (!pending)
96
- return;
97
- this.pending = undefined;
98
- // "200 result=1 ..." on success; "5xx ..." or a negative result on
99
- // failure — either way this only needs to settle the promise, not parse
100
- // the (unused) payload.
101
- const match = line.match(/^\d+\s+result=(-?\d+)/);
102
- const result = match ? Number(match[1]) : NaN;
103
- if (line.startsWith("2") && !(Number.isFinite(result) && result < 0)) {
104
- pending.resolve();
105
- }
106
- else {
107
- pending.reject(new Error(`AGI command failed: ${line}`));
108
- }
109
- }
110
- end(cause) {
111
- if (this.ended)
112
- return;
113
- this.ended = true;
114
- this.socket.off("data", this.onData);
115
- const pending = this.pending;
116
- this.pending = undefined;
117
- pending === null || pending === void 0 ? void 0 : pending.reject(new AgiHangupError());
118
- if (!this.socket.destroyed)
119
- this.socket.end();
120
- this.emit("hangup", cause);
121
- }
122
- /** Sends one raw FastAGI command line and waits for its response. */
123
- send(command) {
124
- if (this.ended)
125
- return Promise.reject(new AgiHangupError());
126
- if (this.pending) {
127
- return Promise.reject(new Error("An AGI command is already in progress"));
128
- }
129
- return new Promise((resolve, reject) => {
130
- this.pending = { resolve, reject };
131
- if (!this.socket.writable || this.socket.destroyed) {
132
- this.end();
133
- return;
134
- }
135
- this.socket.write(command + "\n", "utf8", (err) => {
136
- if (err)
137
- this.end(err);
138
- });
139
- });
140
- }
141
- /** Runs a dialplan application via `EXEC`, e.g. `exec("AudioSocket", args)`. */
142
- exec(application, args) {
143
- return this.send(`EXEC ${application} ${args !== null && args !== void 0 ? args : ""}`);
144
- }
145
- setVariable(name, value) {
146
- return this.send(`SET VARIABLE ${name} "${value}"`);
147
- }
148
- /**
149
- * Ends the AGI session cleanly once this side is done issuing commands.
150
- * This — not a special "done" command — is how Asterisk's AGI() app knows
151
- * to return control to the dialplan: it detects the AGI script closing the
152
- * connection. Without this, a real channel would sit in AGI() forever.
153
- * Distinct from `end()`'s hangup semantics: this is *us* finishing, not
154
- * the far end going away, so it doesn't emit "hangup".
155
- */
156
- close() {
157
- if (this.ended)
158
- return;
159
- this.ended = true;
160
- this.socket.off("data", this.onData);
161
- // A command can still be in flight (e.g. the caller gave up on it via an
162
- // external race/timeout rather than waiting for a response) — settle it
163
- // so that awaiter doesn't hang forever, same as end()'s hangup path.
164
- const pending = this.pending;
165
- this.pending = undefined;
166
- pending === null || pending === void 0 ? void 0 : pending.reject(new AgiHangupError("AGI session closed locally"));
167
- if (!this.socket.destroyed)
168
- this.socket.end();
169
- }
170
- }
171
- exports.AgiChannel = AgiChannel;
@@ -1,34 +0,0 @@
1
- /**
2
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
- * http://github.com/fonoster/fonoster
4
- *
5
- * This file is part of Fonoster
6
- *
7
- * Licensed under the MIT License (the "License");
8
- * you may not use this file except in compliance with
9
- * the License. You may obtain a copy of the License at
10
- *
11
- * https://opensource.org/licenses/MIT
12
- *
13
- * Unless required by applicable law or agreed to in writing, software
14
- * distributed under the License is distributed on an "AS IS" BASIS,
15
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- * See the License for the specific language governing permissions and
17
- * limitations under the License.
18
- */
19
- import { EventEmitter } from "events";
20
- /**
21
- * Minimal FastAGI (`agi://host:port`) server: Asterisk connects, sends a
22
- * block of `agi_*` header lines terminated by a blank line, then this emits
23
- * "call" with an AgiChannel the caller can issue EXEC/SET VARIABLE commands
24
- * on. See AgiChannel.ts for why this is hand-rolled rather than a dependency.
25
- */
26
- declare class AgiServer extends EventEmitter {
27
- private server;
28
- constructor(props: {
29
- port: number;
30
- });
31
- private handleConnection;
32
- close(): void;
33
- }
34
- export { AgiServer };
@@ -1,109 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AgiServer = void 0;
37
- /**
38
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
39
- * http://github.com/fonoster/fonoster
40
- *
41
- * This file is part of Fonoster
42
- *
43
- * Licensed under the MIT License (the "License");
44
- * you may not use this file except in compliance with
45
- * the License. You may obtain a copy of the License at
46
- *
47
- * https://opensource.org/licenses/MIT
48
- *
49
- * Unless required by applicable law or agreed to in writing, software
50
- * distributed under the License is distributed on an "AS IS" BASIS,
51
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
52
- * See the License for the specific language governing permissions and
53
- * limitations under the License.
54
- */
55
- const events_1 = require("events");
56
- const net = __importStar(require("net"));
57
- const AgiChannel_1 = require("./AgiChannel");
58
- /**
59
- * Minimal FastAGI (`agi://host:port`) server: Asterisk connects, sends a
60
- * block of `agi_*` header lines terminated by a blank line, then this emits
61
- * "call" with an AgiChannel the caller can issue EXEC/SET VARIABLE commands
62
- * on. See AgiChannel.ts for why this is hand-rolled rather than a dependency.
63
- */
64
- class AgiServer extends events_1.EventEmitter {
65
- constructor(props) {
66
- super();
67
- this.server = net.createServer(this.handleConnection.bind(this));
68
- this.server.on("error", (err) => this.emit("error", err));
69
- this.server.listen(props.port, () => this.emit("ready", props.port));
70
- }
71
- handleConnection(socket) {
72
- let buffer = "";
73
- const onData = (data) => {
74
- buffer += data.toString();
75
- const delimiter = buffer.match(/\r?\n\r?\n/);
76
- if (!delimiter || delimiter.index === undefined)
77
- return;
78
- socket.off("data", onData);
79
- socket.off("error", onError);
80
- const header = buffer.slice(0, delimiter.index);
81
- const remainder = buffer.slice(delimiter.index + delimiter[0].length);
82
- const variables = {};
83
- header.split(/\r?\n/).forEach((line) => {
84
- if (!line.startsWith("agi_"))
85
- return;
86
- const separator = line.indexOf(":");
87
- if (separator < 0)
88
- return;
89
- variables[line.slice(4, separator).trim()] = line
90
- .slice(separator + 1)
91
- .trim();
92
- });
93
- const channel = new AgiChannel_1.AgiChannel({ socket, variables });
94
- this.emit("call", channel);
95
- if (remainder)
96
- socket.unshift(Buffer.from(remainder));
97
- };
98
- const onError = (err) => {
99
- if (this.listenerCount("error"))
100
- this.emit("error", err);
101
- };
102
- socket.once("error", onError);
103
- socket.on("data", onData);
104
- }
105
- close() {
106
- this.server.close();
107
- }
108
- }
109
- exports.AgiServer = AgiServer;
@@ -1,16 +0,0 @@
1
- import { AgiServer } from "./AgiServer";
2
- /**
3
- * Starts the FastAGI listener. Asterisk's `AGI(agi://amd:4573,${AMD_MODE})`
4
- * blocks the channel here. Per session: read the requested mode from the
5
- * dialplan's first AGI argument (`compact`, the default, or `full`), mint a
6
- * UUID, EXEC AudioSocket to pull the leading audio (blocks until the
7
- * AudioSocket side hangs up), await the correlated raw result, map it to
8
- * however many channel variables that mode produces, SET each, then return
9
- * so the dialplan resumes at Stasis(mediacontroller).
10
- *
11
- * Always fail-open: deadlines bound both the wait for a verdict and the wait
12
- * for AudioSocket() to return, and the AGI connection is always closed so the
13
- * dialplan resumes even when no variables could be set.
14
- */
15
- declare function startAgiServer(): AgiServer;
16
- export { startAgiServer };