@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.
@@ -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) => {