@sinch/functions-runtime 0.1.0-beta.28 → 0.2.2-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 +30 -70
- 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/README.md
CHANGED
|
@@ -1,52 +1,43 @@
|
|
|
1
1
|
# @sinch/functions-runtime
|
|
2
2
|
|
|
3
|
-
Development runtime for Sinch Functions - build serverless
|
|
3
|
+
Development runtime for Sinch Functions - build serverless communication workflows with ease.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
The easiest way to get started is using the Sinch CLI:
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
# Install the CLI
|
|
11
|
+
npm install -g @sinch/cli
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
# Create a new function (interactive)
|
|
14
|
+
sinch functions init
|
|
15
|
+
```
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
The interactive prompt will guide you through:
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
1. Selecting a runtime (Node.js, C#, etc.)
|
|
20
|
+
2. Choosing a template (simple-voice-ivr recommended for first project)
|
|
21
|
+
3. Setting up your project
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
export async function ice(context, event) {
|
|
20
|
-
return new IceSvamlBuilder()
|
|
21
|
-
.say('Welcome to our service! Press 1 for sales, 2 for support.')
|
|
22
|
-
.runMenu(MenuTemplates.business('Acme Corp').menus)
|
|
23
|
-
.build();
|
|
24
|
-
}
|
|
23
|
+
Then start the local development server:
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const selection = event.menuResult?.value;
|
|
29
|
-
|
|
30
|
-
if (selection === 'sales') {
|
|
31
|
-
return new IceSvamlBuilder()
|
|
32
|
-
.say('Connecting you to sales.')
|
|
33
|
-
.connectPstn('+15551234567')
|
|
34
|
-
.build();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return new IceSvamlBuilder()
|
|
38
|
-
.say('Goodbye!')
|
|
39
|
-
.hangup()
|
|
40
|
-
.build();
|
|
41
|
-
}
|
|
25
|
+
```bash
|
|
26
|
+
sinch functions dev
|
|
42
27
|
```
|
|
43
28
|
|
|
44
|
-
|
|
29
|
+
Your function is now running locally with hot reload!
|
|
30
|
+
|
|
31
|
+
## Deploy to Production
|
|
32
|
+
|
|
33
|
+
When you're ready to deploy:
|
|
45
34
|
|
|
46
35
|
```bash
|
|
47
|
-
|
|
36
|
+
sinch functions deploy
|
|
48
37
|
```
|
|
49
38
|
|
|
39
|
+
Your function will be built and deployed to the Sinch Functions platform.
|
|
40
|
+
|
|
50
41
|
## Features
|
|
51
42
|
|
|
52
43
|
### SVAML Builders
|
|
@@ -84,7 +75,7 @@ const pieResponse = new PieSvamlBuilder()
|
|
|
84
75
|
Create IVR menus with validation:
|
|
85
76
|
|
|
86
77
|
```typescript
|
|
87
|
-
import { MenuBuilder, MenuTemplates
|
|
78
|
+
import { MenuBuilder, MenuTemplates } from '@sinch/functions-runtime';
|
|
88
79
|
|
|
89
80
|
// Use pre-built templates
|
|
90
81
|
const businessMenu = MenuTemplates.business('Acme Corp');
|
|
@@ -107,9 +98,6 @@ const customMenu = new MenuBuilder()
|
|
|
107
98
|
Store and retrieve data across function invocations:
|
|
108
99
|
|
|
109
100
|
```typescript
|
|
110
|
-
import { LocalCache } from '@sinch/functions-runtime';
|
|
111
|
-
|
|
112
|
-
// In your function
|
|
113
101
|
export async function ice(context, event) {
|
|
114
102
|
const cache = context.cache;
|
|
115
103
|
|
|
@@ -118,11 +106,6 @@ export async function ice(context, event) {
|
|
|
118
106
|
|
|
119
107
|
// Retrieve data
|
|
120
108
|
const count = await cache.get('call-count');
|
|
121
|
-
|
|
122
|
-
// Check existence
|
|
123
|
-
if (await cache.has('call-count')) {
|
|
124
|
-
// ...
|
|
125
|
-
}
|
|
126
109
|
}
|
|
127
110
|
```
|
|
128
111
|
|
|
@@ -139,11 +122,8 @@ export async function ice(context, event) {
|
|
|
139
122
|
// Get variables
|
|
140
123
|
const companyName = config.getVariable('COMPANY_NAME', 'Default');
|
|
141
124
|
|
|
142
|
-
// Get secrets
|
|
125
|
+
// Get secrets (encrypted at rest)
|
|
143
126
|
const apiKey = config.getSecret('API_KEY');
|
|
144
|
-
|
|
145
|
-
// Require variables (throws if missing)
|
|
146
|
-
const required = config.requireVariable('IMPORTANT_VAR');
|
|
147
127
|
}
|
|
148
128
|
```
|
|
149
129
|
|
|
@@ -165,8 +145,6 @@ Full TypeScript support with comprehensive types:
|
|
|
165
145
|
import type {
|
|
166
146
|
FunctionContext,
|
|
167
147
|
IceCallbackData,
|
|
168
|
-
AceCallbackData,
|
|
169
|
-
PieCallbackData,
|
|
170
148
|
SvamletResponse
|
|
171
149
|
} from '@sinch/functions-runtime';
|
|
172
150
|
|
|
@@ -178,29 +156,11 @@ export async function ice(
|
|
|
178
156
|
}
|
|
179
157
|
```
|
|
180
158
|
|
|
181
|
-
##
|
|
182
|
-
|
|
183
|
-
### Builders
|
|
184
|
-
|
|
185
|
-
- `IceSvamlBuilder` - Build ICE responses
|
|
186
|
-
- `AceSvamlBuilder` - Build ACE responses
|
|
187
|
-
- `PieSvamlBuilder` - Build PIE responses
|
|
188
|
-
- `MenuBuilder` - Build IVR menus
|
|
189
|
-
- `MenuTemplates` - Pre-built menu templates
|
|
190
|
-
|
|
191
|
-
### Cache
|
|
192
|
-
|
|
193
|
-
- `IFunctionCache` - Cache interface
|
|
194
|
-
- `LocalCache` - In-memory cache (development)
|
|
195
|
-
|
|
196
|
-
### Configuration
|
|
197
|
-
|
|
198
|
-
- `UniversalConfig` - Configuration utility
|
|
199
|
-
- `createConfig()` - Create config instance
|
|
200
|
-
|
|
201
|
-
### Types
|
|
159
|
+
## Documentation
|
|
202
160
|
|
|
203
|
-
|
|
161
|
+
- [Sinch Functions Documentation](https://developers.sinch.com/docs/functions)
|
|
162
|
+
- [Sinch Voice API](https://developers.sinch.com/docs/voice)
|
|
163
|
+
- [SVAML Reference](https://developers.sinch.com/docs/voice/api-reference/svaml)
|
|
204
164
|
|
|
205
165
|
## License
|
|
206
166
|
|
|
@@ -6,6 +6,12 @@ import { createRequire as createRequire3 } from "module";
|
|
|
6
6
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
7
7
|
import fs3 from "fs";
|
|
8
8
|
|
|
9
|
+
// ../runtime-shared/dist/ai/connect-agent.js
|
|
10
|
+
var AgentProvider;
|
|
11
|
+
(function(AgentProvider2) {
|
|
12
|
+
AgentProvider2["ElevenLabs"] = "elevenlabs";
|
|
13
|
+
})(AgentProvider || (AgentProvider = {}));
|
|
14
|
+
|
|
9
15
|
// ../runtime-shared/dist/host/middleware.js
|
|
10
16
|
import { createRequire } from "module";
|
|
11
17
|
var requireCjs = createRequire(import.meta.url);
|
|
@@ -522,6 +528,65 @@ function setupRequestHandler(app, options = {}) {
|
|
|
522
528
|
});
|
|
523
529
|
}
|
|
524
530
|
|
|
531
|
+
// ../runtime-shared/dist/sinch/index.js
|
|
532
|
+
import { SinchClient, validateAuthenticationHeader } from "@sinch/sdk-core";
|
|
533
|
+
|
|
534
|
+
// ../runtime-shared/dist/ai/elevenlabs/state.js
|
|
535
|
+
var ElevenLabsStateManager = class {
|
|
536
|
+
state = {
|
|
537
|
+
isConfigured: false
|
|
538
|
+
};
|
|
539
|
+
/**
|
|
540
|
+
* Get the current state
|
|
541
|
+
*/
|
|
542
|
+
getState() {
|
|
543
|
+
return { ...this.state };
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Check if ElevenLabs is configured
|
|
547
|
+
*/
|
|
548
|
+
isConfigured() {
|
|
549
|
+
return this.state.isConfigured;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Update state with auto-configuration results
|
|
553
|
+
*/
|
|
554
|
+
setConfigured(data) {
|
|
555
|
+
this.state = {
|
|
556
|
+
...data,
|
|
557
|
+
isConfigured: true,
|
|
558
|
+
configuredAt: /* @__PURE__ */ new Date()
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Clear the configuration state
|
|
563
|
+
*/
|
|
564
|
+
clear() {
|
|
565
|
+
this.state = {
|
|
566
|
+
isConfigured: false
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Get the phone number ID for making calls
|
|
571
|
+
*/
|
|
572
|
+
getPhoneNumberId() {
|
|
573
|
+
return this.state.phoneNumberId;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get the SIP address for connecting to the agent
|
|
577
|
+
*/
|
|
578
|
+
getSipAddress() {
|
|
579
|
+
return this.state.sipAddress;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Get the configured agent ID
|
|
583
|
+
*/
|
|
584
|
+
getAgentId() {
|
|
585
|
+
return this.state.agentId;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
var ElevenLabsState = new ElevenLabsStateManager();
|
|
589
|
+
|
|
525
590
|
// ../runtime-shared/dist/utils/templateRender.js
|
|
526
591
|
import fs from "fs";
|
|
527
592
|
import path from "path";
|
|
@@ -611,6 +676,382 @@ function createCacheClient(_projectId, _functionName) {
|
|
|
611
676
|
return new LocalCache();
|
|
612
677
|
}
|
|
613
678
|
|
|
679
|
+
// src/tunnel/index.ts
|
|
680
|
+
import WebSocket from "ws";
|
|
681
|
+
import axios from "axios";
|
|
682
|
+
|
|
683
|
+
// src/tunnel/webhook-config.ts
|
|
684
|
+
import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
|
|
685
|
+
async function configureConversationWebhooks(tunnelUrl, config) {
|
|
686
|
+
try {
|
|
687
|
+
const conversationAppId = process.env.CONVERSATION_APP_ID;
|
|
688
|
+
const projectId = process.env.PROJECT_ID;
|
|
689
|
+
const keyId = process.env.KEY_ID;
|
|
690
|
+
const keySecret = process.env.KEY_SECRET;
|
|
691
|
+
if (!conversationAppId || !projectId || !keyId || !keySecret) {
|
|
692
|
+
console.log("\u{1F4A1} Conversation API not fully configured - skipping webhook setup");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const webhookUrl = `${tunnelUrl}/conversation`;
|
|
696
|
+
console.log(`\u{1F4AC} Conversation webhook URL: ${webhookUrl}`);
|
|
697
|
+
const sinchClient = new SinchClient2({
|
|
698
|
+
projectId,
|
|
699
|
+
keyId,
|
|
700
|
+
keySecret
|
|
701
|
+
});
|
|
702
|
+
const webhooksResult = await sinchClient.conversation.webhooks.list({ app_id: conversationAppId });
|
|
703
|
+
const existingWebhooks = webhooksResult.webhooks || [];
|
|
704
|
+
const tunnelWebhooks = existingWebhooks.filter(
|
|
705
|
+
(w) => w.target?.includes("/api/ingress/")
|
|
706
|
+
);
|
|
707
|
+
for (const staleWebhook of tunnelWebhooks) {
|
|
708
|
+
try {
|
|
709
|
+
await sinchClient.conversation.webhooks.delete({ webhook_id: staleWebhook.id });
|
|
710
|
+
console.log(`\u{1F9F9} Cleaned up stale tunnel webhook: ${staleWebhook.id}`);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const createResult = await sinchClient.conversation.webhooks.create({
|
|
715
|
+
webhookCreateRequestBody: {
|
|
716
|
+
app_id: conversationAppId,
|
|
717
|
+
target: webhookUrl,
|
|
718
|
+
target_type: "HTTP",
|
|
719
|
+
triggers: ["MESSAGE_INBOUND"]
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
config.conversationWebhookId = createResult.id;
|
|
723
|
+
console.log(`\u2705 Created Conversation webhook: ${webhookUrl}`);
|
|
724
|
+
console.log("\u{1F4AC} Send a message to your Conversation app to test!");
|
|
725
|
+
} catch (error) {
|
|
726
|
+
console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error.message);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async function cleanupConversationWebhook(config) {
|
|
730
|
+
if (!config.conversationWebhookId) return;
|
|
731
|
+
try {
|
|
732
|
+
const conversationAppId = process.env.CONVERSATION_APP_ID;
|
|
733
|
+
const projectId = process.env.PROJECT_ID;
|
|
734
|
+
const keyId = process.env.KEY_ID;
|
|
735
|
+
const keySecret = process.env.KEY_SECRET;
|
|
736
|
+
if (!conversationAppId || !projectId || !keyId || !keySecret) return;
|
|
737
|
+
const sinchClient = new SinchClient2({
|
|
738
|
+
projectId,
|
|
739
|
+
keyId,
|
|
740
|
+
keySecret
|
|
741
|
+
});
|
|
742
|
+
await sinchClient.conversation.webhooks.delete({ webhook_id: config.conversationWebhookId });
|
|
743
|
+
console.log("\u{1F9F9} Cleaned up tunnel webhook");
|
|
744
|
+
config.conversationWebhookId = void 0;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async function configureElevenLabs() {
|
|
749
|
+
try {
|
|
750
|
+
const agentId = process.env.ELEVENLABS_AGENT_ID;
|
|
751
|
+
const apiKey = process.env.ELEVENLABS_API_KEY;
|
|
752
|
+
if (!agentId || !apiKey) {
|
|
753
|
+
console.log("\u{1F4A1} ElevenLabs not fully configured - skipping auto-configuration");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
void apiKey;
|
|
757
|
+
console.log("\u{1F916} ElevenLabs auto-configuration enabled");
|
|
758
|
+
console.log(` Agent ID: ${agentId}`);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
console.log("\u26A0\uFE0F Could not configure ElevenLabs:", error.message);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/tunnel/index.ts
|
|
765
|
+
var TUNNEL_GATEWAY_DEFAULT = "https://tunnel.fn.sinch.com";
|
|
766
|
+
var TunnelClient = class {
|
|
767
|
+
ws = null;
|
|
768
|
+
tunnelUrl = null;
|
|
769
|
+
tunnelId = null;
|
|
770
|
+
isConnected = false;
|
|
771
|
+
reconnectAttempts = 0;
|
|
772
|
+
maxReconnectAttempts = 10;
|
|
773
|
+
reconnectDelay = 5e3;
|
|
774
|
+
heartbeatInterval = null;
|
|
775
|
+
localPort;
|
|
776
|
+
webhookConfig = {};
|
|
777
|
+
welcomeResolver = null;
|
|
778
|
+
constructor(localPort = 3e3) {
|
|
779
|
+
this.localPort = localPort;
|
|
780
|
+
}
|
|
781
|
+
getTunnelGatewayUrl() {
|
|
782
|
+
const explicitUrl = process.env.TUNNEL_GATEWAY_URL;
|
|
783
|
+
if (explicitUrl) {
|
|
784
|
+
return explicitUrl;
|
|
785
|
+
}
|
|
786
|
+
return TUNNEL_GATEWAY_DEFAULT;
|
|
787
|
+
}
|
|
788
|
+
generateTunnelId() {
|
|
789
|
+
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
790
|
+
const timestamp = Date.now();
|
|
791
|
+
let timestampPart = "";
|
|
792
|
+
let t = timestamp;
|
|
793
|
+
for (let i = 0; i < 10; i++) {
|
|
794
|
+
timestampPart = ENCODING[t % 32] + timestampPart;
|
|
795
|
+
t = Math.floor(t / 32);
|
|
796
|
+
}
|
|
797
|
+
const randomBytes = new Uint8Array(10);
|
|
798
|
+
crypto.getRandomValues(randomBytes);
|
|
799
|
+
let randomPart = "";
|
|
800
|
+
for (let i = 0; i < 10; i++) {
|
|
801
|
+
const byte = randomBytes[i];
|
|
802
|
+
randomPart += ENCODING[byte >> 3];
|
|
803
|
+
if (randomPart.length < 16) {
|
|
804
|
+
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ? randomBytes[i + 1] >> 6 : 0)];
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
randomPart = randomPart.substring(0, 16);
|
|
808
|
+
return timestampPart + randomPart;
|
|
809
|
+
}
|
|
810
|
+
async connect() {
|
|
811
|
+
if (process.env.SINCH_TUNNEL !== "true") {
|
|
812
|
+
console.log("Tunnel is disabled (set SINCH_TUNNEL=true to enable)");
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const gatewayUrl = this.getTunnelGatewayUrl();
|
|
816
|
+
this.tunnelId = this.generateTunnelId();
|
|
817
|
+
const gatewayUri = new URL(gatewayUrl);
|
|
818
|
+
const wsUrl = new URL(gatewayUrl);
|
|
819
|
+
wsUrl.protocol = gatewayUri.protocol === "https:" ? "wss:" : "ws:";
|
|
820
|
+
wsUrl.pathname = "/ws";
|
|
821
|
+
wsUrl.searchParams.set("tunnel", this.tunnelId);
|
|
822
|
+
const tunnelEndpoint = wsUrl.toString();
|
|
823
|
+
console.log(`Connecting to tunnel gateway at ${tunnelEndpoint}...`);
|
|
824
|
+
try {
|
|
825
|
+
this.ws = new WebSocket(tunnelEndpoint);
|
|
826
|
+
const welcomePromise = new Promise((resolve, reject) => {
|
|
827
|
+
this.welcomeResolver = resolve;
|
|
828
|
+
setTimeout(() => reject(new Error("Timed out waiting for welcome message")), 1e4);
|
|
829
|
+
});
|
|
830
|
+
this.ws.on("open", () => {
|
|
831
|
+
this.isConnected = true;
|
|
832
|
+
this.reconnectAttempts = 0;
|
|
833
|
+
console.log("WebSocket connected, waiting for welcome message...");
|
|
834
|
+
});
|
|
835
|
+
this.ws.on("message", async (data) => {
|
|
836
|
+
try {
|
|
837
|
+
const message = JSON.parse(data.toString());
|
|
838
|
+
await this.handleMessage(message);
|
|
839
|
+
} catch (error) {
|
|
840
|
+
console.error("Error processing tunnel message:", error);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
this.ws.on("close", async () => {
|
|
844
|
+
this.isConnected = false;
|
|
845
|
+
console.log("Tunnel connection closed");
|
|
846
|
+
this.stopHeartbeat();
|
|
847
|
+
this.scheduleReconnect();
|
|
848
|
+
});
|
|
849
|
+
this.ws.on("error", (error) => {
|
|
850
|
+
console.error("Tunnel connection error:", error.message);
|
|
851
|
+
});
|
|
852
|
+
await welcomePromise;
|
|
853
|
+
if (!this.tunnelUrl) {
|
|
854
|
+
throw new Error("Did not receive tunnel URL from gateway");
|
|
855
|
+
}
|
|
856
|
+
console.log("Tunnel connected successfully!");
|
|
857
|
+
this.startHeartbeat();
|
|
858
|
+
await this.configureWebhooks();
|
|
859
|
+
} catch (error) {
|
|
860
|
+
console.error("Failed to establish tunnel connection:", error.message);
|
|
861
|
+
this.scheduleReconnect();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async handleMessage(message) {
|
|
865
|
+
switch (message.type) {
|
|
866
|
+
case "welcome":
|
|
867
|
+
this.handleWelcomeMessage(message);
|
|
868
|
+
break;
|
|
869
|
+
case "request":
|
|
870
|
+
await this.handleRequest(message);
|
|
871
|
+
break;
|
|
872
|
+
case "ping":
|
|
873
|
+
this.sendPong();
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
handleWelcomeMessage(message) {
|
|
878
|
+
this.tunnelId = message.tunnelId || null;
|
|
879
|
+
this.tunnelUrl = message.publicUrl || null;
|
|
880
|
+
console.log(`Received welcome: tunnelId=${this.tunnelId}`);
|
|
881
|
+
if (this.welcomeResolver) {
|
|
882
|
+
this.welcomeResolver(true);
|
|
883
|
+
this.welcomeResolver = null;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
async handleRequest(message) {
|
|
887
|
+
console.log(`Forwarding ${message.method} request to ${message.path}`);
|
|
888
|
+
try {
|
|
889
|
+
const localUrl = `http://localhost:${this.localPort}${message.path}${message.query || ""}`;
|
|
890
|
+
const axiosConfig = {
|
|
891
|
+
method: message.method,
|
|
892
|
+
url: localUrl,
|
|
893
|
+
headers: {}
|
|
894
|
+
};
|
|
895
|
+
if (message.headers) {
|
|
896
|
+
axiosConfig.headers = { ...message.headers };
|
|
897
|
+
}
|
|
898
|
+
if (message.body) {
|
|
899
|
+
axiosConfig.data = message.body;
|
|
900
|
+
}
|
|
901
|
+
const response = await axios(axiosConfig);
|
|
902
|
+
const headers = {};
|
|
903
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
904
|
+
if (value) {
|
|
905
|
+
const normalizedKey = key.toLowerCase() === "content-type" ? "Content-Type" : key.toLowerCase() === "content-length" ? "Content-Length" : key;
|
|
906
|
+
headers[normalizedKey] = String(value);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (!headers["Content-Type"] && response.data) {
|
|
910
|
+
headers["Content-Type"] = "application/json";
|
|
911
|
+
}
|
|
912
|
+
const body = typeof response.data === "string" ? response.data : JSON.stringify(response.data);
|
|
913
|
+
const responseMessage = {
|
|
914
|
+
type: "response",
|
|
915
|
+
id: message.id,
|
|
916
|
+
statusCode: response.status,
|
|
917
|
+
headers,
|
|
918
|
+
body
|
|
919
|
+
};
|
|
920
|
+
this.ws?.send(JSON.stringify(responseMessage));
|
|
921
|
+
} catch (error) {
|
|
922
|
+
console.error("Error forwarding request:", error.message);
|
|
923
|
+
const errorResponse = {
|
|
924
|
+
type: "response",
|
|
925
|
+
id: message.id,
|
|
926
|
+
statusCode: error.response?.status || 502,
|
|
927
|
+
headers: { "Content-Type": "text/plain" },
|
|
928
|
+
body: "Error forwarding request to local server"
|
|
929
|
+
};
|
|
930
|
+
this.ws?.send(JSON.stringify(errorResponse));
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
sendPong() {
|
|
934
|
+
const pongMessage = { type: "pong" };
|
|
935
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
936
|
+
this.ws.send(JSON.stringify(pongMessage));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
startHeartbeat() {
|
|
940
|
+
this.heartbeatInterval = setInterval(() => {
|
|
941
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
942
|
+
const pingMessage = { type: "ping" };
|
|
943
|
+
this.ws.send(JSON.stringify(pingMessage));
|
|
944
|
+
}
|
|
945
|
+
}, 3e4);
|
|
946
|
+
}
|
|
947
|
+
stopHeartbeat() {
|
|
948
|
+
if (this.heartbeatInterval) {
|
|
949
|
+
clearInterval(this.heartbeatInterval);
|
|
950
|
+
this.heartbeatInterval = null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Configure all webhooks (Voice, Conversation, ElevenLabs)
|
|
955
|
+
*/
|
|
956
|
+
async configureWebhooks() {
|
|
957
|
+
const autoConfigVoice = process.env.AUTO_CONFIGURE_VOICE !== "false";
|
|
958
|
+
const autoConfigConversation = process.env.AUTO_CONFIGURE_CONVERSATION !== "false";
|
|
959
|
+
if (autoConfigVoice && process.env.VOICE_APPLICATION_KEY) {
|
|
960
|
+
await this.configureVoiceWebhooks();
|
|
961
|
+
}
|
|
962
|
+
if (autoConfigConversation && process.env.CONVERSATION_APP_ID) {
|
|
963
|
+
await configureConversationWebhooks(this.tunnelUrl, this.webhookConfig);
|
|
964
|
+
}
|
|
965
|
+
if (process.env.ELEVENLABS_AUTO_CONFIGURE === "true") {
|
|
966
|
+
await configureElevenLabs();
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Cleanup webhooks on disconnect
|
|
971
|
+
*/
|
|
972
|
+
async cleanupWebhooks() {
|
|
973
|
+
await cleanupConversationWebhook(this.webhookConfig);
|
|
974
|
+
}
|
|
975
|
+
async configureVoiceWebhooks() {
|
|
976
|
+
try {
|
|
977
|
+
const appKey = process.env.VOICE_APPLICATION_KEY;
|
|
978
|
+
const appSecret = process.env.VOICE_APPLICATION_SECRET;
|
|
979
|
+
if (!appKey || !appSecret) {
|
|
980
|
+
console.log("\u{1F4A1} Voice API not configured - skipping phone number display");
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
const updateUrl = `https://callingapi.sinch.com/v1/configuration/callbacks/applications/${appKey}/`;
|
|
985
|
+
const auth = Buffer.from(`${appKey}:${appSecret}`).toString("base64");
|
|
986
|
+
await axios.post(updateUrl, {
|
|
987
|
+
url: {
|
|
988
|
+
primary: this.tunnelUrl,
|
|
989
|
+
fallback: null
|
|
990
|
+
}
|
|
991
|
+
}, {
|
|
992
|
+
headers: {
|
|
993
|
+
"Authorization": `Basic ${auth}`,
|
|
994
|
+
"Content-Type": "application/json"
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
console.log("\u2705 Updated voice webhook URL");
|
|
998
|
+
} catch (error) {
|
|
999
|
+
console.log("\u26A0\uFE0F Could not update webhook URL:", error.message);
|
|
1000
|
+
}
|
|
1001
|
+
try {
|
|
1002
|
+
const listUrl = `https://callingapi.sinch.com/v1/configuration/numbers/`;
|
|
1003
|
+
const auth = Buffer.from(`${appKey}:${appSecret}`).toString("base64");
|
|
1004
|
+
const response = await axios.get(listUrl, {
|
|
1005
|
+
headers: {
|
|
1006
|
+
"Authorization": `Basic ${auth}`
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
const numbers = response.data?.numbers || [];
|
|
1010
|
+
const appNumbers = numbers.filter((n) => n.applicationkey === appKey);
|
|
1011
|
+
if (appNumbers.length > 0) {
|
|
1012
|
+
console.log("\u{1F4F1} Test Phone Numbers:");
|
|
1013
|
+
appNumbers.forEach((num) => {
|
|
1014
|
+
console.log(` \u260E\uFE0F ${num.number}`);
|
|
1015
|
+
});
|
|
1016
|
+
console.log("\u{1F4A1} Call any of these numbers to test your voice function!");
|
|
1017
|
+
} else {
|
|
1018
|
+
console.log("\u26A0\uFE0F No phone numbers assigned to this application yet");
|
|
1019
|
+
console.log("\u{1F4A1} Add numbers at https://dashboard.sinch.com/voice/apps");
|
|
1020
|
+
}
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
console.log("\u{1F4A1} Could not fetch phone numbers:", error.message);
|
|
1023
|
+
}
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
console.log("\u{1F4A1} Could not fetch phone numbers (Voice API may not be configured)");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
scheduleReconnect() {
|
|
1029
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1030
|
+
console.error("Max reconnection attempts reached. Giving up.");
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
this.reconnectAttempts++;
|
|
1034
|
+
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 3e4);
|
|
1035
|
+
console.log(`Attempting to reconnect in ${delay / 1e3} seconds...`);
|
|
1036
|
+
setTimeout(() => this.connect(), delay);
|
|
1037
|
+
}
|
|
1038
|
+
async disconnect() {
|
|
1039
|
+
this.stopHeartbeat();
|
|
1040
|
+
await this.cleanupWebhooks();
|
|
1041
|
+
if (this.ws) {
|
|
1042
|
+
this.ws.close();
|
|
1043
|
+
this.ws = null;
|
|
1044
|
+
}
|
|
1045
|
+
this.isConnected = false;
|
|
1046
|
+
}
|
|
1047
|
+
getTunnelUrl() {
|
|
1048
|
+
return this.tunnelUrl;
|
|
1049
|
+
}
|
|
1050
|
+
getIsConnected() {
|
|
1051
|
+
return this.isConnected;
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
|
|
614
1055
|
// src/bin/sinch-runtime.ts
|
|
615
1056
|
var requireCjs3 = createRequire3(import.meta.url);
|
|
616
1057
|
function findFunctionPath3() {
|
|
@@ -783,18 +1224,25 @@ async function main() {
|
|
|
783
1224
|
});
|
|
784
1225
|
});
|
|
785
1226
|
displayStartupInfo(config, verbose, port);
|
|
786
|
-
app.listen(port, () => {
|
|
1227
|
+
app.listen(port, async () => {
|
|
787
1228
|
console.log(`Function server running on http://localhost:${port}`);
|
|
788
1229
|
console.log("\nTest endpoints:");
|
|
789
1230
|
console.log(` ICE: POST http://localhost:${port}/ice`);
|
|
790
1231
|
console.log(` PIE: POST http://localhost:${port}/pie`);
|
|
791
1232
|
console.log(` ACE: POST http://localhost:${port}/ace`);
|
|
792
1233
|
console.log(` DICE: POST http://localhost:${port}/dice`);
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1234
|
+
await displayDetectedFunctions();
|
|
1235
|
+
if (!verbose) {
|
|
1236
|
+
console.log("\nTip: Set VERBOSE=true or use --verbose for detailed output");
|
|
1237
|
+
}
|
|
1238
|
+
if (process.env.SINCH_TUNNEL === "true") {
|
|
1239
|
+
console.log("\nStarting tunnel...");
|
|
1240
|
+
const tunnelClient = new TunnelClient(port);
|
|
1241
|
+
await tunnelClient.connect();
|
|
1242
|
+
process.on("beforeExit", async () => {
|
|
1243
|
+
await tunnelClient.disconnect();
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
798
1246
|
});
|
|
799
1247
|
}
|
|
800
1248
|
main().catch((error) => {
|