@sinch/functions-runtime 0.1.0-beta.28 → 0.2.1-beta
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 +166 -207
- package/dist/bin/sinch-runtime.js +454 -6
- package/dist/bin/sinch-runtime.js.map +1 -1
- package/dist/index.d.ts +91 -5
- package/dist/index.js +343 -71
- package/dist/index.js.map +1 -1
- package/package.json +5 -13
package/dist/index.js
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
|
+
// ../runtime-shared/dist/ai/connect-agent.js
|
|
2
|
+
var AgentProvider;
|
|
3
|
+
(function(AgentProvider2) {
|
|
4
|
+
AgentProvider2["ElevenLabs"] = "elevenlabs";
|
|
5
|
+
})(AgentProvider || (AgentProvider = {}));
|
|
6
|
+
var AgentSipHelper = class {
|
|
7
|
+
/**
|
|
8
|
+
* Build a SIP URI for connecting to an ElevenLabs agent
|
|
9
|
+
*/
|
|
10
|
+
static buildElevenLabsSipUri(agentId, options) {
|
|
11
|
+
const baseUri = `sip:${agentId}@sip.elevenlabs.io`;
|
|
12
|
+
if (options?.customHeaders && Object.keys(options.customHeaders).length > 0) {
|
|
13
|
+
const headers = Object.entries(options.customHeaders).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
14
|
+
return `${baseUri};${headers}`;
|
|
15
|
+
}
|
|
16
|
+
return baseUri;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build a SIP URI for the given provider and agent
|
|
20
|
+
*/
|
|
21
|
+
static buildSipUri(provider, agentId, options) {
|
|
22
|
+
switch (provider) {
|
|
23
|
+
case AgentProvider.ElevenLabs:
|
|
24
|
+
return this.buildElevenLabsSipUri(agentId, options);
|
|
25
|
+
default:
|
|
26
|
+
throw new Error(`Unsupported agent provider: ${provider}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function createConnectAgentAction(provider, agentId, options) {
|
|
31
|
+
const sipUri = AgentSipHelper.buildSipUri(provider, agentId, options);
|
|
32
|
+
const action = {
|
|
33
|
+
name: "connectSip",
|
|
34
|
+
destination: {
|
|
35
|
+
endpoint: sipUri
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (options?.cli) {
|
|
39
|
+
action.cli = options.cli;
|
|
40
|
+
}
|
|
41
|
+
if (options?.maxDuration !== void 0) {
|
|
42
|
+
action.maxDuration = options.maxDuration;
|
|
43
|
+
}
|
|
44
|
+
if (options?.suppressCallbacks !== void 0) {
|
|
45
|
+
action.suppressCallbacks = options.suppressCallbacks;
|
|
46
|
+
}
|
|
47
|
+
return action;
|
|
48
|
+
}
|
|
49
|
+
|
|
1
50
|
// ../runtime-shared/dist/builders/svaml.js
|
|
2
51
|
var BaseSvamlBuilder = class {
|
|
3
52
|
instructions = [];
|
|
@@ -201,6 +250,25 @@ var IceSvamlBuilder = class extends BaseSvamlBuilder {
|
|
|
201
250
|
};
|
|
202
251
|
return this;
|
|
203
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Connect to an AI voice agent
|
|
255
|
+
*
|
|
256
|
+
* @param provider - The AI agent provider (e.g., AgentProvider.ElevenLabs)
|
|
257
|
+
* @param agentId - The agent ID to connect to
|
|
258
|
+
* @param options - Connection options
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* return new IceSvamlBuilder()
|
|
263
|
+
* .say('Connecting you to our AI assistant.')
|
|
264
|
+
* .connectAgent(AgentProvider.ElevenLabs, 'agent_123')
|
|
265
|
+
* .build();
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
connectAgent(provider, agentId, options) {
|
|
269
|
+
this.action = createConnectAgentAction(provider, agentId, options);
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
204
272
|
};
|
|
205
273
|
var AceSvamlBuilder = class extends BaseSvamlBuilder {
|
|
206
274
|
};
|
|
@@ -250,6 +318,28 @@ var PieSvamlBuilder = class extends BaseSvamlBuilder {
|
|
|
250
318
|
};
|
|
251
319
|
return this;
|
|
252
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Connect to an AI voice agent
|
|
323
|
+
*
|
|
324
|
+
* @param provider - The AI agent provider (e.g., AgentProvider.ElevenLabs)
|
|
325
|
+
* @param agentId - The agent ID to connect to
|
|
326
|
+
* @param options - Connection options
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```typescript
|
|
330
|
+
* // Transfer to AI agent based on menu selection
|
|
331
|
+
* if (event.menuResult?.value === 'ai') {
|
|
332
|
+
* return new PieSvamlBuilder()
|
|
333
|
+
* .say('Connecting you to our AI assistant.')
|
|
334
|
+
* .connectAgent(AgentProvider.ElevenLabs, 'agent_123')
|
|
335
|
+
* .build();
|
|
336
|
+
* }
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
connectAgent(provider, agentId, options) {
|
|
340
|
+
this.action = createConnectAgentAction(provider, agentId, options);
|
|
341
|
+
return this;
|
|
342
|
+
}
|
|
253
343
|
};
|
|
254
344
|
function createIceBuilder() {
|
|
255
345
|
return new IceSvamlBuilder();
|
|
@@ -1161,30 +1251,9 @@ function setupRequestHandler(app, options = {}) {
|
|
|
1161
1251
|
}
|
|
1162
1252
|
|
|
1163
1253
|
// ../runtime-shared/dist/sinch/index.js
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
async function loadSinchSdk() {
|
|
1167
|
-
if (SinchClient !== null) {
|
|
1168
|
-
return SinchClient !== false;
|
|
1169
|
-
}
|
|
1170
|
-
try {
|
|
1171
|
-
const sdk = await import("@sinch/sdk-core");
|
|
1172
|
-
SinchClient = sdk.SinchClient;
|
|
1173
|
-
validateAuthenticationHeader = sdk.validateAuthenticationHeader;
|
|
1174
|
-
return true;
|
|
1175
|
-
} catch {
|
|
1176
|
-
SinchClient = false;
|
|
1177
|
-
validateAuthenticationHeader = false;
|
|
1178
|
-
console.debug("[SINCH] SDK not available - Sinch API features disabled");
|
|
1179
|
-
return false;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
async function createSinchClients() {
|
|
1254
|
+
import { SinchClient, validateAuthenticationHeader } from "@sinch/sdk-core";
|
|
1255
|
+
function createSinchClients() {
|
|
1183
1256
|
const clients = {};
|
|
1184
|
-
const sdkAvailable = await loadSinchSdk();
|
|
1185
|
-
if (!sdkAvailable) {
|
|
1186
|
-
return clients;
|
|
1187
|
-
}
|
|
1188
1257
|
const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
|
|
1189
1258
|
if (!hasCredentials) {
|
|
1190
1259
|
return clients;
|
|
@@ -1193,8 +1262,7 @@ async function createSinchClients() {
|
|
|
1193
1262
|
const sinchClient = new SinchClient({
|
|
1194
1263
|
projectId: process.env.PROJECT_ID,
|
|
1195
1264
|
keyId: process.env.PROJECT_ID_API_KEY,
|
|
1196
|
-
keySecret: process.env.PROJECT_ID_API_SECRET
|
|
1197
|
-
region: process.env.SINCH_REGION || "us"
|
|
1265
|
+
keySecret: process.env.PROJECT_ID_API_SECRET
|
|
1198
1266
|
});
|
|
1199
1267
|
if (process.env.CONVERSATION_APP_ID) {
|
|
1200
1268
|
clients.conversation = sinchClient.conversation;
|
|
@@ -1206,8 +1274,7 @@ async function createSinchClients() {
|
|
|
1206
1274
|
keyId: process.env.PROJECT_ID_API_KEY,
|
|
1207
1275
|
keySecret: process.env.PROJECT_ID_API_SECRET,
|
|
1208
1276
|
applicationKey: process.env.VOICE_APPLICATION_KEY,
|
|
1209
|
-
applicationSecret: process.env.VOICE_APPLICATION_SECRET
|
|
1210
|
-
region: process.env.SINCH_REGION || "us"
|
|
1277
|
+
applicationSecret: process.env.VOICE_APPLICATION_SECRET
|
|
1211
1278
|
});
|
|
1212
1279
|
clients.voice = voiceClient.voice;
|
|
1213
1280
|
console.log("[SINCH] Voice API initialized with application credentials");
|
|
@@ -1224,7 +1291,7 @@ async function createSinchClients() {
|
|
|
1224
1291
|
console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
|
|
1225
1292
|
return {};
|
|
1226
1293
|
}
|
|
1227
|
-
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET
|
|
1294
|
+
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
1228
1295
|
clients.validateWebhookSignature = (requestData) => {
|
|
1229
1296
|
console.log("[SINCH] Validating Voice webhook signature");
|
|
1230
1297
|
try {
|
|
@@ -1240,24 +1307,72 @@ async function createSinchClients() {
|
|
|
1240
1307
|
return clients;
|
|
1241
1308
|
}
|
|
1242
1309
|
var cachedClients = null;
|
|
1243
|
-
var initPromise = null;
|
|
1244
1310
|
function getSinchClients() {
|
|
1245
|
-
if (cachedClients) {
|
|
1246
|
-
|
|
1311
|
+
if (!cachedClients) {
|
|
1312
|
+
cachedClients = createSinchClients();
|
|
1247
1313
|
}
|
|
1248
|
-
|
|
1249
|
-
initPromise = createSinchClients().then((clients) => {
|
|
1250
|
-
cachedClients = clients;
|
|
1251
|
-
return clients;
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
return {};
|
|
1314
|
+
return cachedClients;
|
|
1255
1315
|
}
|
|
1256
1316
|
function resetSinchClients() {
|
|
1257
1317
|
cachedClients = null;
|
|
1258
|
-
initPromise = null;
|
|
1259
1318
|
}
|
|
1260
1319
|
|
|
1320
|
+
// ../runtime-shared/dist/ai/elevenlabs/state.js
|
|
1321
|
+
var ElevenLabsStateManager = class {
|
|
1322
|
+
state = {
|
|
1323
|
+
isConfigured: false
|
|
1324
|
+
};
|
|
1325
|
+
/**
|
|
1326
|
+
* Get the current state
|
|
1327
|
+
*/
|
|
1328
|
+
getState() {
|
|
1329
|
+
return { ...this.state };
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Check if ElevenLabs is configured
|
|
1333
|
+
*/
|
|
1334
|
+
isConfigured() {
|
|
1335
|
+
return this.state.isConfigured;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Update state with auto-configuration results
|
|
1339
|
+
*/
|
|
1340
|
+
setConfigured(data) {
|
|
1341
|
+
this.state = {
|
|
1342
|
+
...data,
|
|
1343
|
+
isConfigured: true,
|
|
1344
|
+
configuredAt: /* @__PURE__ */ new Date()
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Clear the configuration state
|
|
1349
|
+
*/
|
|
1350
|
+
clear() {
|
|
1351
|
+
this.state = {
|
|
1352
|
+
isConfigured: false
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Get the phone number ID for making calls
|
|
1357
|
+
*/
|
|
1358
|
+
getPhoneNumberId() {
|
|
1359
|
+
return this.state.phoneNumberId;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Get the SIP address for connecting to the agent
|
|
1363
|
+
*/
|
|
1364
|
+
getSipAddress() {
|
|
1365
|
+
return this.state.sipAddress;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Get the configured agent ID
|
|
1369
|
+
*/
|
|
1370
|
+
getAgentId() {
|
|
1371
|
+
return this.state.agentId;
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
var ElevenLabsState = new ElevenLabsStateManager();
|
|
1375
|
+
|
|
1261
1376
|
// ../runtime-shared/dist/utils/templateRender.js
|
|
1262
1377
|
import fs from "fs";
|
|
1263
1378
|
import path from "path";
|
|
@@ -1500,53 +1615,158 @@ function createCacheClient(_projectId, _functionName) {
|
|
|
1500
1615
|
// src/tunnel/index.ts
|
|
1501
1616
|
import WebSocket from "ws";
|
|
1502
1617
|
import axios from "axios";
|
|
1618
|
+
|
|
1619
|
+
// src/tunnel/webhook-config.ts
|
|
1620
|
+
import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
|
|
1621
|
+
async function configureConversationWebhooks(tunnelUrl, config) {
|
|
1622
|
+
try {
|
|
1623
|
+
const conversationAppId = process.env.CONVERSATION_APP_ID;
|
|
1624
|
+
const projectId = process.env.PROJECT_ID;
|
|
1625
|
+
const keyId = process.env.KEY_ID;
|
|
1626
|
+
const keySecret = process.env.KEY_SECRET;
|
|
1627
|
+
if (!conversationAppId || !projectId || !keyId || !keySecret) {
|
|
1628
|
+
console.log("\u{1F4A1} Conversation API not fully configured - skipping webhook setup");
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const webhookUrl = `${tunnelUrl}/conversation`;
|
|
1632
|
+
console.log(`\u{1F4AC} Conversation webhook URL: ${webhookUrl}`);
|
|
1633
|
+
const sinchClient = new SinchClient2({
|
|
1634
|
+
projectId,
|
|
1635
|
+
keyId,
|
|
1636
|
+
keySecret
|
|
1637
|
+
});
|
|
1638
|
+
const webhooksResult = await sinchClient.conversation.webhooks.list({ app_id: conversationAppId });
|
|
1639
|
+
const existingWebhooks = webhooksResult.webhooks || [];
|
|
1640
|
+
const tunnelWebhooks = existingWebhooks.filter(
|
|
1641
|
+
(w) => w.target?.includes("/api/ingress/")
|
|
1642
|
+
);
|
|
1643
|
+
for (const staleWebhook of tunnelWebhooks) {
|
|
1644
|
+
try {
|
|
1645
|
+
await sinchClient.conversation.webhooks.delete({ webhook_id: staleWebhook.id });
|
|
1646
|
+
console.log(`\u{1F9F9} Cleaned up stale tunnel webhook: ${staleWebhook.id}`);
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const createResult = await sinchClient.conversation.webhooks.create({
|
|
1651
|
+
webhookCreateRequestBody: {
|
|
1652
|
+
app_id: conversationAppId,
|
|
1653
|
+
target: webhookUrl,
|
|
1654
|
+
target_type: "HTTP",
|
|
1655
|
+
triggers: ["MESSAGE_INBOUND"]
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
config.conversationWebhookId = createResult.id;
|
|
1659
|
+
console.log(`\u2705 Created Conversation webhook: ${webhookUrl}`);
|
|
1660
|
+
console.log("\u{1F4AC} Send a message to your Conversation app to test!");
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error.message);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
async function cleanupConversationWebhook(config) {
|
|
1666
|
+
if (!config.conversationWebhookId) return;
|
|
1667
|
+
try {
|
|
1668
|
+
const conversationAppId = process.env.CONVERSATION_APP_ID;
|
|
1669
|
+
const projectId = process.env.PROJECT_ID;
|
|
1670
|
+
const keyId = process.env.KEY_ID;
|
|
1671
|
+
const keySecret = process.env.KEY_SECRET;
|
|
1672
|
+
if (!conversationAppId || !projectId || !keyId || !keySecret) return;
|
|
1673
|
+
const sinchClient = new SinchClient2({
|
|
1674
|
+
projectId,
|
|
1675
|
+
keyId,
|
|
1676
|
+
keySecret
|
|
1677
|
+
});
|
|
1678
|
+
await sinchClient.conversation.webhooks.delete({ webhook_id: config.conversationWebhookId });
|
|
1679
|
+
console.log("\u{1F9F9} Cleaned up tunnel webhook");
|
|
1680
|
+
config.conversationWebhookId = void 0;
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
async function configureElevenLabs() {
|
|
1685
|
+
try {
|
|
1686
|
+
const agentId = process.env.ELEVENLABS_AGENT_ID;
|
|
1687
|
+
const apiKey = process.env.ELEVENLABS_API_KEY;
|
|
1688
|
+
if (!agentId || !apiKey) {
|
|
1689
|
+
console.log("\u{1F4A1} ElevenLabs not fully configured - skipping auto-configuration");
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
void apiKey;
|
|
1693
|
+
console.log("\u{1F916} ElevenLabs auto-configuration enabled");
|
|
1694
|
+
console.log(` Agent ID: ${agentId}`);
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
console.log("\u26A0\uFE0F Could not configure ElevenLabs:", error.message);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/tunnel/index.ts
|
|
1701
|
+
var TUNNEL_GATEWAY_DEFAULT = "https://tunnel.fn.sinch.com";
|
|
1503
1702
|
var TunnelClient = class {
|
|
1504
1703
|
ws = null;
|
|
1505
1704
|
tunnelUrl = null;
|
|
1705
|
+
tunnelId = null;
|
|
1506
1706
|
isConnected = false;
|
|
1507
1707
|
reconnectAttempts = 0;
|
|
1508
1708
|
maxReconnectAttempts = 10;
|
|
1509
1709
|
reconnectDelay = 5e3;
|
|
1510
1710
|
heartbeatInterval = null;
|
|
1511
1711
|
localPort;
|
|
1712
|
+
webhookConfig = {};
|
|
1713
|
+
welcomeResolver = null;
|
|
1512
1714
|
constructor(localPort = 3e3) {
|
|
1513
1715
|
this.localPort = localPort;
|
|
1514
1716
|
}
|
|
1717
|
+
getTunnelGatewayUrl() {
|
|
1718
|
+
const explicitUrl = process.env.TUNNEL_GATEWAY_URL;
|
|
1719
|
+
if (explicitUrl) {
|
|
1720
|
+
return explicitUrl;
|
|
1721
|
+
}
|
|
1722
|
+
return TUNNEL_GATEWAY_DEFAULT;
|
|
1723
|
+
}
|
|
1724
|
+
generateTunnelId() {
|
|
1725
|
+
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
1726
|
+
const timestamp = Date.now();
|
|
1727
|
+
let timestampPart = "";
|
|
1728
|
+
let t = timestamp;
|
|
1729
|
+
for (let i = 0; i < 10; i++) {
|
|
1730
|
+
timestampPart = ENCODING[t % 32] + timestampPart;
|
|
1731
|
+
t = Math.floor(t / 32);
|
|
1732
|
+
}
|
|
1733
|
+
const randomBytes = new Uint8Array(10);
|
|
1734
|
+
crypto.getRandomValues(randomBytes);
|
|
1735
|
+
let randomPart = "";
|
|
1736
|
+
for (let i = 0; i < 10; i++) {
|
|
1737
|
+
const byte = randomBytes[i];
|
|
1738
|
+
randomPart += ENCODING[byte >> 3];
|
|
1739
|
+
if (randomPart.length < 16) {
|
|
1740
|
+
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ? randomBytes[i + 1] >> 6 : 0)];
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
randomPart = randomPart.substring(0, 16);
|
|
1744
|
+
return timestampPart + randomPart;
|
|
1745
|
+
}
|
|
1515
1746
|
async connect() {
|
|
1516
1747
|
if (process.env.SINCH_TUNNEL !== "true") {
|
|
1517
1748
|
console.log("Tunnel is disabled (set SINCH_TUNNEL=true to enable)");
|
|
1518
1749
|
return;
|
|
1519
1750
|
}
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
const apiUrl = process.env.API_URL || "https://functions.api.sinch.com";
|
|
1528
|
-
const baseUrl = new URL(apiUrl);
|
|
1529
|
-
const wsUrl = new URL(baseUrl.toString());
|
|
1530
|
-
wsUrl.protocol = baseUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
1531
|
-
wsUrl.pathname = `/api/tunnel/${projectId}/${sessionId}`;
|
|
1751
|
+
const gatewayUrl = this.getTunnelGatewayUrl();
|
|
1752
|
+
this.tunnelId = this.generateTunnelId();
|
|
1753
|
+
const gatewayUri = new URL(gatewayUrl);
|
|
1754
|
+
const wsUrl = new URL(gatewayUrl);
|
|
1755
|
+
wsUrl.protocol = gatewayUri.protocol === "https:" ? "wss:" : "ws:";
|
|
1756
|
+
wsUrl.pathname = "/ws";
|
|
1757
|
+
wsUrl.searchParams.set("tunnel", this.tunnelId);
|
|
1532
1758
|
const tunnelEndpoint = wsUrl.toString();
|
|
1533
|
-
|
|
1534
|
-
tunnelUrlObj.pathname = `/api/ingress/${sessionId}`;
|
|
1535
|
-
this.tunnelUrl = tunnelUrlObj.toString();
|
|
1536
|
-
console.log("Connecting to tunnel server...");
|
|
1759
|
+
console.log(`Connecting to tunnel gateway at ${tunnelEndpoint}...`);
|
|
1537
1760
|
try {
|
|
1538
|
-
this.ws = new WebSocket(tunnelEndpoint
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1761
|
+
this.ws = new WebSocket(tunnelEndpoint);
|
|
1762
|
+
const welcomePromise = new Promise((resolve, reject) => {
|
|
1763
|
+
this.welcomeResolver = resolve;
|
|
1764
|
+
setTimeout(() => reject(new Error("Timed out waiting for welcome message")), 1e4);
|
|
1542
1765
|
});
|
|
1543
|
-
this.ws.on("open",
|
|
1766
|
+
this.ws.on("open", () => {
|
|
1544
1767
|
this.isConnected = true;
|
|
1545
1768
|
this.reconnectAttempts = 0;
|
|
1546
|
-
console.log("
|
|
1547
|
-
console.log(`\u{1F4CD} Tunnel URL: ${this.tunnelUrl}`);
|
|
1548
|
-
console.log(`\u{1F4DE} Voice webhook URL: ${this.tunnelUrl}/`);
|
|
1549
|
-
await this.displayTestPhoneNumbers();
|
|
1769
|
+
console.log("WebSocket connected, waiting for welcome message...");
|
|
1550
1770
|
});
|
|
1551
1771
|
this.ws.on("message", async (data) => {
|
|
1552
1772
|
try {
|
|
@@ -1556,7 +1776,7 @@ var TunnelClient = class {
|
|
|
1556
1776
|
console.error("Error processing tunnel message:", error);
|
|
1557
1777
|
}
|
|
1558
1778
|
});
|
|
1559
|
-
this.ws.on("close", () => {
|
|
1779
|
+
this.ws.on("close", async () => {
|
|
1560
1780
|
this.isConnected = false;
|
|
1561
1781
|
console.log("Tunnel connection closed");
|
|
1562
1782
|
this.stopHeartbeat();
|
|
@@ -1565,14 +1785,23 @@ var TunnelClient = class {
|
|
|
1565
1785
|
this.ws.on("error", (error) => {
|
|
1566
1786
|
console.error("Tunnel connection error:", error.message);
|
|
1567
1787
|
});
|
|
1788
|
+
await welcomePromise;
|
|
1789
|
+
if (!this.tunnelUrl) {
|
|
1790
|
+
throw new Error("Did not receive tunnel URL from gateway");
|
|
1791
|
+
}
|
|
1792
|
+
console.log("Tunnel connected successfully!");
|
|
1568
1793
|
this.startHeartbeat();
|
|
1794
|
+
await this.configureWebhooks();
|
|
1569
1795
|
} catch (error) {
|
|
1570
|
-
console.error("Failed to establish tunnel connection:", error);
|
|
1796
|
+
console.error("Failed to establish tunnel connection:", error.message);
|
|
1571
1797
|
this.scheduleReconnect();
|
|
1572
1798
|
}
|
|
1573
1799
|
}
|
|
1574
1800
|
async handleMessage(message) {
|
|
1575
1801
|
switch (message.type) {
|
|
1802
|
+
case "welcome":
|
|
1803
|
+
this.handleWelcomeMessage(message);
|
|
1804
|
+
break;
|
|
1576
1805
|
case "request":
|
|
1577
1806
|
await this.handleRequest(message);
|
|
1578
1807
|
break;
|
|
@@ -1581,6 +1810,15 @@ var TunnelClient = class {
|
|
|
1581
1810
|
break;
|
|
1582
1811
|
}
|
|
1583
1812
|
}
|
|
1813
|
+
handleWelcomeMessage(message) {
|
|
1814
|
+
this.tunnelId = message.tunnelId || null;
|
|
1815
|
+
this.tunnelUrl = message.publicUrl || null;
|
|
1816
|
+
console.log(`Received welcome: tunnelId=${this.tunnelId}`);
|
|
1817
|
+
if (this.welcomeResolver) {
|
|
1818
|
+
this.welcomeResolver(true);
|
|
1819
|
+
this.welcomeResolver = null;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1584
1822
|
async handleRequest(message) {
|
|
1585
1823
|
console.log(`Forwarding ${message.method} request to ${message.path}`);
|
|
1586
1824
|
try {
|
|
@@ -1597,12 +1835,23 @@ var TunnelClient = class {
|
|
|
1597
1835
|
axiosConfig.data = message.body;
|
|
1598
1836
|
}
|
|
1599
1837
|
const response = await axios(axiosConfig);
|
|
1838
|
+
const headers = {};
|
|
1839
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
1840
|
+
if (value) {
|
|
1841
|
+
const normalizedKey = key.toLowerCase() === "content-type" ? "Content-Type" : key.toLowerCase() === "content-length" ? "Content-Length" : key;
|
|
1842
|
+
headers[normalizedKey] = String(value);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (!headers["Content-Type"] && response.data) {
|
|
1846
|
+
headers["Content-Type"] = "application/json";
|
|
1847
|
+
}
|
|
1848
|
+
const body = typeof response.data === "string" ? response.data : JSON.stringify(response.data);
|
|
1600
1849
|
const responseMessage = {
|
|
1601
1850
|
type: "response",
|
|
1602
1851
|
id: message.id,
|
|
1603
1852
|
statusCode: response.status,
|
|
1604
|
-
headers
|
|
1605
|
-
body
|
|
1853
|
+
headers,
|
|
1854
|
+
body
|
|
1606
1855
|
};
|
|
1607
1856
|
this.ws?.send(JSON.stringify(responseMessage));
|
|
1608
1857
|
} catch (error) {
|
|
@@ -1637,7 +1886,29 @@ var TunnelClient = class {
|
|
|
1637
1886
|
this.heartbeatInterval = null;
|
|
1638
1887
|
}
|
|
1639
1888
|
}
|
|
1640
|
-
|
|
1889
|
+
/**
|
|
1890
|
+
* Configure all webhooks (Voice, Conversation, ElevenLabs)
|
|
1891
|
+
*/
|
|
1892
|
+
async configureWebhooks() {
|
|
1893
|
+
const autoConfigVoice = process.env.AUTO_CONFIGURE_VOICE !== "false";
|
|
1894
|
+
const autoConfigConversation = process.env.AUTO_CONFIGURE_CONVERSATION !== "false";
|
|
1895
|
+
if (autoConfigVoice && process.env.VOICE_APPLICATION_KEY) {
|
|
1896
|
+
await this.configureVoiceWebhooks();
|
|
1897
|
+
}
|
|
1898
|
+
if (autoConfigConversation && process.env.CONVERSATION_APP_ID) {
|
|
1899
|
+
await configureConversationWebhooks(this.tunnelUrl, this.webhookConfig);
|
|
1900
|
+
}
|
|
1901
|
+
if (process.env.ELEVENLABS_AUTO_CONFIGURE === "true") {
|
|
1902
|
+
await configureElevenLabs();
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Cleanup webhooks on disconnect
|
|
1907
|
+
*/
|
|
1908
|
+
async cleanupWebhooks() {
|
|
1909
|
+
await cleanupConversationWebhook(this.webhookConfig);
|
|
1910
|
+
}
|
|
1911
|
+
async configureVoiceWebhooks() {
|
|
1641
1912
|
try {
|
|
1642
1913
|
const appKey = process.env.VOICE_APPLICATION_KEY;
|
|
1643
1914
|
const appSecret = process.env.VOICE_APPLICATION_SECRET;
|
|
@@ -1659,7 +1930,7 @@ var TunnelClient = class {
|
|
|
1659
1930
|
"Content-Type": "application/json"
|
|
1660
1931
|
}
|
|
1661
1932
|
});
|
|
1662
|
-
console.log(
|
|
1933
|
+
console.log("\u2705 Updated voice webhook URL");
|
|
1663
1934
|
} catch (error) {
|
|
1664
1935
|
console.log("\u26A0\uFE0F Could not update webhook URL:", error.message);
|
|
1665
1936
|
}
|
|
@@ -1700,8 +1971,9 @@ var TunnelClient = class {
|
|
|
1700
1971
|
console.log(`Attempting to reconnect in ${delay / 1e3} seconds...`);
|
|
1701
1972
|
setTimeout(() => this.connect(), delay);
|
|
1702
1973
|
}
|
|
1703
|
-
disconnect() {
|
|
1974
|
+
async disconnect() {
|
|
1704
1975
|
this.stopHeartbeat();
|
|
1976
|
+
await this.cleanupWebhooks();
|
|
1705
1977
|
if (this.ws) {
|
|
1706
1978
|
this.ws.close();
|
|
1707
1979
|
this.ws = null;
|