@framers/agentos 0.1.111 → 0.1.113
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/dist/api/strategies/debate.d.ts +12 -1
- package/dist/api/strategies/debate.d.ts.map +1 -1
- package/dist/api/strategies/debate.js +41 -5
- package/dist/api/strategies/debate.js.map +1 -1
- package/dist/api/strategies/hierarchical.d.ts +15 -1
- package/dist/api/strategies/hierarchical.d.ts.map +1 -1
- package/dist/api/strategies/hierarchical.js +51 -7
- package/dist/api/strategies/hierarchical.js.map +1 -1
- package/dist/api/strategies/index.d.ts +26 -4
- package/dist/api/strategies/index.d.ts.map +1 -1
- package/dist/api/strategies/index.js +26 -4
- package/dist/api/strategies/index.js.map +1 -1
- package/dist/api/strategies/parallel.d.ts +15 -4
- package/dist/api/strategies/parallel.d.ts.map +1 -1
- package/dist/api/strategies/parallel.js +53 -16
- package/dist/api/strategies/parallel.js.map +1 -1
- package/dist/api/strategies/review-loop.d.ts +15 -1
- package/dist/api/strategies/review-loop.d.ts.map +1 -1
- package/dist/api/strategies/review-loop.js +36 -10
- package/dist/api/strategies/review-loop.js.map +1 -1
- package/dist/api/strategies/sequential.d.ts +11 -1
- package/dist/api/strategies/sequential.d.ts.map +1 -1
- package/dist/api/strategies/sequential.js +39 -8
- package/dist/api/strategies/sequential.js.map +1 -1
- package/dist/api/strategies/shared.d.ts +71 -7
- package/dist/api/strategies/shared.d.ts.map +1 -1
- package/dist/api/strategies/shared.js +89 -10
- package/dist/api/strategies/shared.js.map +1 -1
- package/dist/api/types.d.ts +54 -1
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js.map +1 -1
- package/dist/memory/facade/Memory.d.ts.map +1 -1
- package/dist/memory/facade/Memory.js +8 -0
- package/dist/memory/facade/Memory.js.map +1 -1
- package/dist/memory/facade/types.d.ts +10 -0
- package/dist/memory/facade/types.d.ts.map +1 -1
- package/dist/memory/index.d.ts +6 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +5 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/observation/MemoryObserver.d.ts +63 -1
- package/dist/memory/observation/MemoryObserver.d.ts.map +1 -1
- package/dist/memory/observation/MemoryObserver.js +115 -4
- package/dist/memory/observation/MemoryObserver.js.map +1 -1
- package/dist/memory/observation/ObservationCompressor.d.ts +88 -0
- package/dist/memory/observation/ObservationCompressor.d.ts.map +1 -0
- package/dist/memory/observation/ObservationCompressor.js +207 -0
- package/dist/memory/observation/ObservationCompressor.js.map +1 -0
- package/dist/memory/observation/ObservationReflector.d.ts +82 -0
- package/dist/memory/observation/ObservationReflector.d.ts.map +1 -0
- package/dist/memory/observation/ObservationReflector.js +212 -0
- package/dist/memory/observation/ObservationReflector.js.map +1 -0
- package/dist/memory/observation/temporal.d.ts +54 -0
- package/dist/memory/observation/temporal.d.ts.map +1 -0
- package/dist/memory/observation/temporal.js +115 -0
- package/dist/memory/observation/temporal.js.map +1 -0
- package/dist/orchestration/builders/VoiceNodeBuilder.d.ts +82 -25
- package/dist/orchestration/builders/VoiceNodeBuilder.d.ts.map +1 -1
- package/dist/orchestration/builders/VoiceNodeBuilder.js +86 -26
- package/dist/orchestration/builders/VoiceNodeBuilder.js.map +1 -1
- package/dist/orchestration/events/GraphEvent.d.ts +67 -5
- package/dist/orchestration/events/GraphEvent.d.ts.map +1 -1
- package/dist/orchestration/events/GraphEvent.js.map +1 -1
- package/dist/orchestration/runtime/VoiceNodeExecutor.d.ts +102 -25
- package/dist/orchestration/runtime/VoiceNodeExecutor.d.ts.map +1 -1
- package/dist/orchestration/runtime/VoiceNodeExecutor.js +133 -38
- package/dist/orchestration/runtime/VoiceNodeExecutor.js.map +1 -1
- package/dist/orchestration/runtime/VoiceTransportAdapter.d.ts +94 -32
- package/dist/orchestration/runtime/VoiceTransportAdapter.d.ts.map +1 -1
- package/dist/orchestration/runtime/VoiceTransportAdapter.js +82 -28
- package/dist/orchestration/runtime/VoiceTransportAdapter.js.map +1 -1
- package/dist/orchestration/runtime/VoiceTurnCollector.d.ts +73 -20
- package/dist/orchestration/runtime/VoiceTurnCollector.d.ts.map +1 -1
- package/dist/orchestration/runtime/VoiceTurnCollector.js +84 -23
- package/dist/orchestration/runtime/VoiceTurnCollector.js.map +1 -1
- package/dist/voice/CallManager.d.ts.map +1 -1
- package/dist/voice/CallManager.js +9 -1
- package/dist/voice/CallManager.js.map +1 -1
- package/dist/voice/MediaStreamParser.d.ts +115 -6
- package/dist/voice/MediaStreamParser.d.ts.map +1 -1
- package/dist/voice/MediaStreamParser.js +44 -0
- package/dist/voice/MediaStreamParser.js.map +1 -1
- package/dist/voice/TelephonyStreamTransport.d.ts +112 -20
- package/dist/voice/TelephonyStreamTransport.d.ts.map +1 -1
- package/dist/voice/TelephonyStreamTransport.js +136 -30
- package/dist/voice/TelephonyStreamTransport.js.map +1 -1
- package/dist/voice/parsers/PlivoMediaStreamParser.d.ts +64 -6
- package/dist/voice/parsers/PlivoMediaStreamParser.d.ts.map +1 -1
- package/dist/voice/parsers/PlivoMediaStreamParser.js +67 -6
- package/dist/voice/parsers/PlivoMediaStreamParser.js.map +1 -1
- package/dist/voice/parsers/TelnyxMediaStreamParser.d.ts +55 -8
- package/dist/voice/parsers/TelnyxMediaStreamParser.d.ts.map +1 -1
- package/dist/voice/parsers/TelnyxMediaStreamParser.js +60 -9
- package/dist/voice/parsers/TelnyxMediaStreamParser.js.map +1 -1
- package/dist/voice/parsers/TwilioMediaStreamParser.d.ts +73 -11
- package/dist/voice/parsers/TwilioMediaStreamParser.d.ts.map +1 -1
- package/dist/voice/parsers/TwilioMediaStreamParser.js +81 -12
- package/dist/voice/parsers/TwilioMediaStreamParser.js.map +1 -1
- package/dist/voice/providers/plivo.d.ts +108 -12
- package/dist/voice/providers/plivo.d.ts.map +1 -1
- package/dist/voice/providers/plivo.js +106 -9
- package/dist/voice/providers/plivo.js.map +1 -1
- package/dist/voice/providers/telnyx.d.ts +110 -20
- package/dist/voice/providers/telnyx.d.ts.map +1 -1
- package/dist/voice/providers/telnyx.js +111 -20
- package/dist/voice/providers/telnyx.js.map +1 -1
- package/dist/voice/providers/twilio.d.ts +91 -13
- package/dist/voice/providers/twilio.d.ts.map +1 -1
- package/dist/voice/providers/twilio.js +94 -14
- package/dist/voice/providers/twilio.js.map +1 -1
- package/dist/voice/twiml.d.ts +70 -12
- package/dist/voice/twiml.d.ts.map +1 -1
- package/dist/voice/twiml.js +70 -12
- package/dist/voice/twiml.js.map +1 -1
- package/dist/voice/types.d.ts +142 -15
- package/dist/voice/types.d.ts.map +1 -1
- package/dist/voice/types.js +34 -3
- package/dist/voice/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,8 +2,56 @@
|
|
|
2
2
|
* @fileoverview Telnyx telephony provider for AgentOS voice calls.
|
|
3
3
|
*
|
|
4
4
|
* Implements {@link IVoiceCallProvider} using the Telnyx Call Control v2 API.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* ## REST API contract
|
|
7
|
+
*
|
|
8
|
+
* | Operation | Method | Endpoint | Body format |
|
|
9
|
+
* |----------------|--------|------------------------------------------|-------------|
|
|
10
|
+
* | Initiate call | POST | `/v2/calls` | JSON |
|
|
11
|
+
* | Hangup call | POST | `/v2/calls/{id}/actions/hangup` | JSON |
|
|
12
|
+
* | Play TTS | POST | `/v2/calls/{id}/actions/speak` | JSON |
|
|
13
|
+
* | Start stream | POST | `/v2/calls/{id}/actions/streaming_start` | JSON |
|
|
14
|
+
*
|
|
15
|
+
* All requests use Bearer token authentication: `Authorization: Bearer {apiKey}`.
|
|
16
|
+
* Request bodies are JSON (unlike Twilio's form-encoded convention).
|
|
17
|
+
*
|
|
18
|
+
* ## Streaming after `call.answered`
|
|
19
|
+
*
|
|
20
|
+
* Telnyx requires a two-step flow for media streaming:
|
|
21
|
+
* 1. Initiate the call via `POST /v2/calls` with a `webhook_url`.
|
|
22
|
+
* 2. When the `call.answered` webhook fires, issue a separate
|
|
23
|
+
* `POST /v2/calls/{id}/actions/streaming_start` request with the
|
|
24
|
+
* WebSocket URL. This is handled by the CallManager, not by this provider.
|
|
25
|
+
*
|
|
26
|
+
* ## Webhook verification: Ed25519
|
|
27
|
+
*
|
|
28
|
+
* Telnyx signs webhooks using Ed25519 public key cryptography:
|
|
29
|
+
*
|
|
30
|
+
* 1. Telnyx generates a signed payload: `{timestamp}|{rawBody}`.
|
|
31
|
+
* 2. The signature is computed with the account's Ed25519 private key.
|
|
32
|
+
* 3. The public key is provided as a base64-encoded DER SPKI blob.
|
|
33
|
+
* 4. Headers: `X-Telnyx-Timestamp` (the timestamp) and
|
|
34
|
+
* `X-Telnyx-Signature-Ed25519` (base64-encoded Ed25519 signature).
|
|
35
|
+
* 5. Verification: decode the signature from base64, construct the payload
|
|
36
|
+
* string `{timestamp}|{body}`, and verify using `crypto.verify()` with
|
|
37
|
+
* the SPKI public key.
|
|
38
|
+
*
|
|
39
|
+
* When no public key is configured, verification is skipped (returns
|
|
40
|
+
* `valid: true`) to support development environments.
|
|
41
|
+
*
|
|
42
|
+
* ## Event mapping table (hangup_cause)
|
|
43
|
+
*
|
|
44
|
+
* | Telnyx `event_type` | `hangup_cause` | Normalised `kind` |
|
|
45
|
+
* |----------------------------------|---------------------------|----------------------|
|
|
46
|
+
* | `call.initiated` | -- | `call-ringing` |
|
|
47
|
+
* | `call.answered` | -- | `call-answered` |
|
|
48
|
+
* | `call.hangup` | `normal_clearing` | `call-hangup-user` |
|
|
49
|
+
* | `call.hangup` | `user_busy` | `call-hangup-user` |
|
|
50
|
+
* | `call.hangup` | `originator_cancel` | `call-hangup-user` |
|
|
51
|
+
* | `call.hangup` | (anything else) | `call-completed` |
|
|
52
|
+
* | `call.dtmf.received` | -- | `call-dtmf` |
|
|
53
|
+
* | `call.machine.detection.ended` | result=`machine` | `call-voicemail` |
|
|
54
|
+
* | `call.machine.detection.ended` | result=`human` | (no event) |
|
|
7
55
|
*
|
|
8
56
|
* @module @framers/agentos/voice/providers/telnyx
|
|
9
57
|
*/
|
|
@@ -29,7 +77,11 @@ import { randomUUID } from 'node:crypto';
|
|
|
29
77
|
* ```
|
|
30
78
|
*/
|
|
31
79
|
export class TelnyxVoiceProvider {
|
|
80
|
+
/**
|
|
81
|
+
* @param config - Telnyx credentials and optional overrides.
|
|
82
|
+
*/
|
|
32
83
|
constructor(config) {
|
|
84
|
+
/** Provider identifier, always `'telnyx'`. */
|
|
33
85
|
this.name = 'telnyx';
|
|
34
86
|
this.config = config;
|
|
35
87
|
this.baseUrl = 'https://api.telnyx.com/v2';
|
|
@@ -40,14 +92,24 @@ export class TelnyxVoiceProvider {
|
|
|
40
92
|
/**
|
|
41
93
|
* Verify an incoming Telnyx webhook using Ed25519 signature verification.
|
|
42
94
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
95
|
+
* ## Algorithm (step by step)
|
|
96
|
+
*
|
|
97
|
+
* 1. If no public key is configured, skip verification (return `valid: true`).
|
|
98
|
+
* This supports development environments without cryptographic setup.
|
|
99
|
+
* 2. Extract `X-Telnyx-Timestamp` and `X-Telnyx-Signature-Ed25519` headers.
|
|
100
|
+
* 3. Decode the signature from base64 into a raw byte Buffer.
|
|
101
|
+
* 4. Construct the signed payload: `"{timestamp}|{rawBody}"`.
|
|
102
|
+
* 5. Decode the SPKI public key from base64.
|
|
103
|
+
* 6. Call `crypto.verify(null, payload, { key, format: 'der', type: 'spki' }, signature)`.
|
|
104
|
+
* 7. Return the verification result.
|
|
45
105
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
106
|
+
* @param ctx - Raw webhook request context.
|
|
107
|
+
* @returns Verification result. Returns `{ valid: true }` when no public key
|
|
108
|
+
* is configured (development mode).
|
|
49
109
|
*/
|
|
50
110
|
verifyWebhook(ctx) {
|
|
111
|
+
// Skip verification when no public key is configured -- allows
|
|
112
|
+
// development without needing to set up Ed25519 key pairs.
|
|
51
113
|
if (!this.config.publicKey) {
|
|
52
114
|
return { valid: true };
|
|
53
115
|
}
|
|
@@ -57,9 +119,11 @@ export class TelnyxVoiceProvider {
|
|
|
57
119
|
return { valid: false, error: 'Missing Telnyx signature headers' };
|
|
58
120
|
}
|
|
59
121
|
const signature = Buffer.from(sigHeader, 'base64');
|
|
122
|
+
// Telnyx signs the concatenation of timestamp, pipe separator, and raw body.
|
|
60
123
|
const payload = Buffer.from(`${timestamp}|${ctx.body.toString()}`);
|
|
61
124
|
try {
|
|
62
|
-
const valid = verify(null,
|
|
125
|
+
const valid = verify(null, // Ed25519 does not use a separate hash algorithm parameter.
|
|
126
|
+
payload, {
|
|
63
127
|
key: Buffer.from(this.config.publicKey, 'base64'),
|
|
64
128
|
format: 'der',
|
|
65
129
|
type: 'spki',
|
|
@@ -67,18 +131,27 @@ export class TelnyxVoiceProvider {
|
|
|
67
131
|
return { valid };
|
|
68
132
|
}
|
|
69
133
|
catch {
|
|
134
|
+
// crypto.verify throws on malformed keys or invalid DER encoding.
|
|
70
135
|
return { valid: false, error: 'Verification failed' };
|
|
71
136
|
}
|
|
72
137
|
}
|
|
73
138
|
/**
|
|
74
139
|
* Parse a Telnyx webhook JSON body into normalized {@link NormalizedCallEvent}s.
|
|
75
140
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
141
|
+
* Telnyx sends all webhook payloads as JSON with a `data.event_type`
|
|
142
|
+
* discriminant field. The `data.payload` object contains call-specific
|
|
143
|
+
* fields like `call_control_id`, `hangup_cause`, `digit`, and `result`.
|
|
144
|
+
*
|
|
145
|
+
* ## Hangup cause mapping
|
|
146
|
+
*
|
|
147
|
+
* Telnyx's `call.hangup` event includes a `hangup_cause` field that must
|
|
148
|
+
* be inspected to determine whether the user or the system terminated
|
|
149
|
+
* the call:
|
|
150
|
+
* - `normal_clearing` / `user_busy` / `originator_cancel` -> `call-hangup-user`
|
|
151
|
+
* - All other causes (e.g., `call_rejected`, `unallocated_number`) -> `call-completed`
|
|
152
|
+
*
|
|
153
|
+
* @param ctx - Raw webhook request context.
|
|
154
|
+
* @returns Parsed result containing zero or more normalized events.
|
|
82
155
|
*/
|
|
83
156
|
parseWebhookEvent(ctx) {
|
|
84
157
|
let parsed;
|
|
@@ -92,6 +165,7 @@ export class TelnyxVoiceProvider {
|
|
|
92
165
|
const providerCallId = payload.call_control_id ?? '';
|
|
93
166
|
const timestamp = Date.now();
|
|
94
167
|
const events = [];
|
|
168
|
+
/** Helper: shared base fields with a unique event ID for idempotency. */
|
|
95
169
|
const base = () => ({ eventId: randomUUID(), providerCallId, timestamp });
|
|
96
170
|
switch (event_type) {
|
|
97
171
|
case 'call.initiated':
|
|
@@ -101,7 +175,8 @@ export class TelnyxVoiceProvider {
|
|
|
101
175
|
events.push({ ...base(), kind: 'call-answered' });
|
|
102
176
|
break;
|
|
103
177
|
case 'call.hangup': {
|
|
104
|
-
// Distinguish user-initiated hangup from normal completion
|
|
178
|
+
// Distinguish user-initiated hangup from normal call completion
|
|
179
|
+
// by inspecting the hangup_cause field.
|
|
105
180
|
const cause = payload.hangup_cause ?? '';
|
|
106
181
|
const kind = cause === 'user_busy' || cause === 'normal_clearing' || cause === 'originator_cancel'
|
|
107
182
|
? 'call-hangup-user'
|
|
@@ -110,17 +185,19 @@ export class TelnyxVoiceProvider {
|
|
|
110
185
|
break;
|
|
111
186
|
}
|
|
112
187
|
case 'call.dtmf.received': {
|
|
188
|
+
// DTMF arrives via webhook only (never over the media stream WebSocket).
|
|
113
189
|
const digit = payload.digit ?? '';
|
|
114
190
|
events.push({ ...base(), kind: 'call-dtmf', digit });
|
|
115
191
|
break;
|
|
116
192
|
}
|
|
117
193
|
case 'call.machine.detection.ended':
|
|
194
|
+
// Only emit voicemail for machine detection; human detection is a no-op.
|
|
118
195
|
if (payload.result === 'machine') {
|
|
119
196
|
events.push({ ...base(), kind: 'call-voicemail' });
|
|
120
197
|
}
|
|
121
198
|
break;
|
|
122
199
|
default:
|
|
123
|
-
// Unrecognized event type
|
|
200
|
+
// Unrecognized event type -- silently ignore for forward-compatibility.
|
|
124
201
|
break;
|
|
125
202
|
}
|
|
126
203
|
return { events };
|
|
@@ -129,9 +206,15 @@ export class TelnyxVoiceProvider {
|
|
|
129
206
|
/**
|
|
130
207
|
* Initiate an outbound call via the Telnyx Call Control v2 API.
|
|
131
208
|
*
|
|
132
|
-
* POSTs to `/calls` with a JSON body
|
|
133
|
-
*
|
|
134
|
-
*
|
|
209
|
+
* POSTs to `/v2/calls` with a JSON body containing the `connection_id`,
|
|
210
|
+
* phone numbers, and webhook URL. The `mediaStreamUrl` (if provided) is
|
|
211
|
+
* stored internally for use after the call is answered -- it is NOT sent
|
|
212
|
+
* in the initial call creation request because Telnyx requires
|
|
213
|
+
* `streaming_start` to be issued as a separate action after `call.answered`.
|
|
214
|
+
*
|
|
215
|
+
* @param input - Call initiation parameters (from/to numbers, webhook URL).
|
|
216
|
+
* @returns Result containing the Telnyx `call_control_id` on success.
|
|
217
|
+
* @throws Never throws; returns `{ success: false, error: '...' }` on failure.
|
|
135
218
|
*/
|
|
136
219
|
async initiateCall(input) {
|
|
137
220
|
const url = `${this.baseUrl}/calls`;
|
|
@@ -157,6 +240,11 @@ export class TelnyxVoiceProvider {
|
|
|
157
240
|
}
|
|
158
241
|
/**
|
|
159
242
|
* Hang up an active call via the Telnyx Call Control hangup action.
|
|
243
|
+
*
|
|
244
|
+
* POSTs an empty JSON body to `/v2/calls/{call_control_id}/actions/hangup`.
|
|
245
|
+
* Telnyx will terminate the call and fire a `call.hangup` webhook.
|
|
246
|
+
*
|
|
247
|
+
* @param input - Contains the Telnyx `call_control_id` to hang up.
|
|
160
248
|
*/
|
|
161
249
|
async hangupCall(input) {
|
|
162
250
|
const url = `${this.baseUrl}/calls/${input.providerCallId}/actions/hangup`;
|
|
@@ -172,7 +260,10 @@ export class TelnyxVoiceProvider {
|
|
|
172
260
|
/**
|
|
173
261
|
* Speak text into a live call using Telnyx's text-to-speech speak action.
|
|
174
262
|
*
|
|
175
|
-
*
|
|
263
|
+
* POSTs a JSON body to `/v2/calls/{id}/actions/speak` with the text
|
|
264
|
+
* `payload`, `voice` (default `'female'`), and `language` (default `'en-US'`).
|
|
265
|
+
*
|
|
266
|
+
* @param input - TTS parameters (text, optional voice, call ID).
|
|
176
267
|
*/
|
|
177
268
|
async playTts(input) {
|
|
178
269
|
const url = `${this.baseUrl}/calls/${input.providerCallId}/actions/speak`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telnyx.js","sourceRoot":"","sources":["../../../src/voice/providers/telnyx.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"telnyx.js","sourceRoot":"","sources":["../../../src/voice/providers/telnyx.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,20 +2,60 @@
|
|
|
2
2
|
* @fileoverview Twilio telephony provider for AgentOS voice calls.
|
|
3
3
|
*
|
|
4
4
|
* Implements {@link IVoiceCallProvider} using the Twilio REST API v2010-04-01.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* ## REST API contract
|
|
7
|
+
*
|
|
8
|
+
* | Operation | Method | Endpoint | Body format |
|
|
9
|
+
* |----------------|--------|---------------------------------------------------|-----------------|
|
|
10
|
+
* | Initiate call | POST | `/2010-04-01/Accounts/{sid}/Calls.json` | form-encoded |
|
|
11
|
+
* | Hangup call | POST | `/2010-04-01/Accounts/{sid}/Calls/{callSid}.json` | form-encoded |
|
|
12
|
+
* | Play TTS | POST | `/2010-04-01/Accounts/{sid}/Calls/{callSid}.json` | form-encoded |
|
|
13
|
+
*
|
|
14
|
+
* All requests use HTTP Basic authentication: `Authorization: Basic base64(accountSid:authToken)`.
|
|
15
|
+
* Request bodies are `application/x-www-form-urlencoded` (not JSON), which is
|
|
16
|
+
* Twilio's legacy convention for the 2010-04-01 API.
|
|
17
|
+
*
|
|
18
|
+
* ## Webhook verification: HMAC-SHA1
|
|
19
|
+
*
|
|
20
|
+
* Twilio signs every webhook request using HMAC-SHA1. The verification algorithm:
|
|
21
|
+
*
|
|
22
|
+
* 1. Start with the **full request URL** (including scheme, host, path, and any query string).
|
|
23
|
+
* 2. Parse the POST body as form-encoded key-value pairs.
|
|
24
|
+
* 3. Sort the parameters **alphabetically by key name**.
|
|
25
|
+
* 4. Concatenate each key+value pair (no separator) directly to the URL string.
|
|
26
|
+
* 5. Compute `HMAC-SHA1(authToken, concatenatedString)`.
|
|
27
|
+
* 6. Base64-encode the HMAC digest.
|
|
28
|
+
* 7. Compare the result with the `X-Twilio-Signature` request header.
|
|
29
|
+
*
|
|
30
|
+
* If the computed signature matches the header, the request is authentic.
|
|
31
|
+
*
|
|
32
|
+
* ## Event mapping table
|
|
33
|
+
*
|
|
34
|
+
* | Twilio `CallStatus` | Normalised `kind` |
|
|
35
|
+
* |---------------------|----------------------|
|
|
36
|
+
* | `ringing` | `call-ringing` |
|
|
37
|
+
* | `in-progress` | `call-answered` |
|
|
38
|
+
* | `completed` | `call-completed` |
|
|
39
|
+
* | `failed` | `call-failed` |
|
|
40
|
+
* | `busy` | `call-busy` |
|
|
41
|
+
* | `no-answer` | `call-no-answer` |
|
|
42
|
+
* | `canceled` | `call-hangup-user` |
|
|
43
|
+
* | (+ `Digits` param) | `call-dtmf` |
|
|
6
44
|
*
|
|
7
45
|
* @module @framers/agentos/voice/providers/twilio
|
|
8
46
|
*/
|
|
9
47
|
import type { IVoiceCallProvider, InitiateCallInput, InitiateCallResult, HangupCallInput, PlayTtsInput } from '../IVoiceCallProvider.js';
|
|
10
48
|
import type { WebhookContext, WebhookVerificationResult, WebhookParseResult } from '../types.js';
|
|
11
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for {@link TwilioVoiceProvider}.
|
|
51
|
+
*/
|
|
12
52
|
export interface TwilioVoiceProviderConfig {
|
|
13
53
|
/** Twilio Account SID (starts with "AC"). */
|
|
14
54
|
accountSid: string;
|
|
15
|
-
/** Twilio Auth Token. */
|
|
55
|
+
/** Twilio Auth Token (used for both API auth and webhook HMAC verification). */
|
|
16
56
|
authToken: string;
|
|
17
57
|
/**
|
|
18
|
-
* Optional fetch override
|
|
58
|
+
* Optional fetch implementation override -- inject a mock in tests.
|
|
19
59
|
* Defaults to the global `fetch`.
|
|
20
60
|
*/
|
|
21
61
|
fetchImpl?: typeof fetch;
|
|
@@ -35,44 +75,82 @@ export interface TwilioVoiceProviderConfig {
|
|
|
35
75
|
* ```
|
|
36
76
|
*/
|
|
37
77
|
export declare class TwilioVoiceProvider implements IVoiceCallProvider {
|
|
78
|
+
/** Provider identifier, always `'twilio'`. */
|
|
38
79
|
readonly name: "twilio";
|
|
80
|
+
/** Immutable configuration snapshot. */
|
|
39
81
|
private readonly config;
|
|
82
|
+
/** Base URL for the Twilio REST API (2010-04-01 version). */
|
|
40
83
|
private readonly baseUrl;
|
|
84
|
+
/** Pre-computed `Authorization: Basic ...` header value. */
|
|
41
85
|
private readonly authHeader;
|
|
86
|
+
/** HTTP fetch implementation (injectable for testing). */
|
|
42
87
|
private readonly fetch;
|
|
88
|
+
/**
|
|
89
|
+
* @param config - Twilio credentials and optional overrides.
|
|
90
|
+
*/
|
|
43
91
|
constructor(config: TwilioVoiceProviderConfig);
|
|
44
92
|
/**
|
|
45
93
|
* Verify an incoming Twilio webhook request using HMAC-SHA1.
|
|
46
94
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
95
|
+
* ## Algorithm (step by step)
|
|
96
|
+
*
|
|
97
|
+
* 1. Extract the `X-Twilio-Signature` header from the request.
|
|
98
|
+
* 2. Parse the request body as URL-encoded form data.
|
|
99
|
+
* 3. Sort all key-value pairs alphabetically by key.
|
|
100
|
+
* 4. Build the signed string: start with the full URL, then append each
|
|
101
|
+
* key + value (no delimiters between pairs).
|
|
102
|
+
* 5. Compute `HMAC-SHA1` of the signed string using the auth token as the key.
|
|
103
|
+
* 6. Base64-encode the digest and compare it to the header value.
|
|
104
|
+
*
|
|
105
|
+
* @param ctx - Raw webhook request context.
|
|
106
|
+
* @returns Verification result with `valid: true` if the signature matches.
|
|
49
107
|
*/
|
|
50
108
|
verifyWebhook(ctx: WebhookContext): WebhookVerificationResult;
|
|
51
109
|
/**
|
|
52
110
|
* Parse a Twilio webhook body into normalized {@link NormalizedCallEvent}s.
|
|
53
111
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
112
|
+
* Twilio sends webhooks with a form-encoded body containing `CallSid`,
|
|
113
|
+
* `CallStatus`, and optionally `Digits` (for DTMF input from `<Gather>`).
|
|
114
|
+
* Each webhook may produce one or two events (status + optional DTMF).
|
|
115
|
+
*
|
|
116
|
+
* @param ctx - Raw webhook request context.
|
|
117
|
+
* @returns Parsed result containing zero or more normalized events.
|
|
56
118
|
*/
|
|
57
119
|
parseWebhookEvent(ctx: WebhookContext): WebhookParseResult;
|
|
58
120
|
/**
|
|
59
121
|
* Initiate an outbound call via the Twilio Calls API.
|
|
60
122
|
*
|
|
61
|
-
* Posts to `/Accounts/{accountSid}/Calls.json` with form-encoded body
|
|
62
|
-
*
|
|
63
|
-
* are
|
|
123
|
+
* Posts to `/Accounts/{accountSid}/Calls.json` with a **form-encoded** body
|
|
124
|
+
* (not JSON -- this is Twilio's 2010-era API convention). All four status
|
|
125
|
+
* callback events (`initiated`, `ringing`, `answered`, `completed`) are
|
|
126
|
+
* requested so the {@link CallManager} receives the full state progression.
|
|
127
|
+
*
|
|
128
|
+
* @param input - Call initiation parameters (from/to numbers, webhook URLs).
|
|
129
|
+
* @returns Result containing the Twilio `CallSid` on success.
|
|
130
|
+
* @throws Never throws; returns `{ success: false, error: '...' }` on failure.
|
|
64
131
|
*/
|
|
65
132
|
initiateCall(input: InitiateCallInput): Promise<InitiateCallResult>;
|
|
66
133
|
/**
|
|
67
|
-
* Hang up an active call by
|
|
134
|
+
* Hang up an active call by POSTing `Status=completed`.
|
|
135
|
+
*
|
|
136
|
+
* Twilio uses the same Calls resource endpoint for both querying and
|
|
137
|
+
* modifying a call. Setting `Status=completed` instructs Twilio to
|
|
138
|
+
* immediately terminate the call.
|
|
139
|
+
*
|
|
140
|
+
* @param input - Contains the Twilio `CallSid` to hang up.
|
|
68
141
|
*/
|
|
69
142
|
hangupCall(input: HangupCallInput): Promise<void>;
|
|
70
143
|
/**
|
|
71
144
|
* Inject TTS into a live call using a TwiML `<Say>` verb.
|
|
72
145
|
*
|
|
73
|
-
* Sends a `Twiml` parameter containing a minimal `<Response><Say>`
|
|
146
|
+
* Sends a `Twiml` form parameter containing a minimal `<Response><Say>`
|
|
147
|
+
* document. Twilio will parse the TwiML, synthesise the speech, and play
|
|
148
|
+
* it to the caller in real-time.
|
|
149
|
+
*
|
|
74
150
|
* The optional `voice` attribute maps to Twilio's built-in voice names
|
|
75
151
|
* (e.g., `alice`, `Polly.Joanna`).
|
|
152
|
+
*
|
|
153
|
+
* @param input - TTS parameters (text, voice, call ID).
|
|
76
154
|
*/
|
|
77
155
|
playTts(input: PlayTtsInput): Promise<void>;
|
|
78
156
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twilio.d.ts","sourceRoot":"","sources":["../../../src/voice/providers/twilio.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"twilio.d.ts","sourceRoot":"","sources":["../../../src/voice/providers/twilio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAKH,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,cAAc,EACd,yBAAyB,EACzB,kBAAkB,EAEnB,MAAM,aAAa,CAAC;AAMrB;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAMD;;;;;;;;;;;;;GAaG;AACH,qBAAa,mBAAoB,YAAW,kBAAkB;IAC5D,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,wCAAwC;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IAEnD,6DAA6D;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IAErC;;OAEG;gBACS,MAAM,EAAE,yBAAyB;IAY7C;;;;;;;;;;;;;;;OAeG;IACH,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,yBAAyB;IA0B7D;;;;;;;;;OASG;IACH,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,kBAAkB;IA8D1D;;;;;;;;;;;OAWG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAiCzE;;;;;;;;OAQG;IACG,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAavD;;;;;;;;;;;OAWG;IACG,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAelD"}
|
|
@@ -2,7 +2,45 @@
|
|
|
2
2
|
* @fileoverview Twilio telephony provider for AgentOS voice calls.
|
|
3
3
|
*
|
|
4
4
|
* Implements {@link IVoiceCallProvider} using the Twilio REST API v2010-04-01.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* ## REST API contract
|
|
7
|
+
*
|
|
8
|
+
* | Operation | Method | Endpoint | Body format |
|
|
9
|
+
* |----------------|--------|---------------------------------------------------|-----------------|
|
|
10
|
+
* | Initiate call | POST | `/2010-04-01/Accounts/{sid}/Calls.json` | form-encoded |
|
|
11
|
+
* | Hangup call | POST | `/2010-04-01/Accounts/{sid}/Calls/{callSid}.json` | form-encoded |
|
|
12
|
+
* | Play TTS | POST | `/2010-04-01/Accounts/{sid}/Calls/{callSid}.json` | form-encoded |
|
|
13
|
+
*
|
|
14
|
+
* All requests use HTTP Basic authentication: `Authorization: Basic base64(accountSid:authToken)`.
|
|
15
|
+
* Request bodies are `application/x-www-form-urlencoded` (not JSON), which is
|
|
16
|
+
* Twilio's legacy convention for the 2010-04-01 API.
|
|
17
|
+
*
|
|
18
|
+
* ## Webhook verification: HMAC-SHA1
|
|
19
|
+
*
|
|
20
|
+
* Twilio signs every webhook request using HMAC-SHA1. The verification algorithm:
|
|
21
|
+
*
|
|
22
|
+
* 1. Start with the **full request URL** (including scheme, host, path, and any query string).
|
|
23
|
+
* 2. Parse the POST body as form-encoded key-value pairs.
|
|
24
|
+
* 3. Sort the parameters **alphabetically by key name**.
|
|
25
|
+
* 4. Concatenate each key+value pair (no separator) directly to the URL string.
|
|
26
|
+
* 5. Compute `HMAC-SHA1(authToken, concatenatedString)`.
|
|
27
|
+
* 6. Base64-encode the HMAC digest.
|
|
28
|
+
* 7. Compare the result with the `X-Twilio-Signature` request header.
|
|
29
|
+
*
|
|
30
|
+
* If the computed signature matches the header, the request is authentic.
|
|
31
|
+
*
|
|
32
|
+
* ## Event mapping table
|
|
33
|
+
*
|
|
34
|
+
* | Twilio `CallStatus` | Normalised `kind` |
|
|
35
|
+
* |---------------------|----------------------|
|
|
36
|
+
* | `ringing` | `call-ringing` |
|
|
37
|
+
* | `in-progress` | `call-answered` |
|
|
38
|
+
* | `completed` | `call-completed` |
|
|
39
|
+
* | `failed` | `call-failed` |
|
|
40
|
+
* | `busy` | `call-busy` |
|
|
41
|
+
* | `no-answer` | `call-no-answer` |
|
|
42
|
+
* | `canceled` | `call-hangup-user` |
|
|
43
|
+
* | (+ `Digits` param) | `call-dtmf` |
|
|
6
44
|
*
|
|
7
45
|
* @module @framers/agentos/voice/providers/twilio
|
|
8
46
|
*/
|
|
@@ -26,10 +64,15 @@ import { randomUUID } from 'node:crypto';
|
|
|
26
64
|
* ```
|
|
27
65
|
*/
|
|
28
66
|
export class TwilioVoiceProvider {
|
|
67
|
+
/**
|
|
68
|
+
* @param config - Twilio credentials and optional overrides.
|
|
69
|
+
*/
|
|
29
70
|
constructor(config) {
|
|
71
|
+
/** Provider identifier, always `'twilio'`. */
|
|
30
72
|
this.name = 'twilio';
|
|
31
73
|
this.config = config;
|
|
32
74
|
this.baseUrl = 'https://api.twilio.com/2010-04-01';
|
|
75
|
+
// Twilio uses HTTP Basic auth with accountSid:authToken.
|
|
33
76
|
this.authHeader =
|
|
34
77
|
'Basic ' +
|
|
35
78
|
Buffer.from(`${config.accountSid}:${config.authToken}`).toString('base64');
|
|
@@ -39,21 +82,32 @@ export class TwilioVoiceProvider {
|
|
|
39
82
|
/**
|
|
40
83
|
* Verify an incoming Twilio webhook request using HMAC-SHA1.
|
|
41
84
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
85
|
+
* ## Algorithm (step by step)
|
|
86
|
+
*
|
|
87
|
+
* 1. Extract the `X-Twilio-Signature` header from the request.
|
|
88
|
+
* 2. Parse the request body as URL-encoded form data.
|
|
89
|
+
* 3. Sort all key-value pairs alphabetically by key.
|
|
90
|
+
* 4. Build the signed string: start with the full URL, then append each
|
|
91
|
+
* key + value (no delimiters between pairs).
|
|
92
|
+
* 5. Compute `HMAC-SHA1` of the signed string using the auth token as the key.
|
|
93
|
+
* 6. Base64-encode the digest and compare it to the header value.
|
|
94
|
+
*
|
|
95
|
+
* @param ctx - Raw webhook request context.
|
|
96
|
+
* @returns Verification result with `valid: true` if the signature matches.
|
|
44
97
|
*/
|
|
45
98
|
verifyWebhook(ctx) {
|
|
46
99
|
const signature = ctx.headers['x-twilio-signature'];
|
|
47
100
|
if (!signature || Array.isArray(signature)) {
|
|
48
101
|
return { valid: false, error: 'Missing x-twilio-signature header' };
|
|
49
102
|
}
|
|
50
|
-
//
|
|
103
|
+
// Step 2-4: Parse form body, sort params, build signed data string.
|
|
51
104
|
const bodyParams = new URLSearchParams(ctx.body.toString());
|
|
52
105
|
const sorted = [...bodyParams.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
53
106
|
let data = ctx.url;
|
|
54
107
|
for (const [key, value] of sorted) {
|
|
55
108
|
data += key + value;
|
|
56
109
|
}
|
|
110
|
+
// Step 5-6: HMAC-SHA1 with auth token, compare base64 digest.
|
|
57
111
|
const expected = createHmac('sha1', this.config.authToken)
|
|
58
112
|
.update(data)
|
|
59
113
|
.digest('base64');
|
|
@@ -66,8 +120,12 @@ export class TwilioVoiceProvider {
|
|
|
66
120
|
/**
|
|
67
121
|
* Parse a Twilio webhook body into normalized {@link NormalizedCallEvent}s.
|
|
68
122
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
123
|
+
* Twilio sends webhooks with a form-encoded body containing `CallSid`,
|
|
124
|
+
* `CallStatus`, and optionally `Digits` (for DTMF input from `<Gather>`).
|
|
125
|
+
* Each webhook may produce one or two events (status + optional DTMF).
|
|
126
|
+
*
|
|
127
|
+
* @param ctx - Raw webhook request context.
|
|
128
|
+
* @returns Parsed result containing zero or more normalized events.
|
|
71
129
|
*/
|
|
72
130
|
parseWebhookEvent(ctx) {
|
|
73
131
|
const params = new URLSearchParams(ctx.body.toString());
|
|
@@ -76,12 +134,13 @@ export class TwilioVoiceProvider {
|
|
|
76
134
|
const digits = params.get('Digits');
|
|
77
135
|
const timestamp = Date.now();
|
|
78
136
|
const events = [];
|
|
79
|
-
/** Helper: shared base fields. */
|
|
137
|
+
/** Helper: shared base fields with a unique event ID for idempotency. */
|
|
80
138
|
const base = () => ({
|
|
81
139
|
eventId: randomUUID(),
|
|
82
140
|
providerCallId: callSid,
|
|
83
141
|
timestamp,
|
|
84
142
|
});
|
|
143
|
+
// Map Twilio CallStatus values to normalized event kinds.
|
|
85
144
|
switch (callStatus) {
|
|
86
145
|
case 'ringing':
|
|
87
146
|
events.push({ ...base(), kind: 'call-ringing' });
|
|
@@ -102,13 +161,17 @@ export class TwilioVoiceProvider {
|
|
|
102
161
|
events.push({ ...base(), kind: 'call-no-answer' });
|
|
103
162
|
break;
|
|
104
163
|
case 'canceled':
|
|
164
|
+
// Twilio uses "canceled" when the caller hangs up before the callee answers.
|
|
105
165
|
events.push({ ...base(), kind: 'call-hangup-user' });
|
|
106
166
|
break;
|
|
107
167
|
default:
|
|
108
|
-
// initiated / queued / etc.
|
|
168
|
+
// initiated / queued / etc. -- no normalized event emitted.
|
|
169
|
+
// These are transient Twilio-internal states that don't map to
|
|
170
|
+
// meaningful call lifecycle events.
|
|
109
171
|
break;
|
|
110
172
|
}
|
|
111
|
-
// DTMF digit input
|
|
173
|
+
// DTMF digit input (from <Gather> TwiML verb callback).
|
|
174
|
+
// This can co-occur with a CallStatus update in the same webhook.
|
|
112
175
|
if (digits != null && digits !== '') {
|
|
113
176
|
events.push({
|
|
114
177
|
...base(),
|
|
@@ -122,12 +185,18 @@ export class TwilioVoiceProvider {
|
|
|
122
185
|
/**
|
|
123
186
|
* Initiate an outbound call via the Twilio Calls API.
|
|
124
187
|
*
|
|
125
|
-
* Posts to `/Accounts/{accountSid}/Calls.json` with form-encoded body
|
|
126
|
-
*
|
|
127
|
-
* are
|
|
188
|
+
* Posts to `/Accounts/{accountSid}/Calls.json` with a **form-encoded** body
|
|
189
|
+
* (not JSON -- this is Twilio's 2010-era API convention). All four status
|
|
190
|
+
* callback events (`initiated`, `ringing`, `answered`, `completed`) are
|
|
191
|
+
* requested so the {@link CallManager} receives the full state progression.
|
|
192
|
+
*
|
|
193
|
+
* @param input - Call initiation parameters (from/to numbers, webhook URLs).
|
|
194
|
+
* @returns Result containing the Twilio `CallSid` on success.
|
|
195
|
+
* @throws Never throws; returns `{ success: false, error: '...' }` on failure.
|
|
128
196
|
*/
|
|
129
197
|
async initiateCall(input) {
|
|
130
198
|
const url = `${this.baseUrl}/Accounts/${this.config.accountSid}/Calls.json`;
|
|
199
|
+
// Build form-encoded body. Twilio expects this format, not JSON.
|
|
131
200
|
const body = [
|
|
132
201
|
`To=${encodeURIComponent(input.toNumber)}`,
|
|
133
202
|
`From=${encodeURIComponent(input.fromNumber)}`,
|
|
@@ -154,7 +223,13 @@ export class TwilioVoiceProvider {
|
|
|
154
223
|
return { providerCallId: data.sid, success: true };
|
|
155
224
|
}
|
|
156
225
|
/**
|
|
157
|
-
* Hang up an active call by
|
|
226
|
+
* Hang up an active call by POSTing `Status=completed`.
|
|
227
|
+
*
|
|
228
|
+
* Twilio uses the same Calls resource endpoint for both querying and
|
|
229
|
+
* modifying a call. Setting `Status=completed` instructs Twilio to
|
|
230
|
+
* immediately terminate the call.
|
|
231
|
+
*
|
|
232
|
+
* @param input - Contains the Twilio `CallSid` to hang up.
|
|
158
233
|
*/
|
|
159
234
|
async hangupCall(input) {
|
|
160
235
|
const url = `${this.baseUrl}/Accounts/${this.config.accountSid}/Calls/${input.providerCallId}.json`;
|
|
@@ -170,9 +245,14 @@ export class TwilioVoiceProvider {
|
|
|
170
245
|
/**
|
|
171
246
|
* Inject TTS into a live call using a TwiML `<Say>` verb.
|
|
172
247
|
*
|
|
173
|
-
* Sends a `Twiml` parameter containing a minimal `<Response><Say>`
|
|
248
|
+
* Sends a `Twiml` form parameter containing a minimal `<Response><Say>`
|
|
249
|
+
* document. Twilio will parse the TwiML, synthesise the speech, and play
|
|
250
|
+
* it to the caller in real-time.
|
|
251
|
+
*
|
|
174
252
|
* The optional `voice` attribute maps to Twilio's built-in voice names
|
|
175
253
|
* (e.g., `alice`, `Polly.Joanna`).
|
|
254
|
+
*
|
|
255
|
+
* @param input - TTS parameters (text, voice, call ID).
|
|
176
256
|
*/
|
|
177
257
|
async playTts(input) {
|
|
178
258
|
const url = `${this.baseUrl}/Accounts/${this.config.accountSid}/Calls/${input.providerCallId}.json`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twilio.js","sourceRoot":"","sources":["../../../src/voice/providers/twilio.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"twilio.js","sourceRoot":"","sources":["../../../src/voice/providers/twilio.ts"],"names":[],"mappings":"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"}
|