getpatter 0.6.2 → 0.6.3
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 +1 -1
- package/dist/{carrier-config-4ZKVYAWV.mjs → carrier-config-3WDQXP5J.mjs} +43 -1
- package/dist/{chunk-LE63CSOB.mjs → chunk-Z6W5XFWS.mjs} +701 -35
- package/dist/index.d.mts +3599 -3381
- package/dist/index.d.ts +3599 -3381
- package/dist/index.js +1033 -191
- package/dist/index.mjs +262 -145
- package/dist/{test-mode-RS57BDM6.mjs → test-mode-MDBQ4ECE.mjs} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1901,6 +1901,160 @@ var init_elevenlabs_convai = __esm({
|
|
|
1901
1901
|
}
|
|
1902
1902
|
});
|
|
1903
1903
|
|
|
1904
|
+
// src/providers/plivo-adapter.ts
|
|
1905
|
+
async function dropPlivoVoicemail(callUuid, voicemailMessage, authId, authToken) {
|
|
1906
|
+
if (!callUuid || !voicemailMessage || !authId || !authToken) return;
|
|
1907
|
+
const auth2 = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
|
|
1908
|
+
const base = `${PLIVO_API_BASE}/Account/${encodeURIComponent(authId)}/Call/${encodeURIComponent(callUuid)}`;
|
|
1909
|
+
try {
|
|
1910
|
+
const speak = await fetch(`${base}/Speak/`, {
|
|
1911
|
+
method: "POST",
|
|
1912
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: auth2 },
|
|
1913
|
+
body: new URLSearchParams({ text: voicemailMessage }).toString(),
|
|
1914
|
+
signal: AbortSignal.timeout(1e4)
|
|
1915
|
+
});
|
|
1916
|
+
if (!speak.ok) {
|
|
1917
|
+
getLogger().warn(
|
|
1918
|
+
`Plivo voicemail Speak failed (${speak.status}): ${(await speak.text()).slice(0, 200)}`
|
|
1919
|
+
);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
await new Promise(
|
|
1923
|
+
(r) => setTimeout(r, Math.min(3e4, voicemailMessage.length * 60))
|
|
1924
|
+
);
|
|
1925
|
+
await fetch(`${base}/`, { method: "DELETE", headers: { Authorization: auth2 } });
|
|
1926
|
+
getLogger().info(`Voicemail dropped for ${callUuid}`);
|
|
1927
|
+
} catch (e) {
|
|
1928
|
+
getLogger().warn(`Could not drop voicemail: ${String(e)}`);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
function xmlEscapePlivo(s) {
|
|
1932
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1933
|
+
}
|
|
1934
|
+
var PLIVO_API_BASE, PlivoAdapter;
|
|
1935
|
+
var init_plivo_adapter = __esm({
|
|
1936
|
+
"src/providers/plivo-adapter.ts"() {
|
|
1937
|
+
"use strict";
|
|
1938
|
+
init_cjs_shims();
|
|
1939
|
+
init_logger();
|
|
1940
|
+
PLIVO_API_BASE = "https://api.plivo.com/v1";
|
|
1941
|
+
PlivoAdapter = class {
|
|
1942
|
+
authId;
|
|
1943
|
+
baseUrl;
|
|
1944
|
+
authHeader;
|
|
1945
|
+
constructor(authId, authToken) {
|
|
1946
|
+
if (!authId) throw new Error("PlivoAdapter: authId is required");
|
|
1947
|
+
if (!authToken) throw new Error("PlivoAdapter: authToken is required");
|
|
1948
|
+
this.authId = authId;
|
|
1949
|
+
this.baseUrl = `${PLIVO_API_BASE}/Account/${encodeURIComponent(authId)}`;
|
|
1950
|
+
this.authHeader = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
|
|
1951
|
+
}
|
|
1952
|
+
async request(method, path6, jsonBody) {
|
|
1953
|
+
const headers = { Authorization: this.authHeader };
|
|
1954
|
+
if (jsonBody !== void 0) headers["Content-Type"] = "application/json";
|
|
1955
|
+
const response = await fetch(`${this.baseUrl}${path6}`, {
|
|
1956
|
+
method,
|
|
1957
|
+
headers,
|
|
1958
|
+
body: jsonBody !== void 0 ? JSON.stringify(jsonBody) : void 0,
|
|
1959
|
+
signal: AbortSignal.timeout(3e4)
|
|
1960
|
+
});
|
|
1961
|
+
const text = await response.text();
|
|
1962
|
+
if (!response.ok && response.status !== 404) {
|
|
1963
|
+
throw new Error(`Plivo ${method} ${path6} failed: ${response.status} ${text}`);
|
|
1964
|
+
}
|
|
1965
|
+
let data = {};
|
|
1966
|
+
if (text) {
|
|
1967
|
+
try {
|
|
1968
|
+
data = JSON.parse(text);
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return { status: response.status, data };
|
|
1973
|
+
}
|
|
1974
|
+
/** Search and rent an available Plivo number for the given ISO country. */
|
|
1975
|
+
async provisionNumber(countryIso) {
|
|
1976
|
+
const { data } = await this.request(
|
|
1977
|
+
"GET",
|
|
1978
|
+
`/PhoneNumber/?country_iso=${encodeURIComponent(countryIso)}&limit=1`
|
|
1979
|
+
);
|
|
1980
|
+
const number4 = data.objects?.[0]?.number;
|
|
1981
|
+
if (!number4) throw new Error(`PlivoAdapter: no numbers available for ${countryIso}`);
|
|
1982
|
+
await this.request("POST", `/PhoneNumber/${encodeURIComponent(number4)}/`);
|
|
1983
|
+
return number4;
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Point the inbound answer flow for ``number`` at ``answerUrl`` by creating
|
|
1987
|
+
* (or reusing) a Plivo Application and linking the number to it. Most
|
|
1988
|
+
* production deployments pre-configure this in the Plivo console; this
|
|
1989
|
+
* mirrors Twilio's ``configureNumber`` auto-setup convenience.
|
|
1990
|
+
*/
|
|
1991
|
+
async configureNumber(number4, answerUrl) {
|
|
1992
|
+
const { data } = await this.request("POST", "/Application/", {
|
|
1993
|
+
app_name: "patter-inbound",
|
|
1994
|
+
answer_url: answerUrl,
|
|
1995
|
+
answer_method: "POST"
|
|
1996
|
+
});
|
|
1997
|
+
if (!data.app_id) {
|
|
1998
|
+
getLogger().warn("Plivo Application create returned no app_id");
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
await this.request("POST", `/Number/${encodeURIComponent(number4)}/`, { app_id: data.app_id });
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Place an outbound Plivo call routed through ``answerUrl``. Returns Plivo's
|
|
2005
|
+
* ``request_uuid``. The WSS URL travels inside the answer XML, not as a dial
|
|
2006
|
+
* parameter — mirroring the Python adapter.
|
|
2007
|
+
*/
|
|
2008
|
+
async initiateCall(opts) {
|
|
2009
|
+
const payload = {
|
|
2010
|
+
from: opts.from,
|
|
2011
|
+
to: opts.to,
|
|
2012
|
+
answer_url: opts.answerUrl,
|
|
2013
|
+
answer_method: "POST"
|
|
2014
|
+
};
|
|
2015
|
+
if (opts.ringTimeout != null) payload.ring_timeout = Math.max(1, Math.floor(opts.ringTimeout));
|
|
2016
|
+
if (opts.machineDetection) {
|
|
2017
|
+
payload.machine_detection = "true";
|
|
2018
|
+
payload.machine_detection_time = 5e3;
|
|
2019
|
+
if (opts.machineDetectionUrl) {
|
|
2020
|
+
payload.machine_detection_url = opts.machineDetectionUrl;
|
|
2021
|
+
payload.machine_detection_method = "POST";
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
const { data } = await this.request("POST", "/Call/", payload);
|
|
2025
|
+
return { requestUuid: data.request_uuid ?? "" };
|
|
2026
|
+
}
|
|
2027
|
+
/** Hang up an active Plivo call by CallUUID. 204 and 404 are both success. */
|
|
2028
|
+
async endCall(callUuid) {
|
|
2029
|
+
if (!callUuid) throw new Error("PlivoAdapter: callUuid is required");
|
|
2030
|
+
try {
|
|
2031
|
+
await this.request("DELETE", `/Call/${encodeURIComponent(callUuid)}/`);
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
getLogger().warn(`[PlivoAdapter] endCall failed for ${callUuid}: ${String(err)}`);
|
|
2034
|
+
throw err;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Build the Plivo answer XML. Unlike Twilio (``url=`` attribute), Plivo's
|
|
2039
|
+
* ``<Stream>`` takes the WSS URL as its **text content**. ``bidirectional``
|
|
2040
|
+
* enables two-way audio; ``keepCallAlive`` keeps the leg up for the lifetime
|
|
2041
|
+
* of the WebSocket. ``extraHeaders`` (comma-separated ``key=value``) is
|
|
2042
|
+
* delivered back on the WS ``start`` frame as a caller/callee fallback.
|
|
2043
|
+
*
|
|
2044
|
+
* Mirrors the Python adapter's ``generate_stream_xml``.
|
|
2045
|
+
*/
|
|
2046
|
+
static generateStreamXml(streamUrl, contentType = "audio/x-mulaw;rate=8000", extraHeaders) {
|
|
2047
|
+
let attrs = `bidirectional="true" keepCallAlive="true" contentType="${xmlEscapePlivo(contentType)}"`;
|
|
2048
|
+
if (extraHeaders) {
|
|
2049
|
+
const joined = Object.entries(extraHeaders).map(([k, v]) => `${k}=${v}`).join(",");
|
|
2050
|
+
attrs += ` extraHeaders="${xmlEscapePlivo(joined)}"`;
|
|
2051
|
+
}
|
|
2052
|
+
return `<Response><Stream ${attrs}>${xmlEscapePlivo(streamUrl)}</Stream></Response>`;
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
|
|
1904
2058
|
// src/provider-factory.ts
|
|
1905
2059
|
async function createSTT(agent) {
|
|
1906
2060
|
return agent.stt ?? null;
|
|
@@ -1915,6 +2069,178 @@ var init_provider_factory = __esm({
|
|
|
1915
2069
|
}
|
|
1916
2070
|
});
|
|
1917
2071
|
|
|
2072
|
+
// src/telephony/plivo.ts
|
|
2073
|
+
function classifyPlivoAmd(result) {
|
|
2074
|
+
const r = (result || "").trim().toLowerCase();
|
|
2075
|
+
if (r === "human" || r === "person") return "human";
|
|
2076
|
+
if (r.startsWith("machine") || r === "answering_machine" || r === "amd" || r === "true") {
|
|
2077
|
+
return "machine";
|
|
2078
|
+
}
|
|
2079
|
+
if (r === "fax") return "fax";
|
|
2080
|
+
return "unknown";
|
|
2081
|
+
}
|
|
2082
|
+
function validatePlivoSignature(url2, nonce, signature, authToken, params, method = "POST") {
|
|
2083
|
+
if (!signature || !nonce || !authToken) return false;
|
|
2084
|
+
let base = url2;
|
|
2085
|
+
if (method === "POST" && params && Object.keys(params).length > 0) {
|
|
2086
|
+
const keys = Object.keys(params).sort();
|
|
2087
|
+
base += keys.map((k) => `${k}${params[k]}`).join("");
|
|
2088
|
+
}
|
|
2089
|
+
const signed = `${base}.${nonce}`;
|
|
2090
|
+
const expected = import_node_crypto.default.createHmac("sha256", authToken).update(signed).digest("base64");
|
|
2091
|
+
const expBuf = Buffer.from(expected);
|
|
2092
|
+
for (const rawSig of signature.split(",")) {
|
|
2093
|
+
const trimmed = rawSig.trim();
|
|
2094
|
+
if (!trimmed) continue;
|
|
2095
|
+
try {
|
|
2096
|
+
const sigBuf = Buffer.from(trimmed);
|
|
2097
|
+
if (sigBuf.length === expBuf.length && import_node_crypto.default.timingSafeEqual(sigBuf, expBuf)) {
|
|
2098
|
+
return true;
|
|
2099
|
+
}
|
|
2100
|
+
} catch {
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return false;
|
|
2105
|
+
}
|
|
2106
|
+
var import_node_crypto, Carrier, PLIVO_DTMF_ALLOWED, PlivoBridge;
|
|
2107
|
+
var init_plivo = __esm({
|
|
2108
|
+
"src/telephony/plivo.ts"() {
|
|
2109
|
+
"use strict";
|
|
2110
|
+
init_cjs_shims();
|
|
2111
|
+
import_node_crypto = __toESM(require("crypto"));
|
|
2112
|
+
init_provider_factory();
|
|
2113
|
+
init_logger();
|
|
2114
|
+
Carrier = class {
|
|
2115
|
+
kind = "plivo";
|
|
2116
|
+
authId;
|
|
2117
|
+
authToken;
|
|
2118
|
+
constructor(opts = {}) {
|
|
2119
|
+
const authId = opts.authId ?? process.env.PLIVO_AUTH_ID;
|
|
2120
|
+
const authToken = opts.authToken ?? process.env.PLIVO_AUTH_TOKEN;
|
|
2121
|
+
if (!authId) {
|
|
2122
|
+
throw new Error(
|
|
2123
|
+
"Plivo carrier requires authId. Pass { authId: 'MA...' } or set PLIVO_AUTH_ID in the environment."
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
if (!authToken) {
|
|
2127
|
+
throw new Error(
|
|
2128
|
+
"Plivo carrier requires authToken. Pass { authToken: '...' } or set PLIVO_AUTH_TOKEN in the environment."
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
this.authId = authId;
|
|
2132
|
+
this.authToken = authToken;
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
PLIVO_DTMF_ALLOWED = new Set("0123456789*#ABCDabcdwW");
|
|
2136
|
+
PlivoBridge = class {
|
|
2137
|
+
constructor(config2) {
|
|
2138
|
+
this.config = config2;
|
|
2139
|
+
const authId = config2.plivoAuthId ?? "";
|
|
2140
|
+
const authToken = config2.plivoAuthToken ?? "";
|
|
2141
|
+
this.authHeader = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
|
|
2142
|
+
this.apiBase = `https://api.plivo.com/v1/Account/${encodeURIComponent(authId)}`;
|
|
2143
|
+
}
|
|
2144
|
+
config;
|
|
2145
|
+
label = "Plivo";
|
|
2146
|
+
telephonyProvider = "plivo";
|
|
2147
|
+
inputWireFormat = "ulaw_8000";
|
|
2148
|
+
authHeader;
|
|
2149
|
+
apiBase;
|
|
2150
|
+
sendAudio(ws, audioBase64, _streamSid) {
|
|
2151
|
+
ws.send(
|
|
2152
|
+
JSON.stringify({
|
|
2153
|
+
event: "playAudio",
|
|
2154
|
+
media: { contentType: "audio/x-mulaw", sampleRate: 8e3, payload: audioBase64 }
|
|
2155
|
+
})
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
sendMark(ws, markName, streamSid) {
|
|
2159
|
+
ws.send(JSON.stringify({ event: "checkpoint", streamId: streamSid, name: markName }));
|
|
2160
|
+
}
|
|
2161
|
+
sendClear(ws, streamSid) {
|
|
2162
|
+
ws.send(JSON.stringify({ event: "clearAudio", streamId: streamSid }));
|
|
2163
|
+
}
|
|
2164
|
+
async transferCall(callId, toNumber) {
|
|
2165
|
+
if (!/^\+[1-9]\d{6,14}$/.test(toNumber)) {
|
|
2166
|
+
getLogger().warn(`PlivoBridge.transferCall rejected: invalid target ${JSON.stringify(toNumber)}`);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
if (!this.config.plivoAuthId || !this.config.plivoAuthToken || !callId) return;
|
|
2170
|
+
if (!this.config.webhookUrl) {
|
|
2171
|
+
getLogger().warn("PlivoBridge.transferCall skipped: no webhookUrl for aleg_url");
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const alegUrl = `https://${this.config.webhookUrl}/webhooks/plivo/transfer?to=${encodeURIComponent(toNumber)}`;
|
|
2175
|
+
await fetch(`${this.apiBase}/Call/${encodeURIComponent(callId)}/`, {
|
|
2176
|
+
method: "POST",
|
|
2177
|
+
headers: { "Content-Type": "application/json", Authorization: this.authHeader },
|
|
2178
|
+
body: JSON.stringify({ legs: "aleg", aleg_url: alegUrl, aleg_method: "GET" })
|
|
2179
|
+
});
|
|
2180
|
+
getLogger().info(`Call transferred to ${toNumber}`);
|
|
2181
|
+
}
|
|
2182
|
+
async sendDtmf(ws, _callId, digits, _delayMs) {
|
|
2183
|
+
const filtered = Array.from(digits ?? "").filter((d) => PLIVO_DTMF_ALLOWED.has(d)).join("");
|
|
2184
|
+
if (!filtered) {
|
|
2185
|
+
getLogger().warn(`PlivoBridge.sendDtmf: no valid digits in ${JSON.stringify(digits)}`);
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
ws.send(JSON.stringify({ event: "sendDTMF", dtmf: filtered }));
|
|
2189
|
+
}
|
|
2190
|
+
async startRecording(callId) {
|
|
2191
|
+
if (!this.config.plivoAuthId || !this.config.plivoAuthToken || !callId) return;
|
|
2192
|
+
try {
|
|
2193
|
+
const resp = await fetch(`${this.apiBase}/Call/${encodeURIComponent(callId)}/Record/`, {
|
|
2194
|
+
method: "POST",
|
|
2195
|
+
headers: { Authorization: this.authHeader }
|
|
2196
|
+
});
|
|
2197
|
+
if (!resp.ok) {
|
|
2198
|
+
getLogger().warn(`Plivo record start failed (${resp.status}): ${(await resp.text()).slice(0, 200)}`);
|
|
2199
|
+
} else {
|
|
2200
|
+
getLogger().info("Plivo recording started");
|
|
2201
|
+
}
|
|
2202
|
+
} catch (e) {
|
|
2203
|
+
getLogger().warn(`Plivo record start error: ${String(e)}`);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
async endCall(callId, _ws) {
|
|
2207
|
+
if (!this.config.plivoAuthId || !this.config.plivoAuthToken || !callId) return;
|
|
2208
|
+
try {
|
|
2209
|
+
const resp = await fetch(`${this.apiBase}/Call/${encodeURIComponent(callId)}/`, {
|
|
2210
|
+
method: "DELETE",
|
|
2211
|
+
headers: { Authorization: this.authHeader }
|
|
2212
|
+
});
|
|
2213
|
+
if (!resp.ok && resp.status !== 404) {
|
|
2214
|
+
getLogger().warn(`Plivo hangup returned ${resp.status}`);
|
|
2215
|
+
}
|
|
2216
|
+
} catch {
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
createStt(agent) {
|
|
2220
|
+
return createSTT(agent);
|
|
2221
|
+
}
|
|
2222
|
+
async queryTelephonyCost(metricsAcc, callId) {
|
|
2223
|
+
if (!this.config.plivoAuthId || !this.config.plivoAuthToken || !callId) return;
|
|
2224
|
+
try {
|
|
2225
|
+
const resp = await fetch(`${this.apiBase}/Call/${encodeURIComponent(callId)}/`, {
|
|
2226
|
+
headers: { Authorization: this.authHeader },
|
|
2227
|
+
signal: AbortSignal.timeout(5e3)
|
|
2228
|
+
});
|
|
2229
|
+
if (resp.ok) {
|
|
2230
|
+
const data = await resp.json();
|
|
2231
|
+
if (data.total_amount != null) {
|
|
2232
|
+
metricsAcc.setActualTelephonyCost(Math.abs(parseFloat(data.total_amount)));
|
|
2233
|
+
getLogger().info(`Plivo actual cost: $${data.total_amount}`);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
getLogger().debug(`queryTelephonyCost(plivo) failed: ${err?.message ?? err}`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
|
|
1918
2244
|
// src/pricing.ts
|
|
1919
2245
|
function resolveProviderRates(providerConfig, model) {
|
|
1920
2246
|
if (!providerConfig) return { unit: "" };
|
|
@@ -2054,7 +2380,7 @@ function calculateLlmCost(provider2, model, inputTokens, outputTokens, cacheRead
|
|
|
2054
2380
|
function calculateTelephonyCost(provider2, durationSeconds, pricing) {
|
|
2055
2381
|
const config2 = pricing[provider2];
|
|
2056
2382
|
if (!config2 || config2.unit !== "minute") return 0;
|
|
2057
|
-
const minutes =
|
|
2383
|
+
const minutes = config2.roundUp ? Math.ceil(durationSeconds / 60) : durationSeconds / 60;
|
|
2058
2384
|
return minutes * (config2.price ?? 0);
|
|
2059
2385
|
}
|
|
2060
2386
|
var PRICING_VERSION, PRICING_LAST_UPDATED, PricingUnit, DEFAULT_PRICING, llmPricing;
|
|
@@ -2283,7 +2609,7 @@ var init_pricing = __esm({
|
|
|
2283
2609
|
// twilio default = US inbound local (the 99% case for voice agents receiving
|
|
2284
2610
|
// calls on a local number). For US toll-free inbound ($0.022/min) or US
|
|
2285
2611
|
// outbound local ($0.0140/min), override via Patter({ pricing: { twilio: {...} } }).
|
|
2286
|
-
twilio: { unit: PricingUnit.MINUTE, price: 85e-4 },
|
|
2612
|
+
twilio: { unit: PricingUnit.MINUTE, price: 85e-4, roundUp: true },
|
|
2287
2613
|
// Telnyx — direction-aware rates as of 2026-05-11.
|
|
2288
2614
|
// Sources:
|
|
2289
2615
|
// https://telnyx.com/pricing/elastic-sip
|
|
@@ -2301,7 +2627,17 @@ var init_pricing = __esm({
|
|
|
2301
2627
|
// price: 0.0035 } } })`` to bill all inbound at the lower rate.
|
|
2302
2628
|
telnyx: { unit: PricingUnit.MINUTE, price: 7e-3 },
|
|
2303
2629
|
telnyx_inbound: { unit: PricingUnit.MINUTE, price: 35e-4 },
|
|
2304
|
-
telnyx_outbound: { unit: PricingUnit.MINUTE, price: 7e-3 }
|
|
2630
|
+
telnyx_outbound: { unit: PricingUnit.MINUTE, price: 7e-3 },
|
|
2631
|
+
// Plivo — official US pay-as-you-go voice rates (per minute; Plivo rounds
|
|
2632
|
+
// partial minutes up like Twilio). Source: https://www.plivo.com/voice/pricing/
|
|
2633
|
+
// US local inbound: $0.0055/min
|
|
2634
|
+
// US local outbound: $0.0115/min
|
|
2635
|
+
// US toll-free inbound: $0.0180/min (override via new Patter({ pricing }))
|
|
2636
|
+
// The flat ``plivo`` key defaults to inbound local; the billed amount is
|
|
2637
|
+
// also reconciled post-call from the Plivo CDR (``total_amount``).
|
|
2638
|
+
plivo: { unit: PricingUnit.MINUTE, price: 55e-4, roundUp: true },
|
|
2639
|
+
plivo_inbound: { unit: PricingUnit.MINUTE, price: 55e-4, roundUp: true },
|
|
2640
|
+
plivo_outbound: { unit: PricingUnit.MINUTE, price: 0.0115, roundUp: true }
|
|
2305
2641
|
};
|
|
2306
2642
|
llmPricing = {
|
|
2307
2643
|
anthropic: {
|
|
@@ -2985,10 +3321,10 @@ function timingSafeCompare(a, b) {
|
|
|
2985
3321
|
const aBuf = Buffer.from(a);
|
|
2986
3322
|
const bBuf = Buffer.from(b);
|
|
2987
3323
|
if (aBuf.length !== bBuf.length) {
|
|
2988
|
-
|
|
3324
|
+
import_node_crypto2.default.timingSafeEqual(aBuf, aBuf);
|
|
2989
3325
|
return false;
|
|
2990
3326
|
}
|
|
2991
|
-
return
|
|
3327
|
+
return import_node_crypto2.default.timingSafeEqual(aBuf, bBuf);
|
|
2992
3328
|
}
|
|
2993
3329
|
function makeAuthMiddleware(token = "") {
|
|
2994
3330
|
return (req, res, next) => {
|
|
@@ -3010,12 +3346,12 @@ function makeAuthMiddleware(token = "") {
|
|
|
3010
3346
|
res.status(401).json({ error: "Unauthorized" });
|
|
3011
3347
|
};
|
|
3012
3348
|
}
|
|
3013
|
-
var
|
|
3349
|
+
var import_node_crypto2;
|
|
3014
3350
|
var init_auth = __esm({
|
|
3015
3351
|
"src/dashboard/auth.ts"() {
|
|
3016
3352
|
"use strict";
|
|
3017
3353
|
init_cjs_shims();
|
|
3018
|
-
|
|
3354
|
+
import_node_crypto2 = __toESM(require("crypto"));
|
|
3019
3355
|
}
|
|
3020
3356
|
});
|
|
3021
3357
|
|
|
@@ -3320,12 +3656,12 @@ function isRemoteUrl(onMessage) {
|
|
|
3320
3656
|
function isWebSocketUrl(url2) {
|
|
3321
3657
|
return url2.startsWith("ws://") || url2.startsWith("wss://");
|
|
3322
3658
|
}
|
|
3323
|
-
var
|
|
3659
|
+
var import_node_crypto3, MAX_RESPONSE_BYTES, RemoteMessageHandler;
|
|
3324
3660
|
var init_remote_message = __esm({
|
|
3325
3661
|
"src/remote-message.ts"() {
|
|
3326
3662
|
"use strict";
|
|
3327
3663
|
init_cjs_shims();
|
|
3328
|
-
|
|
3664
|
+
import_node_crypto3 = __toESM(require("crypto"));
|
|
3329
3665
|
init_logger();
|
|
3330
3666
|
init_server();
|
|
3331
3667
|
MAX_RESPONSE_BYTES = 64 * 1024;
|
|
@@ -3346,7 +3682,7 @@ var init_remote_message = __esm({
|
|
|
3346
3682
|
if (!this.webhookSecret) {
|
|
3347
3683
|
throw new Error("Cannot sign without a webhookSecret");
|
|
3348
3684
|
}
|
|
3349
|
-
return
|
|
3685
|
+
return import_node_crypto3.default.createHmac("sha256", this.webhookSecret).update(body).digest("hex");
|
|
3350
3686
|
}
|
|
3351
3687
|
/**
|
|
3352
3688
|
* Release resources held by this handler.
|
|
@@ -22119,7 +22455,7 @@ var init_transport = __esm({
|
|
|
22119
22455
|
|
|
22120
22456
|
// node_modules/pkce-challenge/dist/index.node.js
|
|
22121
22457
|
async function getRandomValues(size) {
|
|
22122
|
-
return (await
|
|
22458
|
+
return (await crypto4).getRandomValues(new Uint8Array(size));
|
|
22123
22459
|
}
|
|
22124
22460
|
async function random(size) {
|
|
22125
22461
|
const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
|
|
@@ -22139,7 +22475,7 @@ async function generateVerifier(length) {
|
|
|
22139
22475
|
return await random(length);
|
|
22140
22476
|
}
|
|
22141
22477
|
async function generateChallenge(code_verifier) {
|
|
22142
|
-
const buffer = await (await
|
|
22478
|
+
const buffer = await (await crypto4).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
|
|
22143
22479
|
return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "");
|
|
22144
22480
|
}
|
|
22145
22481
|
async function pkceChallenge(length) {
|
|
@@ -22155,12 +22491,12 @@ async function pkceChallenge(length) {
|
|
|
22155
22491
|
code_challenge: challenge
|
|
22156
22492
|
};
|
|
22157
22493
|
}
|
|
22158
|
-
var
|
|
22494
|
+
var crypto4;
|
|
22159
22495
|
var init_index_node = __esm({
|
|
22160
22496
|
"node_modules/pkce-challenge/dist/index.node.js"() {
|
|
22161
22497
|
"use strict";
|
|
22162
22498
|
init_cjs_shims();
|
|
22163
|
-
|
|
22499
|
+
crypto4 = globalThis.crypto?.webcrypto ?? // Node.js [18-16] REPL
|
|
22164
22500
|
globalThis.crypto ?? // Node.js >18
|
|
22165
22501
|
import("crypto").then((m) => m.webcrypto);
|
|
22166
22502
|
}
|
|
@@ -26754,6 +27090,35 @@ function maskPhoneNumber(number4) {
|
|
|
26754
27090
|
function isValidE164(number4) {
|
|
26755
27091
|
return /^\+[1-9]\d{6,14}$/.test(number4);
|
|
26756
27092
|
}
|
|
27093
|
+
function augmentWithBuiltinHandoffTools(userTools, callbacks) {
|
|
27094
|
+
const out = [...userTools ?? []];
|
|
27095
|
+
if (callbacks.transferCall) {
|
|
27096
|
+
const transferCall = callbacks.transferCall;
|
|
27097
|
+
out.push({
|
|
27098
|
+
...TRANSFER_CALL_TOOL,
|
|
27099
|
+
handler: async (args) => {
|
|
27100
|
+
const number4 = typeof args.number === "string" ? args.number : "";
|
|
27101
|
+
if (!isValidE164(number4)) {
|
|
27102
|
+
return JSON.stringify({ error: "Invalid phone number format", status: "rejected" });
|
|
27103
|
+
}
|
|
27104
|
+
await transferCall(number4);
|
|
27105
|
+
return JSON.stringify({ status: "transferring", to: number4 });
|
|
27106
|
+
}
|
|
27107
|
+
});
|
|
27108
|
+
}
|
|
27109
|
+
if (callbacks.endCall) {
|
|
27110
|
+
const endCall = callbacks.endCall;
|
|
27111
|
+
out.push({
|
|
27112
|
+
...END_CALL_TOOL,
|
|
27113
|
+
handler: async (args) => {
|
|
27114
|
+
const reason = typeof args.reason === "string" ? args.reason : "conversation_complete";
|
|
27115
|
+
await endCall(reason);
|
|
27116
|
+
return JSON.stringify({ status: "ending", reason });
|
|
27117
|
+
}
|
|
27118
|
+
});
|
|
27119
|
+
}
|
|
27120
|
+
return out;
|
|
27121
|
+
}
|
|
26757
27122
|
async function queryDeepgramCost(metricsAcc, deepgramKey, deepgramRequestId) {
|
|
26758
27123
|
try {
|
|
26759
27124
|
const projResp = await fetch("https://api.deepgram.com/v1/projects", {
|
|
@@ -27397,8 +27762,8 @@ var init_stream_handler = __esm({
|
|
|
27397
27762
|
this.ttsByteCarry = null;
|
|
27398
27763
|
}
|
|
27399
27764
|
/**
|
|
27400
|
-
* Start call recording when configured.
|
|
27401
|
-
*
|
|
27765
|
+
* Start call recording when configured. Bridges expose
|
|
27766
|
+
* ``startRecording`` for carrier parity (Twilio and Telnyx supported).
|
|
27402
27767
|
*/
|
|
27403
27768
|
async startRecordingIfRequested(callId) {
|
|
27404
27769
|
const { recording, config: config2 } = this.deps;
|
|
@@ -27644,7 +28009,7 @@ var init_stream_handler = __esm({
|
|
|
27644
28009
|
this.metricsAcc.addSttAudioBytes(pcm16k.length);
|
|
27645
28010
|
}
|
|
27646
28011
|
} else if (this.adapter) {
|
|
27647
|
-
if (this.adapter instanceof ElevenLabsConvAIAdapter && this.deps.bridge.
|
|
28012
|
+
if (this.adapter instanceof ElevenLabsConvAIAdapter && this.deps.bridge.inputWireFormat === "ulaw_8000" && this.adapter.inputAudioFormat !== "ulaw_8000") {
|
|
27648
28013
|
const pcm8k = mulawToPcm16(audioBuffer);
|
|
27649
28014
|
const pcm16k = this.inboundResampler.process(pcm8k);
|
|
27650
28015
|
this.adapter.sendAudio(pcm16k);
|
|
@@ -27805,6 +28170,7 @@ var init_stream_handler = __esm({
|
|
|
27805
28170
|
const carrier = this.deps.bridge.telephonyProvider;
|
|
27806
28171
|
if (carrier === "twilio") return fmt === "ulaw_8000";
|
|
27807
28172
|
if (carrier === "telnyx") return fmt === "pcm_16000";
|
|
28173
|
+
if (carrier === "plivo") return fmt === "ulaw_8000";
|
|
27808
28174
|
return false;
|
|
27809
28175
|
}
|
|
27810
28176
|
/**
|
|
@@ -27921,12 +28287,9 @@ var init_stream_handler = __esm({
|
|
|
27921
28287
|
}
|
|
27922
28288
|
}
|
|
27923
28289
|
if (this.deps.agent.echoCancellation) {
|
|
27924
|
-
|
|
27925
|
-
|
|
27926
|
-
|
|
27927
|
-
`echoCancellation: true on ${carrier} (PSTN). Server-side NLMS cannot model PSTN's ~250\u20131500 ms round-trip echo with a 32 ms filter window \u2014 it will silently no-op. Best practice: keep echoCancellation: false; rely on the carrier + caller device's built-in echo suppression and Patter's self-hearing guard. Enable AEC only for browser/native deployments where the SDK owns the audio path end-to-end.`
|
|
27928
|
-
);
|
|
27929
|
-
}
|
|
28290
|
+
getLogger().warn(
|
|
28291
|
+
`echoCancellation: true on ${this.deps.bridge.telephonyProvider} (PSTN). Server-side NLMS cannot model PSTN's ~250\u20131500 ms round-trip echo with a 32 ms filter window \u2014 it will silently no-op. Best practice: keep echoCancellation: false; rely on the carrier + caller device's built-in echo suppression and Patter's self-hearing guard. Enable AEC only for browser/native deployments where the SDK owns the audio path end-to-end.`
|
|
28292
|
+
);
|
|
27930
28293
|
try {
|
|
27931
28294
|
const { NlmsEchoCanceller: NlmsEchoCanceller2 } = await Promise.resolve().then(() => (init_aec(), aec_exports));
|
|
27932
28295
|
this.aec = new NlmsEchoCanceller2({ sampleRate: 16e3 });
|
|
@@ -28059,13 +28422,20 @@ var init_stream_handler = __esm({
|
|
|
28059
28422
|
);
|
|
28060
28423
|
}
|
|
28061
28424
|
const providerModel = this.deps.agent.llm?.model ?? "";
|
|
28425
|
+
const augmentedTools = augmentWithBuiltinHandoffTools(
|
|
28426
|
+
this.deps.agent.tools,
|
|
28427
|
+
{
|
|
28428
|
+
transferCall: (number4) => this.deps.bridge.transferCall(this.callId, number4),
|
|
28429
|
+
endCall: () => this.deps.bridge.endCall(this.callId, this.ws)
|
|
28430
|
+
}
|
|
28431
|
+
);
|
|
28062
28432
|
this.llmLoop = new LLMLoop(
|
|
28063
28433
|
"",
|
|
28064
28434
|
// apiKey unused when llmProvider is supplied
|
|
28065
28435
|
providerModel,
|
|
28066
28436
|
// propagate so calculateLlmCost can match the price row
|
|
28067
28437
|
resolvedPrompt,
|
|
28068
|
-
|
|
28438
|
+
augmentedTools,
|
|
28069
28439
|
this.deps.agent.llm,
|
|
28070
28440
|
this.deps.agent.disablePhonePreamble ?? false
|
|
28071
28441
|
);
|
|
@@ -28076,11 +28446,18 @@ var init_stream_handler = __esm({
|
|
|
28076
28446
|
} else if (!this.deps.onMessage && this.deps.config.openaiKey) {
|
|
28077
28447
|
let llmModel = this.deps.agent.model || "gpt-4o-mini";
|
|
28078
28448
|
if (llmModel.includes("realtime")) llmModel = "gpt-4o-mini";
|
|
28449
|
+
const augmentedTools = augmentWithBuiltinHandoffTools(
|
|
28450
|
+
this.deps.agent.tools,
|
|
28451
|
+
{
|
|
28452
|
+
transferCall: (number4) => this.deps.bridge.transferCall(this.callId, number4),
|
|
28453
|
+
endCall: () => this.deps.bridge.endCall(this.callId, this.ws)
|
|
28454
|
+
}
|
|
28455
|
+
);
|
|
28079
28456
|
this.llmLoop = new LLMLoop(
|
|
28080
28457
|
this.deps.config.openaiKey,
|
|
28081
28458
|
llmModel,
|
|
28082
28459
|
resolvedPrompt,
|
|
28083
|
-
|
|
28460
|
+
augmentedTools,
|
|
28084
28461
|
void 0,
|
|
28085
28462
|
this.deps.agent.disablePhonePreamble ?? false
|
|
28086
28463
|
);
|
|
@@ -29178,7 +29555,7 @@ function redactPhone(raw) {
|
|
|
29178
29555
|
const mode = redactMode();
|
|
29179
29556
|
if (mode === "full") return raw;
|
|
29180
29557
|
if (mode === "hash_only") {
|
|
29181
|
-
return "sha256:" +
|
|
29558
|
+
return "sha256:" + crypto5.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 16);
|
|
29182
29559
|
}
|
|
29183
29560
|
return maskPhoneNumber(raw);
|
|
29184
29561
|
}
|
|
@@ -29189,7 +29566,7 @@ function utcIso(tsSeconds) {
|
|
|
29189
29566
|
async function atomicWriteJson(filePath, payload) {
|
|
29190
29567
|
const dir = path4.dirname(filePath);
|
|
29191
29568
|
await import_node_fs2.promises.mkdir(dir, { recursive: true });
|
|
29192
|
-
const tmp = path4.join(dir, `.tmp.${process.pid}.${
|
|
29569
|
+
const tmp = path4.join(dir, `.tmp.${process.pid}.${crypto5.randomBytes(4).toString("hex")}.json`);
|
|
29193
29570
|
try {
|
|
29194
29571
|
const handle = await import_node_fs2.promises.open(tmp, "w");
|
|
29195
29572
|
try {
|
|
@@ -29229,12 +29606,12 @@ function rmTree(target) {
|
|
|
29229
29606
|
} catch {
|
|
29230
29607
|
}
|
|
29231
29608
|
}
|
|
29232
|
-
var
|
|
29609
|
+
var crypto5, fs4, import_node_fs2, os, path4, SCHEMA_VERSION, DEFAULT_RETENTION_DAYS, CallLogger;
|
|
29233
29610
|
var init_call_log = __esm({
|
|
29234
29611
|
"src/services/call-log.ts"() {
|
|
29235
29612
|
"use strict";
|
|
29236
29613
|
init_cjs_shims();
|
|
29237
|
-
|
|
29614
|
+
crypto5 = __toESM(require("crypto"));
|
|
29238
29615
|
fs4 = __toESM(require("fs"));
|
|
29239
29616
|
import_node_fs2 = require("fs");
|
|
29240
29617
|
os = __toESM(require("os"));
|
|
@@ -29306,7 +29683,7 @@ var init_call_log = __esm({
|
|
|
29306
29683
|
} catch (err) {
|
|
29307
29684
|
getLogger().warn(`call_log write failed (${sanitizeLogValue(callId)}): ${sanitizeLogValue(String(err))}`);
|
|
29308
29685
|
}
|
|
29309
|
-
if (
|
|
29686
|
+
if (crypto5.randomBytes(1)[0] < 5) {
|
|
29310
29687
|
this.sweepOldDays();
|
|
29311
29688
|
}
|
|
29312
29689
|
}
|
|
@@ -29443,6 +29820,19 @@ function classifyTelnyxAmd(result) {
|
|
|
29443
29820
|
if (result === "fax") return "fax";
|
|
29444
29821
|
return "unknown";
|
|
29445
29822
|
}
|
|
29823
|
+
function twilioStatusToOutcome(callStatus) {
|
|
29824
|
+
const s = (callStatus || "").toLowerCase();
|
|
29825
|
+
if (s === "no-answer") return "no_answer";
|
|
29826
|
+
if (s === "busy") return "busy";
|
|
29827
|
+
return "failed";
|
|
29828
|
+
}
|
|
29829
|
+
function telnyxHangupOutcome(cause) {
|
|
29830
|
+
const c = (cause || "").toLowerCase();
|
|
29831
|
+
if (c === "no_answer" || c === "timeout" || c === "no_user_response") return "no_answer";
|
|
29832
|
+
if (c === "user_busy" || c === "busy") return "busy";
|
|
29833
|
+
if (c === "call_rejected" || c === "rejected" || c === "destination_out_of_order") return "failed";
|
|
29834
|
+
return null;
|
|
29835
|
+
}
|
|
29446
29836
|
function validateWebhookUrl(url2) {
|
|
29447
29837
|
const parsed = new URL(url2);
|
|
29448
29838
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
@@ -29500,7 +29890,7 @@ function validateTelnyxSignature(rawBody, signature, timestamp, publicKey, toler
|
|
|
29500
29890
|
if (ageMs < 0 || ageMs > toleranceSec * 1e3) return false;
|
|
29501
29891
|
const payload = `${timestamp}|${rawBody}`;
|
|
29502
29892
|
const keyBuffer = Buffer.from(publicKey, "base64");
|
|
29503
|
-
const keyObject =
|
|
29893
|
+
const keyObject = import_node_crypto4.default.createPublicKey({
|
|
29504
29894
|
key: keyBuffer,
|
|
29505
29895
|
format: "der",
|
|
29506
29896
|
type: "spki"
|
|
@@ -29510,7 +29900,7 @@ function validateTelnyxSignature(rawBody, signature, timestamp, publicKey, toler
|
|
|
29510
29900
|
if (!trimmed) continue;
|
|
29511
29901
|
try {
|
|
29512
29902
|
const sigBuffer = Buffer.from(trimmed, "base64");
|
|
29513
|
-
if (
|
|
29903
|
+
if (import_node_crypto4.default.verify(null, Buffer.from(payload), keyObject, sigBuffer)) {
|
|
29514
29904
|
return true;
|
|
29515
29905
|
}
|
|
29516
29906
|
} catch {
|
|
@@ -29527,12 +29917,12 @@ function validateTwilioSid(sid, prefix = "CA") {
|
|
|
29527
29917
|
}
|
|
29528
29918
|
function validateTwilioSignature(url2, params, signature, authToken) {
|
|
29529
29919
|
const data = url2 + Object.keys(params).sort().reduce((acc, key) => acc + key + (params[key] ?? ""), "");
|
|
29530
|
-
const expected =
|
|
29920
|
+
const expected = import_node_crypto4.default.createHmac("sha1", authToken).update(data).digest("base64");
|
|
29531
29921
|
try {
|
|
29532
29922
|
const sigBuf = Buffer.from(signature);
|
|
29533
29923
|
const expBuf = Buffer.from(expected);
|
|
29534
29924
|
if (sigBuf.length !== expBuf.length) return false;
|
|
29535
|
-
return
|
|
29925
|
+
return import_node_crypto4.default.timingSafeEqual(sigBuf, expBuf);
|
|
29536
29926
|
} catch {
|
|
29537
29927
|
return false;
|
|
29538
29928
|
}
|
|
@@ -29607,18 +29997,20 @@ async function sleep(ms) {
|
|
|
29607
29997
|
if (ms <= 0) return;
|
|
29608
29998
|
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
29609
29999
|
}
|
|
29610
|
-
var
|
|
30000
|
+
var import_node_crypto4, import_express, import_http, import_ws5, TRANSFER_CALL_TOOL, END_CALL_TOOL, TwilioBridge, TELNYX_DTMF_ALLOWED, TELNYX_DTMF_DURATION_MS, TelnyxBridge, GRACEFUL_SHUTDOWN_TIMEOUT_MS, EmbeddedServer;
|
|
29611
30001
|
var init_server = __esm({
|
|
29612
30002
|
"src/server.ts"() {
|
|
29613
30003
|
"use strict";
|
|
29614
30004
|
init_cjs_shims();
|
|
29615
|
-
|
|
30005
|
+
import_node_crypto4 = __toESM(require("crypto"));
|
|
29616
30006
|
import_express = __toESM(require("express"));
|
|
29617
30007
|
import_http = require("http");
|
|
29618
30008
|
import_ws5 = require("ws");
|
|
29619
30009
|
init_openai_realtime();
|
|
29620
30010
|
init_openai_realtime_2();
|
|
29621
30011
|
init_elevenlabs_convai();
|
|
30012
|
+
init_plivo_adapter();
|
|
30013
|
+
init_plivo();
|
|
29622
30014
|
init_provider_factory();
|
|
29623
30015
|
init_pricing();
|
|
29624
30016
|
init_store();
|
|
@@ -29661,6 +30053,7 @@ var init_server = __esm({
|
|
|
29661
30053
|
config;
|
|
29662
30054
|
label = "Twilio";
|
|
29663
30055
|
telephonyProvider = "twilio";
|
|
30056
|
+
inputWireFormat = "ulaw_8000";
|
|
29664
30057
|
sendAudio(ws, audioBase64, streamSid) {
|
|
29665
30058
|
ws.send(JSON.stringify({ event: "media", streamSid, media: { payload: audioBase64 } }));
|
|
29666
30059
|
}
|
|
@@ -29748,6 +30141,11 @@ var init_server = __esm({
|
|
|
29748
30141
|
config;
|
|
29749
30142
|
label = "Telnyx";
|
|
29750
30143
|
telephonyProvider = "telnyx";
|
|
30144
|
+
// ``streaming_start`` negotiates PCMU bidirectional by default — keeping
|
|
30145
|
+
// ``ulaw_8000`` here matches what TwilioBridge does and keeps the stream
|
|
30146
|
+
// handler's input-transcode branch in the right shape. If a deployment
|
|
30147
|
+
// overrides the negotiation to L16, this should flip to ``pcm_16000``.
|
|
30148
|
+
inputWireFormat = "ulaw_8000";
|
|
29751
30149
|
sendAudio(ws, audioBase64, _streamSid) {
|
|
29752
30150
|
ws.send(JSON.stringify({ event: "media", media: { payload: audioBase64 } }));
|
|
29753
30151
|
}
|
|
@@ -29769,7 +30167,7 @@ var init_server = __esm({
|
|
|
29769
30167
|
});
|
|
29770
30168
|
getLogger().info(`Telnyx call transferred to ${toNumber}`);
|
|
29771
30169
|
}
|
|
29772
|
-
async sendDtmf(callId, digits, delayMs) {
|
|
30170
|
+
async sendDtmf(_ws, callId, digits, delayMs) {
|
|
29773
30171
|
if (!digits) {
|
|
29774
30172
|
getLogger().warn("TelnyxBridge.sendDtmf called with empty digits");
|
|
29775
30173
|
return;
|
|
@@ -29967,6 +30365,99 @@ var init_server = __esm({
|
|
|
29967
30365
|
* (tests) work without further setup. See FIX #91.
|
|
29968
30366
|
*/
|
|
29969
30367
|
recordPrewarmWaste = () => void 0;
|
|
30368
|
+
/**
|
|
30369
|
+
* Per-callId completion deferreds for ``Patter.call({ wait: true })``.
|
|
30370
|
+
* Resolved by the FIRST terminal signal: the Twilio/Telnyx status callback
|
|
30371
|
+
* for no-media outcomes (no-answer / busy / failed), or ``onCallEnd`` for a
|
|
30372
|
+
* connected call (answered / voicemail). The AMD classification is recorded
|
|
30373
|
+
* per callId so the connected-call path can distinguish ``answered`` from
|
|
30374
|
+
* ``voicemail``. This is what lets ``call({ wait: true })`` resolve to a
|
|
30375
|
+
* structured {@link CallResult} without the caller hand-wiring ``onCallEnd``
|
|
30376
|
+
* to a promise. Public so ``client.ts`` can register/await + fail in-flight
|
|
30377
|
+
* waiters on ``disconnect()``. Mirrors Python's ``EmbeddedServer._completions``.
|
|
30378
|
+
*/
|
|
30379
|
+
completions = /* @__PURE__ */ new Map();
|
|
30380
|
+
/** AMD classification recorded per callId, used by the connected-call path. */
|
|
30381
|
+
amdClass = /* @__PURE__ */ new Map();
|
|
30382
|
+
// === Outbound completion registry (call({ wait: true })) ===
|
|
30383
|
+
/**
|
|
30384
|
+
* Register (or return) a completion promise for an outbound call.
|
|
30385
|
+
*
|
|
30386
|
+
* Called by ``Patter.call({ wait: true })`` immediately after the carrier
|
|
30387
|
+
* accepts the dial — the promise resolves to a {@link CallResult} once a
|
|
30388
|
+
* terminal signal arrives. Idempotent: returns the existing pending promise
|
|
30389
|
+
* if one is already registered for ``callId``. Mirrors Python's
|
|
30390
|
+
* ``register_completion``.
|
|
30391
|
+
*/
|
|
30392
|
+
registerCompletion(callId) {
|
|
30393
|
+
const existing = this.completions.get(callId);
|
|
30394
|
+
if (existing && !existing.done) {
|
|
30395
|
+
return existing.promise;
|
|
30396
|
+
}
|
|
30397
|
+
let resolve2;
|
|
30398
|
+
let reject;
|
|
30399
|
+
const promise = new Promise((res, rej) => {
|
|
30400
|
+
resolve2 = res;
|
|
30401
|
+
reject = rej;
|
|
30402
|
+
});
|
|
30403
|
+
this.completions.set(callId, { promise, resolve: resolve2, reject, done: false });
|
|
30404
|
+
return promise;
|
|
30405
|
+
}
|
|
30406
|
+
/** Drop a registered completion (e.g. on a backstop timeout) without resolving it. */
|
|
30407
|
+
deleteCompletion(callId) {
|
|
30408
|
+
this.completions.delete(callId);
|
|
30409
|
+
this.amdClass.delete(callId);
|
|
30410
|
+
}
|
|
30411
|
+
/**
|
|
30412
|
+
* Resolve a pending completion with a {@link CallResult}.
|
|
30413
|
+
*
|
|
30414
|
+
* No-op when no completion is registered for ``callId`` (the common case —
|
|
30415
|
+
* most calls are placed without ``wait: true``) or it is already done.
|
|
30416
|
+
* Builds the result from the ``onCallEnd`` payload when ``data`` is provided
|
|
30417
|
+
* (connected calls carry transcript + {@link CallMetrics}); no-media
|
|
30418
|
+
* outcomes pass ``data`` undefined and yield an empty transcript / no cost.
|
|
30419
|
+
* Mirrors Python's ``_resolve_completion``.
|
|
30420
|
+
*/
|
|
30421
|
+
resolveCompletion(callId, args) {
|
|
30422
|
+
const entry = this.completions.get(callId);
|
|
30423
|
+
if (!entry || entry.done) return;
|
|
30424
|
+
const data = args.data;
|
|
30425
|
+
const metrics = data?.metrics ?? null;
|
|
30426
|
+
const cost = metrics?.cost ?? null;
|
|
30427
|
+
const durationRaw = metrics?.duration_seconds;
|
|
30428
|
+
const duration3 = typeof durationRaw === "number" ? durationRaw : 0;
|
|
30429
|
+
const transcriptRaw = data?.transcript;
|
|
30430
|
+
const transcript = Array.isArray(transcriptRaw) ? transcriptRaw : [];
|
|
30431
|
+
const result = {
|
|
30432
|
+
callId,
|
|
30433
|
+
outcome: args.outcome,
|
|
30434
|
+
status: args.status,
|
|
30435
|
+
durationSeconds: duration3,
|
|
30436
|
+
transcript,
|
|
30437
|
+
cost,
|
|
30438
|
+
metrics
|
|
30439
|
+
};
|
|
30440
|
+
entry.done = true;
|
|
30441
|
+
entry.resolve(result);
|
|
30442
|
+
this.completions.delete(callId);
|
|
30443
|
+
this.amdClass.delete(callId);
|
|
30444
|
+
}
|
|
30445
|
+
/**
|
|
30446
|
+
* Fail every in-flight completion with ``error``. Called by
|
|
30447
|
+
* ``Patter.disconnect()`` so a ``call({ wait: true })`` awaiter does not
|
|
30448
|
+
* hang until its backstop timeout once the server is gone. Mirrors the
|
|
30449
|
+
* Python ``disconnect()`` change that fails in-flight ``wait=True`` awaiters.
|
|
30450
|
+
*/
|
|
30451
|
+
failPendingCompletions(error2) {
|
|
30452
|
+
for (const entry of this.completions.values()) {
|
|
30453
|
+
if (!entry.done) {
|
|
30454
|
+
entry.done = true;
|
|
30455
|
+
entry.reject(error2);
|
|
30456
|
+
}
|
|
30457
|
+
}
|
|
30458
|
+
this.completions.clear();
|
|
30459
|
+
this.amdClass.clear();
|
|
30460
|
+
}
|
|
29970
30461
|
/** Bind HTTP + WebSocket listeners on `port`, mount carrier webhooks and dashboard routes. */
|
|
29971
30462
|
async start(port = 8e3) {
|
|
29972
30463
|
const webhookUrlPattern = /^[a-zA-Z0-9][a-zA-Z0-9.\-]+[a-zA-Z0-9]$/;
|
|
@@ -30030,8 +30521,10 @@ var init_server = __esm({
|
|
|
30030
30521
|
return;
|
|
30031
30522
|
}
|
|
30032
30523
|
const body = req.body;
|
|
30033
|
-
const
|
|
30034
|
-
const
|
|
30524
|
+
const rawCallSid = body["CallSid"] ?? "";
|
|
30525
|
+
const rawCallStatus = body["CallStatus"] ?? "";
|
|
30526
|
+
const callSid = sanitizeLogValue(rawCallSid);
|
|
30527
|
+
const callStatus = sanitizeLogValue(rawCallStatus);
|
|
30035
30528
|
const duration3 = body["CallDuration"] ?? body["Duration"] ?? "";
|
|
30036
30529
|
getLogger().info(
|
|
30037
30530
|
`Twilio status ${callStatus} for call ${callSid} (duration=${duration3})`
|
|
@@ -30048,6 +30541,10 @@ var init_server = __esm({
|
|
|
30048
30541
|
} catch (err) {
|
|
30049
30542
|
getLogger().debug(`recordPrewarmWaste threw: ${String(err)}`);
|
|
30050
30543
|
}
|
|
30544
|
+
this.resolveCompletion(rawCallSid, {
|
|
30545
|
+
outcome: twilioStatusToOutcome(rawCallStatus),
|
|
30546
|
+
status: rawCallStatus
|
|
30547
|
+
});
|
|
30051
30548
|
}
|
|
30052
30549
|
res.status(204).send();
|
|
30053
30550
|
});
|
|
@@ -30090,6 +30587,9 @@ var init_server = __esm({
|
|
|
30090
30587
|
const answeredBy = body["AnsweredBy"] ?? "";
|
|
30091
30588
|
const callSid = body["CallSid"] ?? "";
|
|
30092
30589
|
getLogger().info(`AMD result for ${sanitizeLogValue(callSid)}: ${sanitizeLogValue(answeredBy)}`);
|
|
30590
|
+
if (callSid) {
|
|
30591
|
+
this.amdClass.set(callSid, classifyTwilioAmd(answeredBy));
|
|
30592
|
+
}
|
|
30093
30593
|
const cb = this.onMachineDetection;
|
|
30094
30594
|
if (cb && callSid) {
|
|
30095
30595
|
try {
|
|
@@ -30215,6 +30715,9 @@ var init_server = __esm({
|
|
|
30215
30715
|
getLogger().info(
|
|
30216
30716
|
`Telnyx AMD result for ${sanitizeLogValue(amdCallId)}: ${sanitizeLogValue(amdResult)}`
|
|
30217
30717
|
);
|
|
30718
|
+
if (amdCallId) {
|
|
30719
|
+
this.amdClass.set(amdCallId, classifyTelnyxAmd(amdResult));
|
|
30720
|
+
}
|
|
30218
30721
|
const cbTx = this.onMachineDetection;
|
|
30219
30722
|
if (cbTx && amdCallId) {
|
|
30220
30723
|
try {
|
|
@@ -30251,6 +30754,13 @@ var init_server = __esm({
|
|
|
30251
30754
|
} catch (err) {
|
|
30252
30755
|
getLogger().debug(`recordPrewarmWaste threw: ${String(err)}`);
|
|
30253
30756
|
}
|
|
30757
|
+
const noMediaOutcome = telnyxHangupOutcome(hangupCause);
|
|
30758
|
+
if (noMediaOutcome !== null) {
|
|
30759
|
+
this.resolveCompletion(hangupCallId, {
|
|
30760
|
+
outcome: noMediaOutcome,
|
|
30761
|
+
status: hangupCause
|
|
30762
|
+
});
|
|
30763
|
+
}
|
|
30254
30764
|
}
|
|
30255
30765
|
return res.status(200).send();
|
|
30256
30766
|
}
|
|
@@ -30303,6 +30813,121 @@ var init_server = __esm({
|
|
|
30303
30813
|
}
|
|
30304
30814
|
return res.status(200).send();
|
|
30305
30815
|
});
|
|
30816
|
+
const validatePlivoRequest = (req, res) => {
|
|
30817
|
+
const authToken = this.config.plivoAuthToken;
|
|
30818
|
+
if (!authToken) {
|
|
30819
|
+
if (this.config.requireSignature !== false) {
|
|
30820
|
+
getLogger().error(
|
|
30821
|
+
"Plivo webhook rejected: plivoAuthToken not configured and requireSignature is not false"
|
|
30822
|
+
);
|
|
30823
|
+
res.status(503).send("Webhook signature required");
|
|
30824
|
+
return false;
|
|
30825
|
+
}
|
|
30826
|
+
return true;
|
|
30827
|
+
}
|
|
30828
|
+
const method = req.method.toUpperCase();
|
|
30829
|
+
const params = method === "POST" && req.body && typeof req.body === "object" ? Object.fromEntries(
|
|
30830
|
+
Object.entries(req.body).map(([k, v]) => [k, String(v)])
|
|
30831
|
+
) : {};
|
|
30832
|
+
const signature = req.headers["x-plivo-signature-v3"] || "";
|
|
30833
|
+
const nonce = req.headers["x-plivo-signature-v3-nonce"] || "";
|
|
30834
|
+
const url2 = `https://${this.config.webhookUrl}${req.originalUrl}`;
|
|
30835
|
+
if (!validatePlivoSignature(url2, nonce, signature, authToken, params, method)) {
|
|
30836
|
+
getLogger().warn("Plivo webhook rejected: invalid or missing V3 signature");
|
|
30837
|
+
res.status(403).send("Invalid signature");
|
|
30838
|
+
return false;
|
|
30839
|
+
}
|
|
30840
|
+
return true;
|
|
30841
|
+
};
|
|
30842
|
+
app.post("/webhooks/plivo/voice", (req, res) => {
|
|
30843
|
+
if (!validatePlivoRequest(req, res)) return;
|
|
30844
|
+
const body = req.body ?? {};
|
|
30845
|
+
const callUuid = body["CallUUID"] ?? "";
|
|
30846
|
+
const caller = body["From"] ?? "";
|
|
30847
|
+
const callee = body["To"] ?? "";
|
|
30848
|
+
const qs = `?caller=${encodeURIComponent(caller)}&callee=${encodeURIComponent(callee)}`;
|
|
30849
|
+
const streamUrl = `wss://${this.config.webhookUrl}/ws/plivo/stream/${callUuid || "outbound"}${qs}`;
|
|
30850
|
+
const xml = PlivoAdapter.generateStreamXml(streamUrl, "audio/x-mulaw;rate=8000", {
|
|
30851
|
+
"X-PH-caller": caller,
|
|
30852
|
+
"X-PH-callee": callee
|
|
30853
|
+
});
|
|
30854
|
+
res.type("text/xml").send(xml);
|
|
30855
|
+
});
|
|
30856
|
+
app.post("/webhooks/plivo/status", (req, res) => {
|
|
30857
|
+
if (!validatePlivoRequest(req, res)) return;
|
|
30858
|
+
const body = req.body ?? {};
|
|
30859
|
+
const callUuid = body["CallUUID"] ?? "";
|
|
30860
|
+
const callStatus = body["CallStatus"] ?? body["Status"] ?? "";
|
|
30861
|
+
const duration3 = body["Duration"] ?? body["BillDuration"] ?? "";
|
|
30862
|
+
getLogger().info(
|
|
30863
|
+
`Plivo status ${sanitizeLogValue(callStatus)} for call ${sanitizeLogValue(callUuid)} (duration=${duration3})`
|
|
30864
|
+
);
|
|
30865
|
+
if (callUuid && callStatus) {
|
|
30866
|
+
const extra = {};
|
|
30867
|
+
const parsed = parseFloat(duration3);
|
|
30868
|
+
if (!Number.isNaN(parsed)) extra.duration_seconds = parsed;
|
|
30869
|
+
this.metricsStore.updateCallStatus(callUuid, callStatus, extra);
|
|
30870
|
+
}
|
|
30871
|
+
if (callUuid && ["no-answer", "busy", "failed", "timeout", "cancel"].includes(callStatus)) {
|
|
30872
|
+
try {
|
|
30873
|
+
this.recordPrewarmWaste(callUuid);
|
|
30874
|
+
} catch (err) {
|
|
30875
|
+
getLogger().debug(`recordPrewarmWaste threw: ${String(err)}`);
|
|
30876
|
+
}
|
|
30877
|
+
const outcome = callStatus === "no-answer" || callStatus === "timeout" ? "no_answer" : callStatus === "busy" ? "busy" : "failed";
|
|
30878
|
+
this.resolveCompletion(callUuid, { outcome, status: callStatus });
|
|
30879
|
+
}
|
|
30880
|
+
res.status(200).send();
|
|
30881
|
+
});
|
|
30882
|
+
app.post("/webhooks/plivo/amd", async (req, res) => {
|
|
30883
|
+
if (!validatePlivoRequest(req, res)) return;
|
|
30884
|
+
const body = req.body ?? {};
|
|
30885
|
+
const callUuid = body["CallUUID"] ?? "";
|
|
30886
|
+
const amdRaw = body["Machine"] || body["MachineDetection"] || body["AnsweredBy"] || body["CallStatus"] || "";
|
|
30887
|
+
getLogger().info(`AMD result for ${sanitizeLogValue(callUuid)}: ${sanitizeLogValue(amdRaw)}`);
|
|
30888
|
+
const classification = classifyPlivoAmd(amdRaw);
|
|
30889
|
+
if (callUuid) this.amdClass.set(callUuid, classification);
|
|
30890
|
+
const cb = this.onMachineDetection;
|
|
30891
|
+
if (cb && callUuid) {
|
|
30892
|
+
try {
|
|
30893
|
+
await cb({
|
|
30894
|
+
call_id: callUuid,
|
|
30895
|
+
carrier: "plivo",
|
|
30896
|
+
classification,
|
|
30897
|
+
raw: amdRaw,
|
|
30898
|
+
detected_at: Date.now() / 1e3
|
|
30899
|
+
});
|
|
30900
|
+
} catch (err) {
|
|
30901
|
+
getLogger().warn(`onMachineDetection callback threw: ${sanitizeLogValue(String(err))}`);
|
|
30902
|
+
}
|
|
30903
|
+
}
|
|
30904
|
+
if (classification === "machine" && callUuid) {
|
|
30905
|
+
try {
|
|
30906
|
+
this.recordPrewarmWaste(callUuid);
|
|
30907
|
+
} catch (err) {
|
|
30908
|
+
getLogger().debug(`recordPrewarmWaste threw: ${String(err)}`);
|
|
30909
|
+
}
|
|
30910
|
+
if (this.voicemailMessage && this.config.plivoAuthId && this.config.plivoAuthToken) {
|
|
30911
|
+
await dropPlivoVoicemail(
|
|
30912
|
+
callUuid,
|
|
30913
|
+
this.voicemailMessage,
|
|
30914
|
+
this.config.plivoAuthId,
|
|
30915
|
+
this.config.plivoAuthToken
|
|
30916
|
+
);
|
|
30917
|
+
}
|
|
30918
|
+
}
|
|
30919
|
+
res.status(200).send();
|
|
30920
|
+
});
|
|
30921
|
+
app.all("/webhooks/plivo/transfer", (req, res) => {
|
|
30922
|
+
if (!validatePlivoRequest(req, res)) return;
|
|
30923
|
+
const to = String(req.query.to ?? "");
|
|
30924
|
+
if (!to || !/^\+[1-9]\d{6,14}$/.test(to)) {
|
|
30925
|
+
getLogger().warn(`Plivo transfer XML: invalid target ${JSON.stringify(to)}`);
|
|
30926
|
+
res.type("text/xml").send("<Response><Hangup/></Response>");
|
|
30927
|
+
return;
|
|
30928
|
+
}
|
|
30929
|
+
res.type("text/xml").send(`<Response><Dial><Number>${xmlEscape(to)}</Number></Dial></Response>`);
|
|
30930
|
+
});
|
|
30306
30931
|
this.server = (0, import_http.createServer)(app);
|
|
30307
30932
|
this.wss = new import_ws5.WebSocketServer({ noServer: true });
|
|
30308
30933
|
const MAX_WS_PER_IP = 10;
|
|
@@ -30335,9 +30960,11 @@ var init_server = __esm({
|
|
|
30335
30960
|
ws.once("close", () => {
|
|
30336
30961
|
this.activeConnections.delete(ws);
|
|
30337
30962
|
});
|
|
30338
|
-
const
|
|
30339
|
-
if (
|
|
30963
|
+
const provider2 = this.config.telephonyProvider;
|
|
30964
|
+
if (provider2 === "telnyx") {
|
|
30340
30965
|
this.handleTelnyxStream(ws, url2);
|
|
30966
|
+
} else if (provider2 === "plivo") {
|
|
30967
|
+
this.handlePlivoStream(ws, url2);
|
|
30341
30968
|
} else {
|
|
30342
30969
|
this.handleTwilioStream(ws, url2);
|
|
30343
30970
|
}
|
|
@@ -30521,6 +31148,12 @@ var init_server = __esm({
|
|
|
30521
31148
|
}).catch((err) => getLogger().error(`call_log end error: ${String(err)}`));
|
|
30522
31149
|
}
|
|
30523
31150
|
if (userEnd) await userEnd(data);
|
|
31151
|
+
const cid = typeof data.call_id === "string" ? data.call_id : "";
|
|
31152
|
+
if (cid) {
|
|
31153
|
+
const cls = this.amdClass.get(cid);
|
|
31154
|
+
const outcome = cls === "machine" ? "voicemail" : "answered";
|
|
31155
|
+
this.resolveCompletion(cid, { outcome, status: "completed", data });
|
|
31156
|
+
}
|
|
30524
31157
|
};
|
|
30525
31158
|
return [wrappedStart, wrappedMetrics, wrappedEnd];
|
|
30526
31159
|
}
|
|
@@ -30627,6 +31260,52 @@ var init_server = __esm({
|
|
|
30627
31260
|
});
|
|
30628
31261
|
}
|
|
30629
31262
|
// ---------------------------------------------------------------------------
|
|
31263
|
+
// Plivo WebSocket message parser (thin layer)
|
|
31264
|
+
// ---------------------------------------------------------------------------
|
|
31265
|
+
handlePlivoStream(ws, url2) {
|
|
31266
|
+
const caller = url2.searchParams.get("caller") ?? "";
|
|
31267
|
+
const callee = url2.searchParams.get("callee") ?? "";
|
|
31268
|
+
const bridge = new PlivoBridge(this.config);
|
|
31269
|
+
const handler = new StreamHandler(this.buildStreamHandlerDeps(bridge), ws, caller, callee);
|
|
31270
|
+
ws.on("message", async (raw) => {
|
|
31271
|
+
try {
|
|
31272
|
+
let data;
|
|
31273
|
+
try {
|
|
31274
|
+
data = JSON.parse(raw.toString());
|
|
31275
|
+
} catch (e) {
|
|
31276
|
+
getLogger().error("Failed to parse Plivo WS message:", e);
|
|
31277
|
+
return;
|
|
31278
|
+
}
|
|
31279
|
+
const event = data.event ?? "";
|
|
31280
|
+
if (event === "start") {
|
|
31281
|
+
handler.setStreamSid(data.start?.streamId ?? "");
|
|
31282
|
+
const callId = data.start?.callId ?? "";
|
|
31283
|
+
if (callId) this.activeCallIds.set(ws, callId);
|
|
31284
|
+
await handler.handleCallStart(callId);
|
|
31285
|
+
} else if (event === "media") {
|
|
31286
|
+
const payload = data.media?.payload ?? "";
|
|
31287
|
+
if (payload) handler.handleAudio(Buffer.from(payload, "base64"));
|
|
31288
|
+
} else if (event === "playedStream") {
|
|
31289
|
+
const markName = String(data.name ?? "");
|
|
31290
|
+
if (markName) await handler.onMark(markName);
|
|
31291
|
+
} else if (event === "dtmf") {
|
|
31292
|
+
const digit = String(data.dtmf?.digit ?? "").trim();
|
|
31293
|
+
if (digit) await handler.handleDtmf(digit);
|
|
31294
|
+
} else if (event === "playFailed" || event === "error") {
|
|
31295
|
+
getLogger().warn(`Plivo ${event}: ${data.reason ?? "unknown"}`);
|
|
31296
|
+
} else if (event === "stop") {
|
|
31297
|
+
await handler.handleStop();
|
|
31298
|
+
}
|
|
31299
|
+
} catch (err) {
|
|
31300
|
+
getLogger().error("Stream handler error (Plivo):", err);
|
|
31301
|
+
}
|
|
31302
|
+
});
|
|
31303
|
+
ws.on("close", async () => {
|
|
31304
|
+
this.activeCallIds.delete(ws);
|
|
31305
|
+
await handler.handleWsClose();
|
|
31306
|
+
});
|
|
31307
|
+
}
|
|
31308
|
+
// ---------------------------------------------------------------------------
|
|
30630
31309
|
// Graceful shutdown
|
|
30631
31310
|
// ---------------------------------------------------------------------------
|
|
30632
31311
|
/**
|
|
@@ -30643,10 +31322,10 @@ var init_server = __esm({
|
|
|
30643
31322
|
const httpClosePromise = new Promise((resolve2) => {
|
|
30644
31323
|
this.server.close(() => resolve2());
|
|
30645
31324
|
});
|
|
30646
|
-
const
|
|
31325
|
+
const provider2 = this.config.telephonyProvider;
|
|
30647
31326
|
for (const [ws, callId] of this.activeCallIds) {
|
|
30648
31327
|
try {
|
|
30649
|
-
const bridge =
|
|
31328
|
+
const bridge = provider2 === "telnyx" ? new TelnyxBridge(this.config) : provider2 === "plivo" ? new PlivoBridge(this.config) : new TwilioBridge(this.config);
|
|
30650
31329
|
await bridge.endCall(callId, ws);
|
|
30651
31330
|
} catch {
|
|
30652
31331
|
}
|
|
@@ -30795,6 +31474,7 @@ var init_tunnel = __esm({
|
|
|
30795
31474
|
var carrier_config_exports = {};
|
|
30796
31475
|
__export(carrier_config_exports, {
|
|
30797
31476
|
autoConfigureCarrier: () => autoConfigureCarrier,
|
|
31477
|
+
configurePlivoNumber: () => configurePlivoNumber,
|
|
30798
31478
|
configureTelnyxNumber: () => configureTelnyxNumber,
|
|
30799
31479
|
configureTwilioNumber: () => configureTwilioNumber
|
|
30800
31480
|
});
|
|
@@ -30846,9 +31526,38 @@ async function configureTelnyxNumber(apiKey, connectionId, phoneNumber) {
|
|
|
30846
31526
|
);
|
|
30847
31527
|
}
|
|
30848
31528
|
}
|
|
31529
|
+
async function configurePlivoNumber(authId, authToken, phoneNumber, answerUrl) {
|
|
31530
|
+
const auth2 = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
|
|
31531
|
+
const base = `${PLIVO_API_BASE2}/Account/${encodeURIComponent(authId)}`;
|
|
31532
|
+
const appResp = await fetch(`${base}/Application/`, {
|
|
31533
|
+
method: "POST",
|
|
31534
|
+
headers: { Authorization: auth2, "Content-Type": "application/json" },
|
|
31535
|
+
body: JSON.stringify({
|
|
31536
|
+
app_name: "patter-inbound",
|
|
31537
|
+
answer_url: answerUrl,
|
|
31538
|
+
answer_method: "POST"
|
|
31539
|
+
})
|
|
31540
|
+
});
|
|
31541
|
+
if (!appResp.ok) {
|
|
31542
|
+
throw new Error(`Plivo Application create failed: ${appResp.status} ${await appResp.text()}`);
|
|
31543
|
+
}
|
|
31544
|
+
const appBody = await appResp.json();
|
|
31545
|
+
if (!appBody.app_id) {
|
|
31546
|
+
getLogger().warn("Plivo Application create returned no app_id");
|
|
31547
|
+
return;
|
|
31548
|
+
}
|
|
31549
|
+
const linkResp = await fetch(`${base}/Number/${encodeURIComponent(phoneNumber)}/`, {
|
|
31550
|
+
method: "POST",
|
|
31551
|
+
headers: { Authorization: auth2, "Content-Type": "application/json" },
|
|
31552
|
+
body: JSON.stringify({ app_id: appBody.app_id })
|
|
31553
|
+
});
|
|
31554
|
+
if (!linkResp.ok) {
|
|
31555
|
+
throw new Error(`Plivo Number update failed: ${linkResp.status} ${await linkResp.text()}`);
|
|
31556
|
+
}
|
|
31557
|
+
}
|
|
30849
31558
|
async function autoConfigureCarrier(params) {
|
|
30850
31559
|
const log3 = getLogger();
|
|
30851
|
-
const provider2 = params.telephonyProvider ?? (params.twilioSid ? "twilio" : "telnyx");
|
|
31560
|
+
const provider2 = params.telephonyProvider ?? (params.twilioSid ? "twilio" : params.plivoAuthId ? "plivo" : "telnyx");
|
|
30852
31561
|
if (provider2 === "twilio" && params.twilioSid && params.twilioToken) {
|
|
30853
31562
|
const voiceUrl = `https://${params.webhookHost}/webhooks/twilio/voice`;
|
|
30854
31563
|
try {
|
|
@@ -30867,9 +31576,20 @@ async function autoConfigureCarrier(params) {
|
|
|
30867
31576
|
} catch (err) {
|
|
30868
31577
|
log3.warn("Could not auto-configure Telnyx number: %s", err instanceof Error ? err.message : String(err));
|
|
30869
31578
|
}
|
|
31579
|
+
return;
|
|
31580
|
+
}
|
|
31581
|
+
if (provider2 === "plivo" && params.plivoAuthId && params.plivoAuthToken) {
|
|
31582
|
+
const answerUrl = `https://${params.webhookHost}/webhooks/plivo/voice`;
|
|
31583
|
+
try {
|
|
31584
|
+
await configurePlivoNumber(params.plivoAuthId, params.plivoAuthToken, params.phoneNumber, answerUrl);
|
|
31585
|
+
log3.info("Plivo answer URL set to %s", answerUrl);
|
|
31586
|
+
} catch (err) {
|
|
31587
|
+
log3.warn("Could not auto-configure Plivo answer URL: %s", err instanceof Error ? err.message : String(err));
|
|
31588
|
+
log3.info("Set the Plivo application answer URL manually to: %s", answerUrl);
|
|
31589
|
+
}
|
|
30870
31590
|
}
|
|
30871
31591
|
}
|
|
30872
|
-
var TWILIO_API_BASE, TELNYX_API_BASE;
|
|
31592
|
+
var TWILIO_API_BASE, TELNYX_API_BASE, PLIVO_API_BASE2;
|
|
30873
31593
|
var init_carrier_config = __esm({
|
|
30874
31594
|
"src/carrier-config.ts"() {
|
|
30875
31595
|
"use strict";
|
|
@@ -30877,6 +31597,7 @@ var init_carrier_config = __esm({
|
|
|
30877
31597
|
init_logger();
|
|
30878
31598
|
TWILIO_API_BASE = "https://api.twilio.com/2010-04-01";
|
|
30879
31599
|
TELNYX_API_BASE = "https://api.telnyx.com/v2";
|
|
31600
|
+
PLIVO_API_BASE2 = "https://api.plivo.com/v1";
|
|
30880
31601
|
}
|
|
30881
31602
|
});
|
|
30882
31603
|
|
|
@@ -32500,6 +33221,8 @@ __export(index_exports, {
|
|
|
32500
33221
|
PatterTool: () => PatterTool,
|
|
32501
33222
|
PcmCarry: () => PcmCarry,
|
|
32502
33223
|
PipelineHookExecutor: () => PipelineHookExecutor,
|
|
33224
|
+
Plivo: () => Carrier,
|
|
33225
|
+
PlivoAdapter: () => PlivoAdapter,
|
|
32503
33226
|
PricingUnit: () => PricingUnit,
|
|
32504
33227
|
ProvisionError: () => ProvisionError,
|
|
32505
33228
|
RateLimitError: () => RateLimitError,
|
|
@@ -32526,7 +33249,7 @@ __export(index_exports, {
|
|
|
32526
33249
|
SpeechmaticsTurnDetectionMode: () => TurnDetectionMode,
|
|
32527
33250
|
StatefulResampler: () => StatefulResampler,
|
|
32528
33251
|
StaticTunnel: () => Static,
|
|
32529
|
-
Telnyx: () =>
|
|
33252
|
+
Telnyx: () => Carrier3,
|
|
32530
33253
|
TelnyxAdapter: () => TelnyxAdapter,
|
|
32531
33254
|
TelnyxSTT: () => TelnyxSTT,
|
|
32532
33255
|
TelnyxSTTInputFormat: () => TelnyxSTTInputFormat,
|
|
@@ -32537,7 +33260,7 @@ __export(index_exports, {
|
|
|
32537
33260
|
TestSession: () => TestSession,
|
|
32538
33261
|
TfidfLoopDetector: () => TfidfLoopDetector,
|
|
32539
33262
|
Tool: () => Tool,
|
|
32540
|
-
Twilio: () =>
|
|
33263
|
+
Twilio: () => Carrier2,
|
|
32541
33264
|
TwilioAdapter: () => TwilioAdapter,
|
|
32542
33265
|
ULTRAVOX_DEFAULT_API_BASE: () => ULTRAVOX_DEFAULT_API_BASE,
|
|
32543
33266
|
ULTRAVOX_DEFAULT_SR: () => ULTRAVOX_DEFAULT_SR,
|
|
@@ -33314,7 +34037,7 @@ var Patter = class {
|
|
|
33314
34037
|
}
|
|
33315
34038
|
if (!options.carrier) {
|
|
33316
34039
|
throw new Error(
|
|
33317
|
-
"Local mode requires a `carrier` instance. Pass `carrier: new Twilio({...})` or `carrier: new
|
|
34040
|
+
"Local mode requires a `carrier` instance. Pass `carrier: new Twilio({...})`, `carrier: new Telnyx({...})` or `carrier: new Plivo({...})`."
|
|
33318
34041
|
);
|
|
33319
34042
|
}
|
|
33320
34043
|
const carrier = options.carrier;
|
|
@@ -33492,7 +34215,7 @@ var Patter = class {
|
|
|
33492
34215
|
throw err;
|
|
33493
34216
|
}
|
|
33494
34217
|
const carrier = this.localConfig.carrier;
|
|
33495
|
-
const telephonyProvider = carrier.kind
|
|
34218
|
+
const telephonyProvider = carrier.kind;
|
|
33496
34219
|
const wantsCarrierManagement = opts.manageWebhook !== false || wantsCloudflared;
|
|
33497
34220
|
if (wantsCarrierManagement) {
|
|
33498
34221
|
const { autoConfigureCarrier: autoConfigureCarrier2 } = await Promise.resolve().then(() => (init_carrier_config(), carrier_config_exports));
|
|
@@ -33502,6 +34225,8 @@ var Patter = class {
|
|
|
33502
34225
|
twilioToken: carrier.kind === "twilio" ? carrier.authToken : void 0,
|
|
33503
34226
|
telnyxKey: carrier.kind === "telnyx" ? carrier.apiKey : void 0,
|
|
33504
34227
|
telnyxConnectionId: carrier.kind === "telnyx" ? carrier.connectionId : void 0,
|
|
34228
|
+
plivoAuthId: carrier.kind === "plivo" ? carrier.authId : void 0,
|
|
34229
|
+
plivoAuthToken: carrier.kind === "plivo" ? carrier.authToken : void 0,
|
|
33505
34230
|
phoneNumber: this.localConfig.phoneNumber,
|
|
33506
34231
|
webhookHost: webhookUrl
|
|
33507
34232
|
});
|
|
@@ -33517,6 +34242,8 @@ var Patter = class {
|
|
|
33517
34242
|
telnyxKey: carrier.kind === "telnyx" ? carrier.apiKey : void 0,
|
|
33518
34243
|
telnyxConnectionId: carrier.kind === "telnyx" ? carrier.connectionId : void 0,
|
|
33519
34244
|
telnyxPublicKey: carrier.kind === "telnyx" ? carrier.publicKey : void 0,
|
|
34245
|
+
plivoAuthId: carrier.kind === "plivo" ? carrier.authId : void 0,
|
|
34246
|
+
plivoAuthToken: carrier.kind === "plivo" ? carrier.authToken : void 0,
|
|
33520
34247
|
persistRoot: this.localConfig.persistRoot
|
|
33521
34248
|
},
|
|
33522
34249
|
opts.agent,
|
|
@@ -33928,7 +34655,15 @@ var Patter = class {
|
|
|
33928
34655
|
this.prewarmTtlTimers.set(callId, handle);
|
|
33929
34656
|
});
|
|
33930
34657
|
}
|
|
33931
|
-
/**
|
|
34658
|
+
/**
|
|
34659
|
+
* Place an outbound call via the configured carrier.
|
|
34660
|
+
*
|
|
34661
|
+
* With `wait: false` (default) this resolves to `void` the instant the
|
|
34662
|
+
* carrier accepts the dial (fire-and-forget). With `wait: true` it blocks
|
|
34663
|
+
* until the call reaches a terminal state and resolves to a
|
|
34664
|
+
* {@link CallResult} — see {@link LocalCallOptions.wait}. Mirrors Python's
|
|
34665
|
+
* `Patter.call(..., wait=False)`.
|
|
34666
|
+
*/
|
|
33932
34667
|
async call(options) {
|
|
33933
34668
|
if (!options.to) {
|
|
33934
34669
|
throw new Error("'to' phone number is required");
|
|
@@ -33936,7 +34671,13 @@ var Patter = class {
|
|
|
33936
34671
|
if (!options.to.startsWith("+")) {
|
|
33937
34672
|
throw new Error(`'to' must be in E.164 format (e.g., '+1234567890'). Got: '${options.to}'`);
|
|
33938
34673
|
}
|
|
34674
|
+
if (options.wait && !this.embeddedServer) {
|
|
34675
|
+
throw new PatterConnectionError(
|
|
34676
|
+
"call({ wait: true }) requires an active server to receive the carrier completion webhooks. Call `await phone.serve(...)` first, or use `await using phone = new Patter(...)` (and serve inside the block) which keeps the server up for the duration of the block."
|
|
34677
|
+
);
|
|
34678
|
+
}
|
|
33939
34679
|
const { phoneNumber, webhookUrl, carrier } = this.localConfig;
|
|
34680
|
+
let callId = "";
|
|
33940
34681
|
const effectiveRingTimeout = options.ringTimeout === void 0 ? 25 : options.ringTimeout;
|
|
33941
34682
|
const wantsAmd = options.machineDetection !== false || Boolean(options.voicemailMessage);
|
|
33942
34683
|
if (this.embeddedServer) {
|
|
@@ -33994,11 +34735,74 @@ var Patter = class {
|
|
|
33994
34735
|
}
|
|
33995
34736
|
}
|
|
33996
34737
|
if (telnyxCallId) {
|
|
34738
|
+
callId = telnyxCallId;
|
|
33997
34739
|
this.spawnPrewarmFirstMessage(options.agent, telnyxCallId, effectiveRingTimeout, "telnyx");
|
|
33998
34740
|
if (options.agent.prewarm !== false) {
|
|
33999
34741
|
this.parkProviderConnections(options.agent, telnyxCallId);
|
|
34000
34742
|
}
|
|
34001
34743
|
}
|
|
34744
|
+
return this.maybeAwaitCompletion(options, callId, effectiveRingTimeout);
|
|
34745
|
+
}
|
|
34746
|
+
if (carrier.kind === "plivo") {
|
|
34747
|
+
const auth2 = `Basic ${Buffer.from(`${carrier.authId}:${carrier.authToken}`).toString("base64")}`;
|
|
34748
|
+
const plivoPayload = {
|
|
34749
|
+
from: phoneNumber,
|
|
34750
|
+
to: options.to,
|
|
34751
|
+
answer_url: `https://${webhookUrl}/webhooks/plivo/voice`,
|
|
34752
|
+
answer_method: "POST",
|
|
34753
|
+
// hangup_url is Plivo's StatusCallback analogue — without it the
|
|
34754
|
+
// /webhooks/plivo/status route never fires for outbound calls and
|
|
34755
|
+
// the dashboard misses no-answer / busy / failed.
|
|
34756
|
+
hangup_url: `https://${webhookUrl}/webhooks/plivo/status`,
|
|
34757
|
+
hangup_method: "POST"
|
|
34758
|
+
};
|
|
34759
|
+
if (effectiveRingTimeout !== null && effectiveRingTimeout !== void 0) {
|
|
34760
|
+
plivoPayload.ring_timeout = Math.max(1, Math.floor(effectiveRingTimeout));
|
|
34761
|
+
}
|
|
34762
|
+
if (wantsAmd) {
|
|
34763
|
+
plivoPayload.machine_detection = "true";
|
|
34764
|
+
plivoPayload.machine_detection_time = 5e3;
|
|
34765
|
+
plivoPayload.machine_detection_url = `https://${webhookUrl}/webhooks/plivo/amd`;
|
|
34766
|
+
plivoPayload.machine_detection_method = "POST";
|
|
34767
|
+
}
|
|
34768
|
+
if (options.voicemailMessage && this.embeddedServer) {
|
|
34769
|
+
this.embeddedServer.voicemailMessage = options.voicemailMessage;
|
|
34770
|
+
}
|
|
34771
|
+
const response2 = await fetch(`https://api.plivo.com/v1/Account/${carrier.authId}/Call/`, {
|
|
34772
|
+
method: "POST",
|
|
34773
|
+
headers: { "Content-Type": "application/json", Authorization: auth2 },
|
|
34774
|
+
body: JSON.stringify(plivoPayload)
|
|
34775
|
+
});
|
|
34776
|
+
if (!response2.ok) {
|
|
34777
|
+
throw new ProvisionError(`Failed to initiate Plivo call: ${await response2.text()}`);
|
|
34778
|
+
}
|
|
34779
|
+
let plivoCallId;
|
|
34780
|
+
try {
|
|
34781
|
+
const body = await response2.clone().json();
|
|
34782
|
+
plivoCallId = body.request_uuid;
|
|
34783
|
+
} catch {
|
|
34784
|
+
}
|
|
34785
|
+
if (plivoCallId) {
|
|
34786
|
+
const initiatedPayload = {
|
|
34787
|
+
call_id: plivoCallId,
|
|
34788
|
+
caller: phoneNumber,
|
|
34789
|
+
callee: options.to,
|
|
34790
|
+
direction: "outbound",
|
|
34791
|
+
status: "initiated"
|
|
34792
|
+
};
|
|
34793
|
+
if (this.embeddedServer) {
|
|
34794
|
+
this.embeddedServer.metricsStore.recordCallInitiated(initiatedPayload);
|
|
34795
|
+
}
|
|
34796
|
+
try {
|
|
34797
|
+
const { notifyDashboard: notifyDashboard2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
|
|
34798
|
+
notifyDashboard2(initiatedPayload);
|
|
34799
|
+
} catch {
|
|
34800
|
+
}
|
|
34801
|
+
this.spawnPrewarmFirstMessage(options.agent, plivoCallId, effectiveRingTimeout, "plivo");
|
|
34802
|
+
if (options.agent.prewarm !== false) {
|
|
34803
|
+
this.parkProviderConnections(options.agent, plivoCallId);
|
|
34804
|
+
}
|
|
34805
|
+
}
|
|
34002
34806
|
return;
|
|
34003
34807
|
}
|
|
34004
34808
|
const twilioSid = carrier.accountSid;
|
|
@@ -34070,11 +34874,53 @@ var Patter = class {
|
|
|
34070
34874
|
}
|
|
34071
34875
|
}
|
|
34072
34876
|
if (twilioCallSid) {
|
|
34877
|
+
callId = twilioCallSid;
|
|
34073
34878
|
this.spawnPrewarmFirstMessage(options.agent, twilioCallSid, effectiveRingTimeout, "twilio");
|
|
34074
34879
|
if (options.agent.prewarm !== false) {
|
|
34075
34880
|
this.parkProviderConnections(options.agent, twilioCallSid);
|
|
34076
34881
|
}
|
|
34077
34882
|
}
|
|
34883
|
+
return this.maybeAwaitCompletion(options, callId, effectiveRingTimeout);
|
|
34884
|
+
}
|
|
34885
|
+
/**
|
|
34886
|
+
* When `options.wait` is set, register a completion promise keyed by the
|
|
34887
|
+
* carrier-issued `callId` and await it (bounded by a backstop timeout).
|
|
34888
|
+
* Otherwise resolve to `void` immediately (fire-and-forget).
|
|
34889
|
+
*
|
|
34890
|
+
* The registration happens here — after the carrier accepted the dial and
|
|
34891
|
+
* issued the id — so the future correlates to the right call. The race
|
|
34892
|
+
* window between `initiateCall` returning and this registration is
|
|
34893
|
+
* harmless: the callee is still ringing, so no terminal signal can fire
|
|
34894
|
+
* before we register. Mirrors the Python `call(wait=True)` tail block.
|
|
34895
|
+
*/
|
|
34896
|
+
async maybeAwaitCompletion(options, callId, ringTimeout) {
|
|
34897
|
+
if (!options.wait) return;
|
|
34898
|
+
const server = this.embeddedServer;
|
|
34899
|
+
if (!server || !callId) {
|
|
34900
|
+
throw new PatterConnectionError(
|
|
34901
|
+
"call({ wait: true }): no active server or carrier call id."
|
|
34902
|
+
);
|
|
34903
|
+
}
|
|
34904
|
+
const completion = server.registerCompletion(callId);
|
|
34905
|
+
const backstopMs = ((ringTimeout ?? 25) + 1800) * 1e3;
|
|
34906
|
+
let timer;
|
|
34907
|
+
const backstop = new Promise((_resolve, reject) => {
|
|
34908
|
+
timer = setTimeout(() => {
|
|
34909
|
+
server.deleteCompletion(callId);
|
|
34910
|
+
reject(
|
|
34911
|
+
new PatterConnectionError(
|
|
34912
|
+
`call({ wait: true }): no terminal signal for call ${callId} within ${(backstopMs / 1e3).toFixed(0)}s`,
|
|
34913
|
+
{ code: ErrorCode.TIMEOUT }
|
|
34914
|
+
)
|
|
34915
|
+
);
|
|
34916
|
+
}, backstopMs);
|
|
34917
|
+
timer.unref?.();
|
|
34918
|
+
});
|
|
34919
|
+
try {
|
|
34920
|
+
return await Promise.race([completion, backstop]);
|
|
34921
|
+
} finally {
|
|
34922
|
+
if (timer) clearTimeout(timer);
|
|
34923
|
+
}
|
|
34078
34924
|
}
|
|
34079
34925
|
/**
|
|
34080
34926
|
* Stop the embedded server and any running tunnel. Safe to call multiple
|
|
@@ -34115,6 +34961,11 @@ var Patter = class {
|
|
|
34115
34961
|
this.tunnelHandle = null;
|
|
34116
34962
|
}
|
|
34117
34963
|
if (this.embeddedServer) {
|
|
34964
|
+
this.embeddedServer.failPendingCompletions(
|
|
34965
|
+
new PatterConnectionError(
|
|
34966
|
+
"Patter.disconnect() called while a call({ wait: true }) was still in flight."
|
|
34967
|
+
)
|
|
34968
|
+
);
|
|
34118
34969
|
await this.embeddedServer.stop();
|
|
34119
34970
|
this.embeddedServer = null;
|
|
34120
34971
|
}
|
|
@@ -34138,6 +34989,30 @@ var Patter = class {
|
|
|
34138
34989
|
this._ready.catch(() => {
|
|
34139
34990
|
});
|
|
34140
34991
|
}
|
|
34992
|
+
/**
|
|
34993
|
+
* Explicit-resource-management disposer so callers can write
|
|
34994
|
+
* ``await using phone = new Patter(...)`` and have {@link disconnect} run
|
|
34995
|
+
* automatically when the block exits — on the normal path AND when the
|
|
34996
|
+
* body throws. This guarantees the embedded server, any auto-started
|
|
34997
|
+
* tunnel, and in-flight prewarm/TTS work are torn down so a still-running
|
|
34998
|
+
* TTS WebSocket cannot keep the user billed after the block ends, and any
|
|
34999
|
+
* in-flight ``call({ wait: true })`` awaiter is failed rather than left
|
|
35000
|
+
* hanging. ``disconnect()`` is idempotent, so an explicit ``disconnect()``
|
|
35001
|
+
* inside the block is still safe. Mirrors Python's ``async with Patter(...)``.
|
|
35002
|
+
*
|
|
35003
|
+
* Note: this does NOT start the server (``serve()`` blocks until shutdown,
|
|
35004
|
+
* so it cannot run from a disposer) — call ``serve(...)`` inside the block:
|
|
35005
|
+
*
|
|
35006
|
+
* ```ts
|
|
35007
|
+
* await using phone = new Patter({ carrier: new Twilio(), phoneNumber: "+1555..." });
|
|
35008
|
+
* await phone.serve({ agent }); // inbound, or
|
|
35009
|
+
* const result = await phone.call({ to: "+1555...", agent, wait: true });
|
|
35010
|
+
* // disconnect() has run here — nothing left running.
|
|
35011
|
+
* ```
|
|
35012
|
+
*/
|
|
35013
|
+
async [Symbol.asyncDispose]() {
|
|
35014
|
+
await this.disconnect();
|
|
35015
|
+
}
|
|
34141
35016
|
/**
|
|
34142
35017
|
* Terminate an active call on the configured carrier.
|
|
34143
35018
|
*
|
|
@@ -34192,6 +35067,17 @@ var Patter = class {
|
|
|
34192
35067
|
}
|
|
34193
35068
|
return;
|
|
34194
35069
|
}
|
|
35070
|
+
if (carrier.kind === "plivo") {
|
|
35071
|
+
const auth2 = Buffer.from(`${carrier.authId}:${carrier.authToken}`).toString("base64");
|
|
35072
|
+
const res = await fetch(
|
|
35073
|
+
`https://api.plivo.com/v1/Account/${carrier.authId}/Call/${encodeURIComponent(callSid)}/`,
|
|
35074
|
+
{ method: "DELETE", headers: { Authorization: `Basic ${auth2}` } }
|
|
35075
|
+
);
|
|
35076
|
+
if (!res.ok && res.status !== 404) {
|
|
35077
|
+
throw new Error(`Plivo hangup failed: ${res.status} ${await res.text()}`);
|
|
35078
|
+
}
|
|
35079
|
+
return;
|
|
35080
|
+
}
|
|
34195
35081
|
throw new Error(`endCall() requires a configured carrier; got kind=${carrier.kind}`);
|
|
34196
35082
|
}
|
|
34197
35083
|
};
|
|
@@ -34612,7 +35498,6 @@ init_cjs_shims();
|
|
|
34612
35498
|
|
|
34613
35499
|
// src/integrations/patter-tool.ts
|
|
34614
35500
|
init_cjs_shims();
|
|
34615
|
-
var import_node_events = require("events");
|
|
34616
35501
|
var PARAMETERS_SCHEMA = {
|
|
34617
35502
|
type: "object",
|
|
34618
35503
|
properties: {
|
|
@@ -34639,7 +35524,7 @@ var PARAMETERS_SCHEMA = {
|
|
|
34639
35524
|
};
|
|
34640
35525
|
var DEFAULT_NAME = "make_phone_call";
|
|
34641
35526
|
var DEFAULT_DESCRIPTION = "Place a real outbound phone call. Returns a JSON object with the full transcript, call status, duration in seconds, and cost. Use this when the user asks you to call someone, schedule appointments by phone, or otherwise reach a human via voice.";
|
|
34642
|
-
var PatterTool = class
|
|
35527
|
+
var PatterTool = class {
|
|
34643
35528
|
name;
|
|
34644
35529
|
description;
|
|
34645
35530
|
phone;
|
|
@@ -34647,24 +35532,6 @@ var PatterTool = class _PatterTool {
|
|
|
34647
35532
|
maxDurationSec;
|
|
34648
35533
|
recording;
|
|
34649
35534
|
started = false;
|
|
34650
|
-
/** Resolver for the next `call_initiated` SSE event. Only set inside the
|
|
34651
|
-
* dial mutex (`dialQueue`), so two parallel `execute()` calls never share
|
|
34652
|
-
* it and never lose a dispatch. */
|
|
34653
|
-
pendingDial = null;
|
|
34654
|
-
/** Mutex that serializes the dial → call_id capture critical section.
|
|
34655
|
-
* Each `execute()` chains a continuation onto this promise so the
|
|
34656
|
-
* `pendingDial` slot is owned by exactly one caller at a time. */
|
|
34657
|
-
dialQueue = Promise.resolve();
|
|
34658
|
-
/** Captured SSE listener so `stop()` can detach it (prevents leaks when
|
|
34659
|
-
* the underlying Patter instance outlives this tool). */
|
|
34660
|
-
sseListener = null;
|
|
34661
|
-
/** Captured Patter metrics store, for cleanup in `stop()`. */
|
|
34662
|
-
metricsStoreRef = null;
|
|
34663
|
-
/** call_id → pending promise machinery. */
|
|
34664
|
-
pending = /* @__PURE__ */ new Map();
|
|
34665
|
-
bus = new import_node_events.EventEmitter();
|
|
34666
|
-
/** How long to wait for the `call_initiated` SSE before failing the dial. */
|
|
34667
|
-
static DIAL_CAPTURE_TIMEOUT_MS = 1e4;
|
|
34668
35535
|
constructor(opts) {
|
|
34669
35536
|
if (!opts.phone) {
|
|
34670
35537
|
throw new Error("PatterTool: `phone` (a Patter instance) is required.");
|
|
@@ -34708,7 +35575,15 @@ var PatterTool = class _PatterTool {
|
|
|
34708
35575
|
};
|
|
34709
35576
|
}
|
|
34710
35577
|
// --- Lifecycle ----------------------------------------------------------
|
|
34711
|
-
/**
|
|
35578
|
+
/**
|
|
35579
|
+
* Start the underlying Patter server. Idempotent.
|
|
35580
|
+
*
|
|
35581
|
+
* `execute()` relies on `Patter.call({ wait: true })`, which requires an
|
|
35582
|
+
* active server to receive the carrier completion webhooks — that's what
|
|
35583
|
+
* `serve()` provides here. No `onCallEnd` callback is wired: the SDK's own
|
|
35584
|
+
* per-callId completion registry resolves the result, so the user's
|
|
35585
|
+
* `onCallEnd` slot is left free.
|
|
35586
|
+
*/
|
|
34712
35587
|
async start() {
|
|
34713
35588
|
if (this.started) return;
|
|
34714
35589
|
if (!this.agent) {
|
|
@@ -34720,52 +35595,31 @@ var PatterTool = class _PatterTool {
|
|
|
34720
35595
|
await this.phone.serve({
|
|
34721
35596
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34722
35597
|
agent: builtAgent,
|
|
34723
|
-
recording: this.recording
|
|
34724
|
-
onCallEnd: this.onCallEndHandler.bind(this)
|
|
35598
|
+
recording: this.recording
|
|
34725
35599
|
});
|
|
34726
|
-
const store = this.phone.metricsStore;
|
|
34727
|
-
if (!store) {
|
|
34728
|
-
throw new Error(
|
|
34729
|
-
"PatterTool.start: phone.metricsStore is null after serve() \u2014 is the dashboard disabled?"
|
|
34730
|
-
);
|
|
34731
|
-
}
|
|
34732
|
-
const listener = (event) => {
|
|
34733
|
-
if (event.type === "call_initiated" && this.pendingDial) {
|
|
34734
|
-
const callId = event.data.call_id || "";
|
|
34735
|
-
if (callId) {
|
|
34736
|
-
const dispatch = this.pendingDial;
|
|
34737
|
-
this.pendingDial = null;
|
|
34738
|
-
dispatch(callId);
|
|
34739
|
-
}
|
|
34740
|
-
}
|
|
34741
|
-
};
|
|
34742
|
-
store.on("sse", listener);
|
|
34743
|
-
this.sseListener = listener;
|
|
34744
|
-
this.metricsStoreRef = store;
|
|
34745
35600
|
this.started = true;
|
|
34746
35601
|
}
|
|
34747
|
-
/**
|
|
35602
|
+
/** Best-effort shutdown — tear the Patter server down via `disconnect()`. */
|
|
34748
35603
|
async stop() {
|
|
34749
35604
|
if (!this.started) return;
|
|
34750
|
-
|
|
34751
|
-
|
|
34752
|
-
|
|
34753
|
-
|
|
34754
|
-
|
|
34755
|
-
|
|
34756
|
-
for (const [, p] of this.pending) {
|
|
34757
|
-
clearTimeout(p.timer);
|
|
34758
|
-
p.reject(new Error("PatterTool: shutdown while call pending"));
|
|
34759
|
-
}
|
|
34760
|
-
this.pending.clear();
|
|
34761
|
-
const stoppable = this.phone;
|
|
34762
|
-
if (typeof stoppable.stop === "function") {
|
|
34763
|
-
await stoppable.stop();
|
|
35605
|
+
const disconnectable = this.phone;
|
|
35606
|
+
if (typeof disconnectable.disconnect === "function") {
|
|
35607
|
+
try {
|
|
35608
|
+
await disconnectable.disconnect();
|
|
35609
|
+
} catch {
|
|
35610
|
+
}
|
|
34764
35611
|
}
|
|
34765
35612
|
this.started = false;
|
|
34766
35613
|
}
|
|
34767
35614
|
// --- Execution ----------------------------------------------------------
|
|
34768
|
-
/**
|
|
35615
|
+
/**
|
|
35616
|
+
* Dial outbound, wait for the call to end, return a structured result.
|
|
35617
|
+
*
|
|
35618
|
+
* Thin wrapper over `Patter.call({ wait: true })`: the SDK now owns the
|
|
35619
|
+
* dial → callId → terminal-signal correlation, so this just bounds the wait
|
|
35620
|
+
* with `max_duration_sec` and maps the {@link CallResult} into the tool's
|
|
35621
|
+
* public envelope. Mirrors Python's `PatterTool.execute`.
|
|
35622
|
+
*/
|
|
34769
35623
|
async execute(args) {
|
|
34770
35624
|
if (!this.started) await this.start();
|
|
34771
35625
|
if (!args || typeof args.to !== "string" || !args.to.startsWith("+")) {
|
|
@@ -34781,55 +35635,32 @@ var PatterTool = class _PatterTool {
|
|
|
34781
35635
|
...args.goal !== void 0 ? { systemPrompt: args.goal } : {},
|
|
34782
35636
|
...args.first_message !== void 0 ? { firstMessage: args.first_message } : {}
|
|
34783
35637
|
});
|
|
34784
|
-
|
|
34785
|
-
|
|
34786
|
-
|
|
34787
|
-
|
|
34788
|
-
|
|
35638
|
+
let timer;
|
|
35639
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
35640
|
+
timer = setTimeout(() => {
|
|
35641
|
+
reject(
|
|
35642
|
+
new Error(
|
|
35643
|
+
`PatterTool.execute: call to ${args.to} exceeded ${timeoutSec}s timeout`
|
|
35644
|
+
)
|
|
35645
|
+
);
|
|
34789
35646
|
}, timeoutSec * 1e3);
|
|
34790
|
-
|
|
34791
|
-
resolve: resolve2,
|
|
34792
|
-
reject,
|
|
34793
|
-
timer,
|
|
34794
|
-
startedAt: Date.now() / 1e3
|
|
34795
|
-
});
|
|
34796
|
-
});
|
|
34797
|
-
}
|
|
34798
|
-
/** Issue the outbound dial under the mutex and return its assigned call_id. */
|
|
34799
|
-
async acquireCallId(to, agent) {
|
|
34800
|
-
let release;
|
|
34801
|
-
const slot = new Promise((r) => {
|
|
34802
|
-
release = r;
|
|
35647
|
+
timer.unref?.();
|
|
34803
35648
|
});
|
|
34804
|
-
|
|
34805
|
-
this.dialQueue = previous.then(() => slot);
|
|
34806
|
-
await previous;
|
|
34807
|
-
let captureTimer = null;
|
|
35649
|
+
let result;
|
|
34808
35650
|
try {
|
|
34809
|
-
|
|
34810
|
-
this.
|
|
34811
|
-
|
|
34812
|
-
|
|
34813
|
-
|
|
34814
|
-
|
|
34815
|
-
|
|
34816
|
-
|
|
34817
|
-
|
|
34818
|
-
}, _PatterTool.DIAL_CAPTURE_TIMEOUT_MS);
|
|
34819
|
-
});
|
|
34820
|
-
await this.phone.call({
|
|
34821
|
-
to,
|
|
34822
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34823
|
-
agent
|
|
34824
|
-
});
|
|
34825
|
-
const callId = await callIdPromise;
|
|
34826
|
-
if (captureTimer) clearTimeout(captureTimer);
|
|
34827
|
-
return callId;
|
|
35651
|
+
result = await Promise.race([
|
|
35652
|
+
this.phone.call({
|
|
35653
|
+
to: args.to,
|
|
35654
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35655
|
+
agent: overrideAgent,
|
|
35656
|
+
wait: true
|
|
35657
|
+
}),
|
|
35658
|
+
timeout
|
|
35659
|
+
]);
|
|
34828
35660
|
} finally {
|
|
34829
|
-
if (
|
|
34830
|
-
this.pendingDial = null;
|
|
34831
|
-
release();
|
|
35661
|
+
if (timer) clearTimeout(timer);
|
|
34832
35662
|
}
|
|
35663
|
+
return resultFromCallResult(result);
|
|
34833
35664
|
}
|
|
34834
35665
|
/**
|
|
34835
35666
|
* Hermes-style handler: `(args, kwargs) => Promise<string>` returning a JSON
|
|
@@ -34847,32 +35678,32 @@ var PatterTool = class _PatterTool {
|
|
|
34847
35678
|
}
|
|
34848
35679
|
};
|
|
34849
35680
|
}
|
|
34850
|
-
// --- Internal: onCallEnd dispatcher -------------------------------------
|
|
34851
|
-
async onCallEndHandler(data) {
|
|
34852
|
-
const callId = data.call_id || "";
|
|
34853
|
-
if (!callId) return;
|
|
34854
|
-
const pending = this.pending.get(callId);
|
|
34855
|
-
if (!pending) {
|
|
34856
|
-
this.bus.emit("orphan_end", { call_id: callId, data });
|
|
34857
|
-
return;
|
|
34858
|
-
}
|
|
34859
|
-
clearTimeout(pending.timer);
|
|
34860
|
-
this.pending.delete(callId);
|
|
34861
|
-
const metrics = data.metrics && typeof data.metrics === "object" ? data.metrics : null;
|
|
34862
|
-
const cost = metrics && typeof metrics.cost === "object" && metrics.cost && typeof metrics.cost.total === "number" ? metrics.cost.total : void 0;
|
|
34863
|
-
const duration3 = typeof metrics?.duration_seconds === "number" ? metrics?.duration_seconds : Math.max(0, Date.now() / 1e3 - pending.startedAt);
|
|
34864
|
-
const transcript = Array.isArray(data.transcript) ? data.transcript : [];
|
|
34865
|
-
const status = data.status || "completed";
|
|
34866
|
-
pending.resolve({
|
|
34867
|
-
call_id: callId,
|
|
34868
|
-
status,
|
|
34869
|
-
duration_seconds: duration3,
|
|
34870
|
-
cost_usd: cost,
|
|
34871
|
-
transcript,
|
|
34872
|
-
metrics
|
|
34873
|
-
});
|
|
34874
|
-
}
|
|
34875
35681
|
};
|
|
35682
|
+
function resultFromCallResult(result) {
|
|
35683
|
+
if (!result) {
|
|
35684
|
+
return {
|
|
35685
|
+
call_id: "",
|
|
35686
|
+
status: "completed",
|
|
35687
|
+
outcome: "",
|
|
35688
|
+
duration_seconds: 0,
|
|
35689
|
+
cost_usd: void 0,
|
|
35690
|
+
transcript: [],
|
|
35691
|
+
metrics: null
|
|
35692
|
+
};
|
|
35693
|
+
}
|
|
35694
|
+
const costTotal = result.cost?.total;
|
|
35695
|
+
const costUsd = typeof costTotal === "number" ? costTotal : void 0;
|
|
35696
|
+
const metrics = result.metrics ? result.metrics : null;
|
|
35697
|
+
return {
|
|
35698
|
+
call_id: result.callId || "",
|
|
35699
|
+
status: result.status || "completed",
|
|
35700
|
+
outcome: result.outcome || "",
|
|
35701
|
+
duration_seconds: typeof result.durationSeconds === "number" ? result.durationSeconds : 0,
|
|
35702
|
+
cost_usd: costUsd,
|
|
35703
|
+
transcript: result.transcript ? [...result.transcript] : [],
|
|
35704
|
+
metrics
|
|
35705
|
+
};
|
|
35706
|
+
}
|
|
34876
35707
|
|
|
34877
35708
|
// src/index.ts
|
|
34878
35709
|
init_test_mode();
|
|
@@ -35451,6 +36282,12 @@ var ELEVENLABS_VOICE_ID_BY_NAME = {
|
|
|
35451
36282
|
alloy: "EXAVITQu4vr4xnSDxMaL"
|
|
35452
36283
|
};
|
|
35453
36284
|
var VOICE_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
|
|
36285
|
+
var CARRIER_NATIVE_FORMAT = {
|
|
36286
|
+
twilio: "ulaw_8000",
|
|
36287
|
+
telnyx: "pcm_16000",
|
|
36288
|
+
// Plivo streams mulaw 8 kHz (we pin contentType in the answer XML).
|
|
36289
|
+
plivo: "ulaw_8000"
|
|
36290
|
+
};
|
|
35454
36291
|
function resolveVoiceId(voice) {
|
|
35455
36292
|
if (!voice) return voice;
|
|
35456
36293
|
if (VOICE_ID_PATTERN.test(voice)) return voice;
|
|
@@ -35539,11 +36376,8 @@ var ElevenLabsTTS = class _ElevenLabsTTS {
|
|
|
35539
36376
|
*/
|
|
35540
36377
|
setTelephonyCarrier(carrier) {
|
|
35541
36378
|
if (this._outputFormatExplicit) return;
|
|
35542
|
-
|
|
35543
|
-
|
|
35544
|
-
} else if (carrier === "telnyx") {
|
|
35545
|
-
this._outputFormat = ElevenLabsOutputFormat.PCM_16000;
|
|
35546
|
-
}
|
|
36379
|
+
const native = CARRIER_NATIVE_FORMAT[carrier];
|
|
36380
|
+
if (native !== void 0) this._outputFormat = native;
|
|
35547
36381
|
}
|
|
35548
36382
|
/**
|
|
35549
36383
|
* Construct an instance pre-configured for Twilio Media Streams.
|
|
@@ -37776,9 +38610,11 @@ var PLAN_REQUIRED_MSG = "ElevenLabs WS streaming requires a Pro plan or higher (
|
|
|
37776
38610
|
function sanitiseLogStr(value, limit = 200) {
|
|
37777
38611
|
return String(value).replace(/[\r\n\x00]/g, " ").slice(0, limit);
|
|
37778
38612
|
}
|
|
37779
|
-
var
|
|
38613
|
+
var CARRIER_NATIVE_FORMAT2 = {
|
|
37780
38614
|
twilio: "ulaw_8000",
|
|
37781
|
-
telnyx: "pcm_16000"
|
|
38615
|
+
telnyx: "pcm_16000",
|
|
38616
|
+
// Plivo streams mulaw 8 kHz (we pin contentType in the answer XML).
|
|
38617
|
+
plivo: "ulaw_8000"
|
|
37782
38618
|
};
|
|
37783
38619
|
var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
|
|
37784
38620
|
static providerKey = "elevenlabs_ws";
|
|
@@ -37862,7 +38698,7 @@ var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
|
|
|
37862
38698
|
*/
|
|
37863
38699
|
setTelephonyCarrier(carrier) {
|
|
37864
38700
|
if (this._outputFormatExplicit) return;
|
|
37865
|
-
const native =
|
|
38701
|
+
const native = CARRIER_NATIVE_FORMAT2[carrier];
|
|
37866
38702
|
if (!native) return;
|
|
37867
38703
|
this._outputFormat = native;
|
|
37868
38704
|
}
|
|
@@ -40024,7 +40860,7 @@ var KrispVivaFilter = class {
|
|
|
40024
40860
|
|
|
40025
40861
|
// src/telephony/twilio.ts
|
|
40026
40862
|
init_cjs_shims();
|
|
40027
|
-
var
|
|
40863
|
+
var Carrier2 = class {
|
|
40028
40864
|
kind = "twilio";
|
|
40029
40865
|
accountSid;
|
|
40030
40866
|
authToken;
|
|
@@ -40048,7 +40884,7 @@ var Carrier = class {
|
|
|
40048
40884
|
|
|
40049
40885
|
// src/telephony/telnyx.ts
|
|
40050
40886
|
init_cjs_shims();
|
|
40051
|
-
var
|
|
40887
|
+
var Carrier3 = class {
|
|
40052
40888
|
kind = "telnyx";
|
|
40053
40889
|
apiKey;
|
|
40054
40890
|
connectionId;
|
|
@@ -40074,6 +40910,7 @@ var Carrier2 = class {
|
|
|
40074
40910
|
};
|
|
40075
40911
|
|
|
40076
40912
|
// src/index.ts
|
|
40913
|
+
init_plivo();
|
|
40077
40914
|
init_openai_realtime_2();
|
|
40078
40915
|
|
|
40079
40916
|
// src/public-api.ts
|
|
@@ -40132,9 +40969,9 @@ init_tunnel();
|
|
|
40132
40969
|
|
|
40133
40970
|
// src/chat-context.ts
|
|
40134
40971
|
init_cjs_shims();
|
|
40135
|
-
var
|
|
40972
|
+
var import_node_crypto5 = require("crypto");
|
|
40136
40973
|
function generateId() {
|
|
40137
|
-
return (0,
|
|
40974
|
+
return (0, import_node_crypto5.randomUUID)().replace(/-/g, "").slice(0, 12);
|
|
40138
40975
|
}
|
|
40139
40976
|
function createMessage(role, content, options) {
|
|
40140
40977
|
return Object.freeze({
|
|
@@ -40914,7 +41751,7 @@ var TwilioAdapter = class _TwilioAdapter {
|
|
|
40914
41751
|
|
|
40915
41752
|
// src/providers/telnyx-adapter.ts
|
|
40916
41753
|
init_cjs_shims();
|
|
40917
|
-
var
|
|
41754
|
+
var import_node_crypto6 = require("crypto");
|
|
40918
41755
|
init_logger();
|
|
40919
41756
|
var TELNYX_API_BASE2 = "https://api.telnyx.com/v2";
|
|
40920
41757
|
var TelnyxAdapter = class {
|
|
@@ -41018,7 +41855,7 @@ var TelnyxAdapter = class {
|
|
|
41018
41855
|
if (!callControlId) throw new Error("TelnyxAdapter: callControlId is required");
|
|
41019
41856
|
const encoded = encodeURIComponent(callControlId);
|
|
41020
41857
|
const body = {
|
|
41021
|
-
command_id: opts.commandId ?? (0,
|
|
41858
|
+
command_id: opts.commandId ?? (0, import_node_crypto6.randomUUID)()
|
|
41022
41859
|
};
|
|
41023
41860
|
try {
|
|
41024
41861
|
await this.request(
|
|
@@ -41035,6 +41872,9 @@ var TelnyxAdapter = class {
|
|
|
41035
41872
|
}
|
|
41036
41873
|
};
|
|
41037
41874
|
|
|
41875
|
+
// src/index.ts
|
|
41876
|
+
init_plivo_adapter();
|
|
41877
|
+
|
|
41038
41878
|
// src/providers/telnyx-stt.ts
|
|
41039
41879
|
init_cjs_shims();
|
|
41040
41880
|
var import_ws12 = __toESM(require("ws"));
|
|
@@ -41352,6 +42192,8 @@ init_event_bus();
|
|
|
41352
42192
|
PatterTool,
|
|
41353
42193
|
PcmCarry,
|
|
41354
42194
|
PipelineHookExecutor,
|
|
42195
|
+
Plivo,
|
|
42196
|
+
PlivoAdapter,
|
|
41355
42197
|
PricingUnit,
|
|
41356
42198
|
ProvisionError,
|
|
41357
42199
|
RateLimitError,
|