@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 CHANGED
@@ -1,52 +1,43 @@
1
1
  # @sinch/functions-runtime
2
2
 
3
- Development runtime for Sinch Functions - build serverless voice applications with ease.
3
+ Development runtime for Sinch Functions - build serverless communication workflows with ease.
4
4
 
5
- ## Installation
5
+ ## Getting Started
6
+
7
+ The easiest way to get started is using the Sinch CLI:
6
8
 
7
9
  ```bash
8
- npm install @sinch/functions-runtime
9
- ```
10
+ # Install the CLI
11
+ npm install -g @sinch/cli
10
12
 
11
- ## Quick Start
13
+ # Create a new function (interactive)
14
+ sinch functions init
15
+ ```
12
16
 
13
- Create a `function.js` file:
17
+ The interactive prompt will guide you through:
14
18
 
15
- ```javascript
16
- import { IceSvamlBuilder, MenuBuilder, MenuTemplates } from '@sinch/functions-runtime';
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
- // Handle incoming calls
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
- // Handle menu selections
27
- export async function pie(context, event) {
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
- Run locally:
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
- npx sinch-runtime
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, createMenu } from '@sinch/functions-runtime';
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
- ## API Reference
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
- All callback data types, SVAML types, and handler types are exported for TypeScript users.
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
- void displayDetectedFunctions().then(() => {
794
- if (!verbose) {
795
- console.log("\nTip: Set VERBOSE=true or use --verbose for detailed output");
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) => {