@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.
@@ -0,0 +1,2 @@
1
+ import { t as createVoiceCallRuntime } from "./runtime-entry-ohPMJR46.js";
2
+ export { createVoiceCallRuntime };
@@ -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 };