@kodelyth/voice-call 2026.5.39 → 2026.6.1
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 +167 -0
- package/dist/api.js +2 -0
- package/dist/cli-metadata.js +12 -0
- package/dist/config-DAwbG2aw.js +621 -0
- package/dist/config-compat-BYfJ5ueI.js +129 -0
- package/dist/guarded-json-api-xAIbFPZh.js +591 -0
- package/dist/index.js +1341 -0
- package/dist/mock-jtSdKDQN.js +135 -0
- package/dist/plivo-L-JTeuEc.js +392 -0
- package/dist/realtime-handler-5pSItXxX.js +1227 -0
- package/dist/realtime-transcription.runtime-CAbQKwCN.js +2 -0
- package/dist/realtime-voice.runtime-vCpCAutg.js +2 -0
- package/dist/response-generator-B-MjbtsM.js +199 -0
- package/dist/runtime-api.js +6 -0
- package/dist/runtime-entry-ohPMJR46.js +3435 -0
- package/dist/runtime-entry.js +2 -0
- package/dist/setup-api.js +37 -0
- package/dist/telnyx-BWr9EZ4x.js +278 -0
- package/dist/twilio-D9B0zY1k.js +679 -0
- package/klaw.plugin.json +30 -133
- package/package.json +18 -6
- package/api.js +0 -7
- package/cli-metadata.js +0 -7
- package/index.js +0 -7
- package/runtime-api.js +0 -7
- package/runtime-entry.js +0 -7
- package/setup-api.js +0 -7
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-BYfJ5ueI.js";
|
|
2
|
+
import { isRecord } from "klaw/plugin-sdk/string-coerce-runtime";
|
|
3
|
+
import { definePluginEntry } from "klaw/plugin-sdk/plugin-entry";
|
|
4
|
+
//#region extensions/voice-call/setup-api.ts
|
|
5
|
+
function migrateVoiceCallPluginConfig(config) {
|
|
6
|
+
const rawVoiceCallConfig = config.plugins?.entries?.["voice-call"]?.config;
|
|
7
|
+
if (!isRecord(rawVoiceCallConfig)) return null;
|
|
8
|
+
const migration = migrateVoiceCallLegacyConfigInput({
|
|
9
|
+
value: rawVoiceCallConfig,
|
|
10
|
+
configPathPrefix: "plugins.entries.voice-call.config"
|
|
11
|
+
});
|
|
12
|
+
if (migration.changes.length === 0) return null;
|
|
13
|
+
const plugins = structuredClone(config.plugins ?? {});
|
|
14
|
+
const entries = { ...plugins.entries };
|
|
15
|
+
entries["voice-call"] = {
|
|
16
|
+
...isRecord(entries["voice-call"]) ? entries["voice-call"] : {},
|
|
17
|
+
config: migration.config
|
|
18
|
+
};
|
|
19
|
+
plugins.entries = entries;
|
|
20
|
+
return {
|
|
21
|
+
config: {
|
|
22
|
+
...config,
|
|
23
|
+
plugins
|
|
24
|
+
},
|
|
25
|
+
changes: migration.changes
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
var setup_api_default = definePluginEntry({
|
|
29
|
+
id: "voice-call",
|
|
30
|
+
name: "Voice Call Setup",
|
|
31
|
+
description: "Lightweight Voice Call setup hooks",
|
|
32
|
+
register(api) {
|
|
33
|
+
api.registerConfigMigration((config) => migrateVoiceCallPluginConfig(config));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
//#endregion
|
|
37
|
+
export { setup_api_default as default };
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-xAIbFPZh.js";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
|
+
function normalizeTelnyxDirection(direction) {
|
|
5
|
+
switch (direction) {
|
|
6
|
+
case "incoming":
|
|
7
|
+
case "inbound": return "inbound";
|
|
8
|
+
case "outgoing":
|
|
9
|
+
case "outbound": return "outbound";
|
|
10
|
+
default: return;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function normalizeBase64ForCompare(value) {
|
|
14
|
+
return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
|
|
15
|
+
}
|
|
16
|
+
function decodeClientStateBase64(value) {
|
|
17
|
+
const buffer = Buffer.from(value, "base64");
|
|
18
|
+
if (normalizeBase64ForCompare(buffer.toString("base64")) !== normalizeBase64ForCompare(value)) return null;
|
|
19
|
+
return buffer.toString("utf8");
|
|
20
|
+
}
|
|
21
|
+
var TelnyxProvider = class {
|
|
22
|
+
constructor(config, options = {}) {
|
|
23
|
+
this.name = "telnyx";
|
|
24
|
+
this.baseUrl = "https://api.telnyx.com/v2";
|
|
25
|
+
this.apiHost = "api.telnyx.com";
|
|
26
|
+
if (!config.apiKey) throw new Error("Telnyx API key is required");
|
|
27
|
+
if (!config.connectionId) throw new Error("Telnyx connection ID is required");
|
|
28
|
+
this.apiKey = config.apiKey;
|
|
29
|
+
this.connectionId = config.connectionId;
|
|
30
|
+
this.publicKey = config.publicKey;
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Make an authenticated request to the Telnyx API.
|
|
35
|
+
*/
|
|
36
|
+
async apiRequest(endpoint, body, options) {
|
|
37
|
+
return await guardedJsonApiRequest({
|
|
38
|
+
url: `${this.baseUrl}${endpoint}`,
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
42
|
+
"Content-Type": "application/json"
|
|
43
|
+
},
|
|
44
|
+
body,
|
|
45
|
+
allowNotFound: options?.allowNotFound,
|
|
46
|
+
allowedHostnames: [this.apiHost],
|
|
47
|
+
auditContext: "voice-call.telnyx.api",
|
|
48
|
+
errorPrefix: "Telnyx API error"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Verify Telnyx webhook signature using Ed25519.
|
|
53
|
+
*/
|
|
54
|
+
verifyWebhook(ctx) {
|
|
55
|
+
const result = verifyTelnyxWebhook(ctx, this.publicKey, { skipVerification: this.options.skipVerification });
|
|
56
|
+
return {
|
|
57
|
+
ok: result.ok,
|
|
58
|
+
reason: result.reason,
|
|
59
|
+
isReplay: result.isReplay,
|
|
60
|
+
verifiedRequestKey: result.verifiedRequestKey
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse Telnyx webhook event into normalized format.
|
|
65
|
+
*/
|
|
66
|
+
parseWebhookEvent(ctx, options) {
|
|
67
|
+
try {
|
|
68
|
+
const data = JSON.parse(ctx.rawBody).data;
|
|
69
|
+
if (!data || !data.event_type) return {
|
|
70
|
+
events: [],
|
|
71
|
+
statusCode: 200
|
|
72
|
+
};
|
|
73
|
+
const event = this.normalizeEvent(data, options?.verifiedRequestKey);
|
|
74
|
+
return {
|
|
75
|
+
events: event ? [event] : [],
|
|
76
|
+
statusCode: 200
|
|
77
|
+
};
|
|
78
|
+
} catch {
|
|
79
|
+
return {
|
|
80
|
+
events: [],
|
|
81
|
+
statusCode: 400
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Convert Telnyx event to normalized event format.
|
|
87
|
+
*/
|
|
88
|
+
normalizeEvent(data, dedupeKey) {
|
|
89
|
+
let callId = "";
|
|
90
|
+
if (data.payload?.client_state) callId = decodeClientStateBase64(data.payload.client_state) ?? data.payload.client_state;
|
|
91
|
+
if (!callId) callId = data.payload?.call_control_id || "";
|
|
92
|
+
const baseEvent = {
|
|
93
|
+
id: data.id || crypto.randomUUID(),
|
|
94
|
+
dedupeKey,
|
|
95
|
+
callId,
|
|
96
|
+
providerCallId: data.payload?.call_control_id,
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
direction: normalizeTelnyxDirection(data.payload?.direction),
|
|
99
|
+
from: data.payload?.from,
|
|
100
|
+
to: data.payload?.to
|
|
101
|
+
};
|
|
102
|
+
switch (data.event_type) {
|
|
103
|
+
case "call.initiated": return {
|
|
104
|
+
...baseEvent,
|
|
105
|
+
type: "call.initiated"
|
|
106
|
+
};
|
|
107
|
+
case "call.ringing": return {
|
|
108
|
+
...baseEvent,
|
|
109
|
+
type: "call.ringing"
|
|
110
|
+
};
|
|
111
|
+
case "call.answered": return {
|
|
112
|
+
...baseEvent,
|
|
113
|
+
type: "call.answered"
|
|
114
|
+
};
|
|
115
|
+
case "call.bridged": return {
|
|
116
|
+
...baseEvent,
|
|
117
|
+
type: "call.active"
|
|
118
|
+
};
|
|
119
|
+
case "call.speak.started": return {
|
|
120
|
+
...baseEvent,
|
|
121
|
+
type: "call.speaking",
|
|
122
|
+
text: data.payload?.text || ""
|
|
123
|
+
};
|
|
124
|
+
case "call.transcription": return {
|
|
125
|
+
...baseEvent,
|
|
126
|
+
type: "call.speech",
|
|
127
|
+
transcript: data.payload?.transcription_data?.transcript ?? data.payload?.transcription ?? "",
|
|
128
|
+
isFinal: data.payload?.transcription_data?.is_final ?? data.payload?.is_final ?? true,
|
|
129
|
+
confidence: data.payload?.transcription_data?.confidence ?? data.payload?.confidence
|
|
130
|
+
};
|
|
131
|
+
case "call.hangup": return {
|
|
132
|
+
...baseEvent,
|
|
133
|
+
type: "call.ended",
|
|
134
|
+
reason: this.mapHangupCause(data.payload?.hangup_cause)
|
|
135
|
+
};
|
|
136
|
+
case "call.dtmf.received": return {
|
|
137
|
+
...baseEvent,
|
|
138
|
+
type: "call.dtmf",
|
|
139
|
+
digits: data.payload?.digit || ""
|
|
140
|
+
};
|
|
141
|
+
case "streaming.started":
|
|
142
|
+
case "streaming.stopped": return null;
|
|
143
|
+
default: return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Map Telnyx hangup cause to normalized end reason.
|
|
148
|
+
* @see https://developers.telnyx.com/docs/api/v2/call-control/Call-Commands#hangup-causes
|
|
149
|
+
*/
|
|
150
|
+
mapHangupCause(cause) {
|
|
151
|
+
switch (cause) {
|
|
152
|
+
case "normal_clearing":
|
|
153
|
+
case "normal_unspecified": return "completed";
|
|
154
|
+
case "originator_cancel": return "hangup-bot";
|
|
155
|
+
case "call_rejected":
|
|
156
|
+
case "user_busy": return "busy";
|
|
157
|
+
case "no_answer":
|
|
158
|
+
case "no_user_response": return "no-answer";
|
|
159
|
+
case "destination_out_of_order":
|
|
160
|
+
case "network_out_of_order":
|
|
161
|
+
case "service_unavailable":
|
|
162
|
+
case "recovery_on_timer_expire": return "failed";
|
|
163
|
+
case "machine_detected":
|
|
164
|
+
case "fax_detected": return "voicemail";
|
|
165
|
+
case "user_hangup":
|
|
166
|
+
case "subscriber_absent": return "hangup-user";
|
|
167
|
+
default:
|
|
168
|
+
if (cause) console.warn(`[telnyx] Unknown hangup cause: ${cause}`);
|
|
169
|
+
return "completed";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async initiateCall(input) {
|
|
173
|
+
const body = {
|
|
174
|
+
connection_id: this.connectionId,
|
|
175
|
+
to: input.to,
|
|
176
|
+
from: input.from,
|
|
177
|
+
webhook_url: input.webhookUrl,
|
|
178
|
+
webhook_url_method: "POST",
|
|
179
|
+
client_state: Buffer.from(input.callId).toString("base64"),
|
|
180
|
+
timeout_secs: 30,
|
|
181
|
+
...input.streamUrl ? buildTelnyxStreamingFields(input.streamUrl, input.streamAuthToken) : {}
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
providerCallId: (await this.apiRequest("/calls", body)).data.call_control_id,
|
|
185
|
+
status: "initiated"
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Hang up a call via Telnyx API.
|
|
190
|
+
*/
|
|
191
|
+
async hangupCall(input) {
|
|
192
|
+
await this.apiRequest(`/calls/${input.providerCallId}/actions/hangup`, { command_id: crypto.randomUUID() }, { allowNotFound: true });
|
|
193
|
+
}
|
|
194
|
+
async answerCall(input) {
|
|
195
|
+
const body = {
|
|
196
|
+
command_id: `klaw-answer-${input.callId}`,
|
|
197
|
+
...input.streamUrl ? buildTelnyxStreamingFields(input.streamUrl, input.streamAuthToken) : {}
|
|
198
|
+
};
|
|
199
|
+
await this.apiRequest(`/calls/${input.providerCallId}/actions/answer`, body);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Play TTS audio via Telnyx speak action.
|
|
203
|
+
*/
|
|
204
|
+
async playTts(input) {
|
|
205
|
+
await this.apiRequest(`/calls/${input.providerCallId}/actions/speak`, {
|
|
206
|
+
command_id: crypto.randomUUID(),
|
|
207
|
+
payload: input.text,
|
|
208
|
+
voice: input.voice || "female",
|
|
209
|
+
language: input.locale || "en-US"
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Start transcription (STT) via Telnyx.
|
|
214
|
+
*/
|
|
215
|
+
async startListening(input) {
|
|
216
|
+
await this.apiRequest(`/calls/${input.providerCallId}/actions/transcription_start`, {
|
|
217
|
+
command_id: crypto.randomUUID(),
|
|
218
|
+
language: input.language || "en"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Stop transcription via Telnyx.
|
|
223
|
+
*/
|
|
224
|
+
async stopListening(input) {
|
|
225
|
+
await this.apiRequest(`/calls/${input.providerCallId}/actions/transcription_stop`, { command_id: crypto.randomUUID() }, { allowNotFound: true });
|
|
226
|
+
}
|
|
227
|
+
async getCallStatus(input) {
|
|
228
|
+
try {
|
|
229
|
+
const data = await guardedJsonApiRequest({
|
|
230
|
+
url: `${this.baseUrl}/calls/${input.providerCallId}`,
|
|
231
|
+
method: "GET",
|
|
232
|
+
headers: {
|
|
233
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
234
|
+
"Content-Type": "application/json"
|
|
235
|
+
},
|
|
236
|
+
allowNotFound: true,
|
|
237
|
+
allowedHostnames: [this.apiHost],
|
|
238
|
+
auditContext: "telnyx-get-call-status",
|
|
239
|
+
errorPrefix: "Telnyx get call status error"
|
|
240
|
+
});
|
|
241
|
+
if (!data) return {
|
|
242
|
+
status: "not-found",
|
|
243
|
+
isTerminal: true
|
|
244
|
+
};
|
|
245
|
+
const state = data.data?.state ?? "unknown";
|
|
246
|
+
const isAlive = data.data?.is_alive;
|
|
247
|
+
if (isAlive === void 0) return {
|
|
248
|
+
status: state,
|
|
249
|
+
isTerminal: false,
|
|
250
|
+
isUnknown: true
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
status: state,
|
|
254
|
+
isTerminal: !isAlive
|
|
255
|
+
};
|
|
256
|
+
} catch {
|
|
257
|
+
return {
|
|
258
|
+
status: "error",
|
|
259
|
+
isTerminal: false,
|
|
260
|
+
isUnknown: true
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
function buildTelnyxStreamingFields(streamUrl, streamAuthToken) {
|
|
266
|
+
return {
|
|
267
|
+
stream_url: streamUrl,
|
|
268
|
+
stream_track: "inbound_track",
|
|
269
|
+
stream_codec: "PCMU",
|
|
270
|
+
stream_bidirectional_mode: "rtp",
|
|
271
|
+
stream_bidirectional_codec: "PCMU",
|
|
272
|
+
stream_bidirectional_sampling_rate: 8e3,
|
|
273
|
+
stream_bidirectional_target_legs: "self",
|
|
274
|
+
...streamAuthToken ? { stream_auth_token: streamAuthToken } : {}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
278
|
+
export { TelnyxProvider };
|