@sinch/functions-runtime 0.4.1 → 0.4.3

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/index.d.ts CHANGED
@@ -1006,6 +1006,35 @@ export interface FunctionContext {
1006
1006
  /** Read a file from the assets/ directory (private, not served over HTTP) */
1007
1007
  assets(filename: string): Promise<string>;
1008
1008
  }
1009
+ /**
1010
+ * WebSocket handler callback
1011
+ */
1012
+ export type WebSocketHandler = (ws: import("ws").WebSocket, req: import("http").IncomingMessage) => void;
1013
+ /**
1014
+ * Runtime configuration object passed to the optional `setup()` export.
1015
+ *
1016
+ * Provides hooks for startup initialization and WebSocket endpoints
1017
+ * without exposing the raw Express app or HTTP server.
1018
+ *
1019
+ * @example
1020
+ * ```typescript
1021
+ * export function setup(runtime: SinchRuntime) {
1022
+ * runtime.onStartup(async (context) => {
1023
+ * // Initialize database, warm caches, etc.
1024
+ * });
1025
+ *
1026
+ * runtime.onWebSocket('/stream', (ws, req) => {
1027
+ * // Handle Sinch connectStream binary audio
1028
+ * });
1029
+ * }
1030
+ * ```
1031
+ */
1032
+ export interface SinchRuntime {
1033
+ /** Register a callback to run once at startup, before the server accepts requests. */
1034
+ onStartup(handler: (context: FunctionContext) => Promise<void> | void): void;
1035
+ /** Register a WebSocket upgrade handler at the given path. */
1036
+ onWebSocket(path: string, handler: WebSocketHandler): void;
1037
+ }
1009
1038
  /**
1010
1039
  * Application credentials structure
1011
1040
  */
@@ -1455,6 +1484,7 @@ export interface SinchClients {
1455
1484
  sms?: SmsService;
1456
1485
  numbers?: NumbersService;
1457
1486
  validateWebhookSignature?: (requestData: WebhookRequestData) => boolean;
1487
+ validateConversationWebhook?: (headers: Record<string, string | string[] | undefined>, body: unknown) => boolean;
1458
1488
  }
1459
1489
  export interface WebhookRequestData {
1460
1490
  method: string;
@@ -2420,6 +2450,12 @@ export declare class TunnelClient {
2420
2450
  private generateTunnelId;
2421
2451
  connect(): Promise<void>;
2422
2452
  private handleMessage;
2453
+ /**
2454
+ * Build a full tunnel URL with optional sub-path and tunnel query param.
2455
+ * e.g. buildTunnelUrl('/webhook/conversation') →
2456
+ * https://tunnel.fn.sinch.com/ingress/webhook/conversation?tunnel=01KKT...
2457
+ */
2458
+ private buildTunnelUrl;
2423
2459
  private handleWelcomeMessage;
2424
2460
  private handleRequest;
2425
2461
  private sendPong;
package/dist/index.js CHANGED
@@ -959,6 +959,122 @@ function validateBasicAuth(authHeader, expectedKey, expectedSecret) {
959
959
  return keyMatch && secretMatch;
960
960
  }
961
961
 
962
+ // ../runtime-shared/dist/security/index.js
963
+ function shouldValidateWebhook(mode, isDevelopment) {
964
+ const normalizedMode = (mode || "deploy").toLowerCase();
965
+ switch (normalizedMode) {
966
+ case "never":
967
+ return false;
968
+ case "always":
969
+ return true;
970
+ case "deploy":
971
+ default:
972
+ return !isDevelopment;
973
+ }
974
+ }
975
+ var VALID_MODES = ["never", "deploy", "always"];
976
+ function getProtectionMode(config) {
977
+ const envValue = process.env.WEBHOOK_PROTECTION ?? process.env.PROTECT_VOICE_CALLBACKS;
978
+ if (envValue) {
979
+ const normalized = envValue.toLowerCase();
980
+ if (VALID_MODES.includes(normalized)) {
981
+ return normalized;
982
+ }
983
+ console.warn(`[SECURITY] Unknown WEBHOOK_PROTECTION value "${envValue}", defaulting to "deploy"`);
984
+ return "deploy";
985
+ }
986
+ const configValue = config?.WebhookProtection ?? config?.webhookProtection ?? config?.ProtectVoiceCallbacks ?? config?.protectVoiceCallbacks;
987
+ if (typeof configValue === "string") {
988
+ const normalized = configValue.toLowerCase();
989
+ if (VALID_MODES.includes(normalized)) {
990
+ return normalized;
991
+ }
992
+ console.warn(`[SECURITY] Unknown webhook protection value "${configValue}", defaulting to "deploy"`);
993
+ return "deploy";
994
+ }
995
+ return "deploy";
996
+ }
997
+
998
+ // ../runtime-shared/dist/sinch/index.js
999
+ import { SinchClient, validateAuthenticationHeader, ConversationCallbackWebhooks } from "@sinch/sdk-core";
1000
+ function createSinchClients() {
1001
+ const clients = {};
1002
+ const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
1003
+ if (process.env.CONVERSATION_WEBHOOK_SECRET) {
1004
+ const callbackProcessor = new ConversationCallbackWebhooks(process.env.CONVERSATION_WEBHOOK_SECRET);
1005
+ clients.validateConversationWebhook = (headers, body) => {
1006
+ try {
1007
+ const result = callbackProcessor.validateAuthenticationHeader(headers, body);
1008
+ console.log("[SINCH] Conversation webhook validation:", result ? "VALID" : "INVALID");
1009
+ return result;
1010
+ } catch (error) {
1011
+ console.error("[SINCH] Conversation validation error:", error instanceof Error ? error.message : error);
1012
+ return false;
1013
+ }
1014
+ };
1015
+ }
1016
+ if (!hasCredentials) {
1017
+ return clients;
1018
+ }
1019
+ try {
1020
+ const sinchClient = new SinchClient({
1021
+ projectId: process.env.PROJECT_ID,
1022
+ keyId: process.env.PROJECT_ID_API_KEY,
1023
+ keySecret: process.env.PROJECT_ID_API_SECRET
1024
+ });
1025
+ if (process.env.CONVERSATION_APP_ID) {
1026
+ clients.conversation = sinchClient.conversation;
1027
+ console.log("[SINCH] Conversation API initialized");
1028
+ }
1029
+ if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
1030
+ const voiceClient = new SinchClient({
1031
+ projectId: process.env.PROJECT_ID,
1032
+ keyId: process.env.PROJECT_ID_API_KEY,
1033
+ keySecret: process.env.PROJECT_ID_API_SECRET,
1034
+ applicationKey: process.env.VOICE_APPLICATION_KEY,
1035
+ applicationSecret: process.env.VOICE_APPLICATION_SECRET
1036
+ });
1037
+ clients.voice = voiceClient.voice;
1038
+ console.log("[SINCH] Voice API initialized with application credentials");
1039
+ }
1040
+ if (process.env.SMS_SERVICE_PLAN_ID) {
1041
+ clients.sms = sinchClient.sms;
1042
+ console.log("[SINCH] SMS API initialized");
1043
+ }
1044
+ if (process.env.ENABLE_NUMBERS_API === "true") {
1045
+ clients.numbers = sinchClient.numbers;
1046
+ console.log("[SINCH] Numbers API initialized");
1047
+ }
1048
+ } catch (error) {
1049
+ console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
1050
+ return {};
1051
+ }
1052
+ if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
1053
+ clients.validateWebhookSignature = (requestData) => {
1054
+ console.log("[SINCH] Validating Voice webhook signature");
1055
+ try {
1056
+ const result = validateAuthenticationHeader(process.env.VOICE_APPLICATION_KEY, process.env.VOICE_APPLICATION_SECRET, requestData.headers, requestData.body, requestData.path, requestData.method);
1057
+ console.log("[SINCH] Validation result:", result ? "VALID" : "INVALID");
1058
+ return result;
1059
+ } catch (error) {
1060
+ console.error("[SINCH] Validation error:", error.message);
1061
+ return false;
1062
+ }
1063
+ };
1064
+ }
1065
+ return clients;
1066
+ }
1067
+ var cachedClients = null;
1068
+ function getSinchClients() {
1069
+ if (!cachedClients) {
1070
+ cachedClients = createSinchClients();
1071
+ }
1072
+ return cachedClients;
1073
+ }
1074
+ function resetSinchClients() {
1075
+ cachedClients = null;
1076
+ }
1077
+
962
1078
  // ../runtime-shared/dist/host/app.js
963
1079
  import { createRequire as createRequire2 } from "module";
964
1080
  import { pathToFileURL } from "url";
@@ -1159,7 +1275,7 @@ var noOpStorage = {
1159
1275
  };
1160
1276
  function buildBaseContext(req, config = {}) {
1161
1277
  return {
1162
- requestId: req.headers["x-request-id"] || generateRequestId(),
1278
+ requestId: req?.headers?.["x-request-id"] || generateRequestId(),
1163
1279
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1164
1280
  env: process.env,
1165
1281
  config: {
@@ -1180,7 +1296,13 @@ function buildBaseContext(req, config = {}) {
1180
1296
  async function handleVoiceCallback(functionName, userFunction, context, callbackData, logger) {
1181
1297
  const handler = userFunction[functionName];
1182
1298
  if (!handler || typeof handler !== "function") {
1183
- throw new Error(`Function '${functionName}' not found in function.js`);
1299
+ if (functionName === "ice") {
1300
+ throw new Error(`Voice callback 'ice' not found \u2014 export an ice() function in function.ts`);
1301
+ }
1302
+ if (logger) {
1303
+ logger(`${functionName.toUpperCase()} callback not implemented \u2014 returning 200`);
1304
+ }
1305
+ return { statusCode: 200, body: {}, headers: {} };
1184
1306
  }
1185
1307
  let result;
1186
1308
  switch (functionName) {
@@ -1223,7 +1345,9 @@ async function handleCustomEndpoint(functionName, userFunction, context, request
1223
1345
  handler = userFunction["home"];
1224
1346
  }
1225
1347
  if (!handler || typeof handler !== "function") {
1226
- throw new Error(`Function '${functionName}' not found in function.js`);
1348
+ const available = Object.keys(userFunction).filter((k) => typeof userFunction[k] === "function");
1349
+ const pathHint = functionName.endsWith("Webhook") ? `/webhook/${functionName.replace("Webhook", "")}` : `/${functionName}`;
1350
+ throw new Error(`No export '${functionName}' found for path ${pathHint}. Available exports: [${available.join(", ")}]. Custom endpoints require a named export matching the last path segment.`);
1227
1351
  }
1228
1352
  const result = await handler(context, request);
1229
1353
  if (logger) {
@@ -1300,6 +1424,33 @@ function setupRequestHandler(app, options = {}) {
1300
1424
  }
1301
1425
  }
1302
1426
  }
1427
+ const protectionMode = getProtectionMode();
1428
+ const isDev = process.env.NODE_ENV !== "production" && process.env.ASPNETCORE_ENVIRONMENT !== "Production";
1429
+ if (shouldValidateWebhook(protectionMode, isDev)) {
1430
+ const sinchClients = getSinchClients();
1431
+ if (isVoiceCallback(functionName) && sinchClients.validateWebhookSignature) {
1432
+ const rawBody = req.rawBody ?? JSON.stringify(req.body);
1433
+ const isValid = sinchClients.validateWebhookSignature({
1434
+ method: req.method,
1435
+ path: req.path,
1436
+ headers: req.headers,
1437
+ body: rawBody
1438
+ });
1439
+ if (!isValid) {
1440
+ logger("[SECURITY] Voice webhook signature validation failed");
1441
+ res.status(401).json({ error: "Invalid webhook signature" });
1442
+ return;
1443
+ }
1444
+ }
1445
+ if (functionName === "conversationWebhook" && sinchClients.validateConversationWebhook) {
1446
+ const isValid = sinchClients.validateConversationWebhook(req.headers, req.body);
1447
+ if (!isValid) {
1448
+ logger("[SECURITY] Conversation webhook signature validation failed");
1449
+ res.status(401).json({ error: "Invalid webhook signature" });
1450
+ return;
1451
+ }
1452
+ }
1453
+ }
1303
1454
  onRequestStart({ functionName, req });
1304
1455
  const context = buildContext(req);
1305
1456
  const userFunction = await Promise.resolve(loadUserFunction());
@@ -1375,73 +1526,6 @@ function setupRequestHandler(app, options = {}) {
1375
1526
  });
1376
1527
  }
1377
1528
 
1378
- // ../runtime-shared/dist/sinch/index.js
1379
- import { SinchClient, validateAuthenticationHeader } from "@sinch/sdk-core";
1380
- function createSinchClients() {
1381
- const clients = {};
1382
- const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
1383
- if (!hasCredentials) {
1384
- return clients;
1385
- }
1386
- try {
1387
- const sinchClient = new SinchClient({
1388
- projectId: process.env.PROJECT_ID,
1389
- keyId: process.env.PROJECT_ID_API_KEY,
1390
- keySecret: process.env.PROJECT_ID_API_SECRET
1391
- });
1392
- if (process.env.CONVERSATION_APP_ID) {
1393
- clients.conversation = sinchClient.conversation;
1394
- console.log("[SINCH] Conversation API initialized");
1395
- }
1396
- if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
1397
- const voiceClient = new SinchClient({
1398
- projectId: process.env.PROJECT_ID,
1399
- keyId: process.env.PROJECT_ID_API_KEY,
1400
- keySecret: process.env.PROJECT_ID_API_SECRET,
1401
- applicationKey: process.env.VOICE_APPLICATION_KEY,
1402
- applicationSecret: process.env.VOICE_APPLICATION_SECRET
1403
- });
1404
- clients.voice = voiceClient.voice;
1405
- console.log("[SINCH] Voice API initialized with application credentials");
1406
- }
1407
- if (process.env.SMS_SERVICE_PLAN_ID) {
1408
- clients.sms = sinchClient.sms;
1409
- console.log("[SINCH] SMS API initialized");
1410
- }
1411
- if (process.env.ENABLE_NUMBERS_API === "true") {
1412
- clients.numbers = sinchClient.numbers;
1413
- console.log("[SINCH] Numbers API initialized");
1414
- }
1415
- } catch (error) {
1416
- console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
1417
- return {};
1418
- }
1419
- if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
1420
- clients.validateWebhookSignature = (requestData) => {
1421
- console.log("[SINCH] Validating Voice webhook signature");
1422
- try {
1423
- const result = validateAuthenticationHeader(process.env.VOICE_APPLICATION_KEY, process.env.VOICE_APPLICATION_SECRET, requestData.headers, requestData.body, requestData.path, requestData.method);
1424
- console.log("[SINCH] Validation result:", result ? "VALID" : "INVALID");
1425
- return result;
1426
- } catch (error) {
1427
- console.error("[SINCH] Validation error:", error.message);
1428
- return false;
1429
- }
1430
- };
1431
- }
1432
- return clients;
1433
- }
1434
- var cachedClients = null;
1435
- function getSinchClients() {
1436
- if (!cachedClients) {
1437
- cachedClients = createSinchClients();
1438
- }
1439
- return cachedClients;
1440
- }
1441
- function resetSinchClients() {
1442
- cachedClients = null;
1443
- }
1444
-
1445
1529
  // ../runtime-shared/dist/ai/elevenlabs/state.js
1446
1530
  var ElevenLabsStateManager = class {
1447
1531
  state = {
@@ -2556,6 +2640,7 @@ import axios from "axios";
2556
2640
 
2557
2641
  // src/tunnel/webhook-config.ts
2558
2642
  import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
2643
+ import { randomBytes } from "crypto";
2559
2644
  var SINCH_FN_URL_PATTERN = /\.fn(-\w+)?\.sinch\.com/;
2560
2645
  function isOurWebhook(target) {
2561
2646
  return !!target && SINCH_FN_URL_PATTERN.test(target);
@@ -2563,7 +2648,7 @@ function isOurWebhook(target) {
2563
2648
  function isTunnelUrl(target) {
2564
2649
  return !!target && target.includes("tunnel.fn");
2565
2650
  }
2566
- async function configureConversationWebhooks(tunnelUrl, config) {
2651
+ async function configureConversationWebhooks(webhookUrl, config) {
2567
2652
  try {
2568
2653
  const conversationAppId = process.env.CONVERSATION_APP_ID;
2569
2654
  const projectId = process.env.PROJECT_ID;
@@ -2573,7 +2658,6 @@ async function configureConversationWebhooks(tunnelUrl, config) {
2573
2658
  console.log("\u{1F4A1} Conversation API not fully configured - skipping webhook setup");
2574
2659
  return;
2575
2660
  }
2576
- const webhookUrl = `${tunnelUrl}/webhook/conversation`;
2577
2661
  console.log(`\u{1F4AC} Conversation webhook URL: ${webhookUrl}`);
2578
2662
  const sinchClient = new SinchClient2({
2579
2663
  projectId,
@@ -2593,17 +2677,22 @@ async function configureConversationWebhooks(tunnelUrl, config) {
2593
2677
  }
2594
2678
  config.conversationWebhookId = deployedWebhook.id;
2595
2679
  config.originalTarget = deployedWebhook.target;
2680
+ const hmacSecret = randomBytes(32).toString("hex");
2681
+ process.env.CONVERSATION_WEBHOOK_SECRET = hmacSecret;
2682
+ resetSinchClients();
2596
2683
  await sinchClient.conversation.webhooks.update({
2597
2684
  webhook_id: deployedWebhook.id,
2598
2685
  webhookUpdateRequestBody: {
2599
- target: webhookUrl
2686
+ target: webhookUrl,
2687
+ secret: hmacSecret
2600
2688
  },
2601
- update_mask: ["target"]
2689
+ update_mask: ["target", "secret"]
2602
2690
  });
2603
2691
  console.log(`\u2705 Updated Conversation webhook to tunnel: ${webhookUrl}`);
2692
+ console.log("\u{1F512} HMAC secret configured for webhook signature validation");
2604
2693
  console.log("\u{1F4AC} Send a message to your Conversation app to test!");
2605
2694
  } catch (error) {
2606
- console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error.message);
2695
+ console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error instanceof Error ? error.message : error);
2607
2696
  }
2608
2697
  }
2609
2698
  async function cleanupConversationWebhook(config) {
@@ -2640,8 +2729,10 @@ async function cleanupConversationWebhook(config) {
2640
2729
  console.log(`\u{1F504} Restored webhook target to: ${restoreTarget}`);
2641
2730
  config.conversationWebhookId = void 0;
2642
2731
  config.originalTarget = void 0;
2732
+ delete process.env.CONVERSATION_WEBHOOK_SECRET;
2733
+ resetSinchClients();
2643
2734
  } catch (error) {
2644
- console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error.message);
2735
+ console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error instanceof Error ? error.message : error);
2645
2736
  }
2646
2737
  }
2647
2738
  async function configureElevenLabs() {
@@ -2656,7 +2747,7 @@ async function configureElevenLabs() {
2656
2747
  console.log("\u{1F916} ElevenLabs auto-configuration enabled");
2657
2748
  console.log(` Agent ID: ${agentId}`);
2658
2749
  } catch (error) {
2659
- console.log("\u26A0\uFE0F Could not configure ElevenLabs:", error.message);
2750
+ console.log("\u26A0\uFE0F Could not configure ElevenLabs:", error instanceof Error ? error.message : error);
2660
2751
  }
2661
2752
  }
2662
2753
 
@@ -2693,14 +2784,14 @@ var TunnelClient = class {
2693
2784
  timestampPart = ENCODING[t % 32] + timestampPart;
2694
2785
  t = Math.floor(t / 32);
2695
2786
  }
2696
- const randomBytes = new Uint8Array(10);
2697
- crypto.getRandomValues(randomBytes);
2787
+ const randomBytes2 = new Uint8Array(10);
2788
+ crypto.getRandomValues(randomBytes2);
2698
2789
  let randomPart = "";
2699
2790
  for (let i = 0; i < 10; i++) {
2700
- const byte = randomBytes[i];
2791
+ const byte = randomBytes2[i];
2701
2792
  randomPart += ENCODING[byte >> 3];
2702
2793
  if (randomPart.length < 16) {
2703
- randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ? randomBytes[i + 1] >> 6 : 0)];
2794
+ randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ? randomBytes2[i + 1] >> 6 : 0)];
2704
2795
  }
2705
2796
  }
2706
2797
  randomPart = randomPart.substring(0, 16);
@@ -2773,6 +2864,15 @@ var TunnelClient = class {
2773
2864
  break;
2774
2865
  }
2775
2866
  }
2867
+ /**
2868
+ * Build a full tunnel URL with optional sub-path and tunnel query param.
2869
+ * e.g. buildTunnelUrl('/webhook/conversation') →
2870
+ * https://tunnel.fn.sinch.com/ingress/webhook/conversation?tunnel=01KKT...
2871
+ */
2872
+ buildTunnelUrl(path6) {
2873
+ const base = this.tunnelUrl.replace(/\/$/, "");
2874
+ return `${base}${path6 || ""}?tunnel=${this.tunnelId}`;
2875
+ }
2776
2876
  handleWelcomeMessage(message) {
2777
2877
  this.tunnelId = message.tunnelId || null;
2778
2878
  this.tunnelUrl = message.publicUrl || null;
@@ -2783,7 +2883,11 @@ var TunnelClient = class {
2783
2883
  }
2784
2884
  }
2785
2885
  async handleRequest(message) {
2886
+ const verbose = process.env.VERBOSE === "true";
2786
2887
  console.log(`Forwarding ${message.method} request to ${message.path}`);
2888
+ if (verbose && message.body) {
2889
+ console.log(` \u2190 Request body: ${message.body.substring(0, 2e3)}`);
2890
+ }
2787
2891
  try {
2788
2892
  const localUrl = `http://localhost:${this.localPort}${message.path}${message.query || ""}`;
2789
2893
  const axiosConfig = {
@@ -2816,9 +2920,15 @@ var TunnelClient = class {
2816
2920
  headers,
2817
2921
  body
2818
2922
  };
2923
+ if (verbose) {
2924
+ console.log(` \u2192 Response ${response.status}: ${body.substring(0, 2e3)}`);
2925
+ }
2819
2926
  this.ws?.send(JSON.stringify(responseMessage));
2820
2927
  } catch (error) {
2821
- console.error("Error forwarding request:", error.message);
2928
+ console.error(`Error forwarding request: ${error.message} (${error.response?.status || "no response"})`);
2929
+ if (verbose && error.response?.data) {
2930
+ console.error(` \u2192 Error body: ${typeof error.response.data === "string" ? error.response.data : JSON.stringify(error.response.data)}`.substring(0, 2e3));
2931
+ }
2822
2932
  const errorResponse = {
2823
2933
  type: "response",
2824
2934
  id: message.id,
@@ -2862,7 +2972,7 @@ var TunnelClient = class {
2862
2972
  await this.configureVoiceWebhooks();
2863
2973
  }
2864
2974
  if (autoConfigConversation && process.env.CONVERSATION_APP_ID) {
2865
- await configureConversationWebhooks(this.tunnelUrl, this.webhookConfig);
2975
+ await configureConversationWebhooks(this.buildTunnelUrl("/webhook/conversation"), this.webhookConfig);
2866
2976
  }
2867
2977
  if (process.env.ELEVENLABS_AUTO_CONFIGURE === "true") {
2868
2978
  await configureElevenLabs();
@@ -2889,7 +2999,7 @@ var TunnelClient = class {
2889
2999
  updateUrl,
2890
3000
  {
2891
3001
  url: {
2892
- primary: this.tunnelUrl,
3002
+ primary: this.buildTunnelUrl(),
2893
3003
  fallback: null
2894
3004
  }
2895
3005
  },
@@ -2951,7 +3061,8 @@ var TunnelClient = class {
2951
3061
  this.isConnected = false;
2952
3062
  }
2953
3063
  getTunnelUrl() {
2954
- return this.tunnelUrl;
3064
+ if (!this.tunnelUrl || !this.tunnelId) return null;
3065
+ return this.buildTunnelUrl();
2955
3066
  }
2956
3067
  getIsConnected() {
2957
3068
  return this.isConnected;