@sinch/functions-runtime 0.3.9 → 0.4.1

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.
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin/sinch-runtime.ts
4
- import path5 from "path";
4
+ import path6 from "path";
5
5
  import { createRequire as createRequire3 } from "module";
6
6
  import { pathToFileURL as pathToFileURL2 } from "url";
7
- import fs4 from "fs";
7
+ import fs5 from "fs";
8
8
 
9
9
  // ../runtime-shared/dist/ai/connect-agent.js
10
10
  var AgentProvider;
@@ -160,6 +160,31 @@ function setupJsonParsing(app, options = {}) {
160
160
  return app;
161
161
  }
162
162
 
163
+ // ../runtime-shared/dist/auth/basic-auth.js
164
+ import { timingSafeEqual } from "crypto";
165
+ function validateBasicAuth(authHeader, expectedKey, expectedSecret) {
166
+ if (!authHeader) {
167
+ return false;
168
+ }
169
+ if (!authHeader.toLowerCase().startsWith("basic ")) {
170
+ return false;
171
+ }
172
+ const decoded = Buffer.from(authHeader.slice(6), "base64").toString("utf-8");
173
+ const colonIndex = decoded.indexOf(":");
174
+ if (colonIndex === -1) {
175
+ return false;
176
+ }
177
+ const providedKey = decoded.slice(0, colonIndex);
178
+ const providedSecret = decoded.slice(colonIndex + 1);
179
+ const expectedKeyBuf = Buffer.from(expectedKey);
180
+ const providedKeyBuf = Buffer.from(providedKey);
181
+ const expectedSecretBuf = Buffer.from(expectedSecret);
182
+ const providedSecretBuf = Buffer.from(providedSecret);
183
+ const keyMatch = expectedKeyBuf.length === providedKeyBuf.length && timingSafeEqual(expectedKeyBuf, providedKeyBuf);
184
+ const secretMatch = expectedSecretBuf.length === providedSecretBuf.length && timingSafeEqual(expectedSecretBuf, providedSecretBuf);
185
+ return keyMatch && secretMatch;
186
+ }
187
+
163
188
  // ../runtime-shared/dist/host/app.js
164
189
  import { createRequire as createRequire2 } from "module";
165
190
  import { pathToFileURL } from "url";
@@ -247,11 +272,11 @@ function isVoiceCallback(functionName) {
247
272
  function isNotificationEvent(functionName) {
248
273
  return NOTIFICATION_EVENTS.includes(functionName);
249
274
  }
250
- function extractFunctionName(path6, body) {
275
+ function extractFunctionName(path7, body) {
251
276
  if (body?.event && isVoiceCallback(body.event)) {
252
277
  return body.event;
253
278
  }
254
- const pathname = path6.split("?")[0];
279
+ const pathname = path7.split("?")[0];
255
280
  const segments = pathname.split("/").filter((s) => s && s !== "*");
256
281
  if (segments.length === 1 && isVoiceCallback(segments[0])) {
257
282
  return segments[0];
@@ -268,10 +293,10 @@ function generateRequestId() {
268
293
  return `req_${Date.now()}_${Math.random().toString(36).substring(7)}`;
269
294
  }
270
295
  function findFunctionPath() {
271
- const fs5 = requireCjs2("fs");
296
+ const fs6 = requireCjs2("fs");
272
297
  const distPath = nodePath.join(process.cwd(), "dist", "function.js");
273
298
  const rootPath = nodePath.join(process.cwd(), "function.js");
274
- if (fs5.existsSync(distPath)) {
299
+ if (fs6.existsSync(distPath)) {
275
300
  return distPath;
276
301
  }
277
302
  return rootPath;
@@ -349,6 +374,15 @@ var noOpCache = {
349
374
  keys: async () => [],
350
375
  getMany: async () => ({})
351
376
  };
377
+ var noOpStorage = {
378
+ write: async () => {
379
+ },
380
+ read: async () => Buffer.alloc(0),
381
+ list: async () => [],
382
+ exists: async () => false,
383
+ delete: async () => {
384
+ }
385
+ };
352
386
  function buildBaseContext(req, config = {}) {
353
387
  return {
354
388
  requestId: req.headers["x-request-id"] || generateRequestId(),
@@ -361,6 +395,8 @@ function buildBaseContext(req, config = {}) {
361
395
  variables: config.variables
362
396
  },
363
397
  cache: noOpCache,
398
+ storage: noOpStorage,
399
+ database: "",
364
400
  assets: (filename) => {
365
401
  const filePath = nodePath.join(process.cwd(), "assets", filename);
366
402
  return nodeFs.promises.readFile(filePath, "utf-8");
@@ -451,7 +487,7 @@ function setupRequestHandler(app, options = {}) {
451
487
  const functionUrl = pathToFileURL(functionPath).href;
452
488
  const module = await import(functionUrl);
453
489
  return module.default || module;
454
- }, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, onRequestStart = () => {
490
+ }, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, authConfig, authKey, authSecret, onRequestStart = () => {
455
491
  }, onRequestEnd = () => {
456
492
  } } = options;
457
493
  app.use("/{*splat}", async (req, res) => {
@@ -479,6 +515,17 @@ function setupRequestHandler(app, options = {}) {
479
515
  try {
480
516
  const functionName = extractFunctionName(req.originalUrl, req.body);
481
517
  logger(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${req.method} ${req.path} -> ${functionName}`);
518
+ if (authConfig && authKey && authSecret) {
519
+ const needsAuth = authConfig === "*" || Array.isArray(authConfig) && authConfig.includes(functionName);
520
+ if (needsAuth) {
521
+ const isValid = validateBasicAuth(req.headers.authorization, authKey, authSecret);
522
+ if (!isValid) {
523
+ logger(`[AUTH] Rejected unauthorized request to ${functionName}`);
524
+ res.status(401).set("WWW-Authenticate", 'Basic realm="sinch-function"').json({ error: "Unauthorized" });
525
+ return;
526
+ }
527
+ }
528
+ }
482
529
  onRequestStart({ functionName, req });
483
530
  const context = buildContext(req);
484
531
  const userFunction = await Promise.resolve(loadUserFunction());
@@ -763,10 +810,306 @@ function createCacheClient(_projectId, _functionName) {
763
810
  return new LocalCache();
764
811
  }
765
812
 
813
+ // src/storage/local.ts
814
+ import * as fs3 from "fs/promises";
815
+ import * as path4 from "path";
816
+ var LocalStorage = class {
817
+ baseDir;
818
+ constructor(baseDir) {
819
+ this.baseDir = baseDir ?? path4.join(process.cwd(), ".sinch", "storage");
820
+ }
821
+ resolvePath(key) {
822
+ const sanitized = key.replace(/^\/+/, "").replace(/\.\./g, "_");
823
+ return path4.join(this.baseDir, sanitized);
824
+ }
825
+ async write(key, data) {
826
+ const filePath = this.resolvePath(key);
827
+ await fs3.mkdir(path4.dirname(filePath), { recursive: true });
828
+ await fs3.writeFile(filePath, data);
829
+ }
830
+ async read(key) {
831
+ const filePath = this.resolvePath(key);
832
+ return fs3.readFile(filePath);
833
+ }
834
+ async list(prefix) {
835
+ const results = [];
836
+ await this.walkDir(this.baseDir, "", results);
837
+ if (prefix) {
838
+ return results.filter((f) => f.startsWith(prefix));
839
+ }
840
+ return results;
841
+ }
842
+ async exists(key) {
843
+ const filePath = this.resolvePath(key);
844
+ try {
845
+ await fs3.access(filePath);
846
+ return true;
847
+ } catch {
848
+ return false;
849
+ }
850
+ }
851
+ async delete(key) {
852
+ const filePath = this.resolvePath(key);
853
+ await fs3.rm(filePath, { force: true });
854
+ }
855
+ async walkDir(dir, relative, results) {
856
+ let entries;
857
+ try {
858
+ entries = await fs3.readdir(dir, { withFileTypes: true });
859
+ } catch {
860
+ return;
861
+ }
862
+ for (const entry of entries) {
863
+ const rel = relative ? `${relative}/${entry.name}` : entry.name;
864
+ if (entry.isDirectory()) {
865
+ await this.walkDir(path4.join(dir, entry.name), rel, results);
866
+ } else {
867
+ results.push(rel);
868
+ }
869
+ }
870
+ }
871
+ };
872
+ function createStorageClient(baseDir) {
873
+ return new LocalStorage(baseDir);
874
+ }
875
+
766
876
  // src/secrets/index.ts
767
- import fs3 from "fs";
768
- import path4 from "path";
877
+ import fs4 from "fs";
878
+ import path5 from "path";
769
879
  import os from "os";
880
+
881
+ // src/secrets/keychain.ts
882
+ import { execFile, spawn } from "child_process";
883
+ import { promisify } from "util";
884
+ var execFileAsync = promisify(execFile);
885
+ function b64(value) {
886
+ return Buffer.from(value, "utf8").toString("base64");
887
+ }
888
+ function psParam(name, value) {
889
+ return `$${name} = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${b64(value)}'));`;
890
+ }
891
+ var WIN32_CRED_READ_SCRIPT = `
892
+ Add-Type -TypeDefinition @'
893
+ using System;
894
+ using System.Runtime.InteropServices;
895
+ public class CredManager {
896
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
897
+ private struct CREDENTIAL {
898
+ public int Flags;
899
+ public int Type;
900
+ public IntPtr TargetName;
901
+ public IntPtr Comment;
902
+ public long LastWritten;
903
+ public int CredentialBlobSize;
904
+ public IntPtr CredentialBlob;
905
+ public int Persist;
906
+ public int AttributeCount;
907
+ public IntPtr Attributes;
908
+ public IntPtr TargetAlias;
909
+ public IntPtr UserName;
910
+ }
911
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
912
+ private static extern bool CredReadW(string target, int type, int flags, out IntPtr cred);
913
+ [DllImport("advapi32.dll")]
914
+ private static extern void CredFree(IntPtr cred);
915
+ public static string Read(string target) {
916
+ IntPtr credPtr;
917
+ if (!CredReadW(target, 1, 0, out credPtr)) return null;
918
+ try {
919
+ CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(credPtr, typeof(CREDENTIAL));
920
+ if (c.CredentialBlobSize > 0 && c.CredentialBlob != IntPtr.Zero)
921
+ return Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
922
+ return "";
923
+ } finally { CredFree(credPtr); }
924
+ }
925
+ }
926
+ '@
927
+ $r = [CredManager]::Read($target)
928
+ if ($r -ne $null) { [Console]::Write($r) }
929
+ else { exit 1 }
930
+ `;
931
+ var WIN32_CRED_WRITE_SCRIPT = `
932
+ Add-Type -TypeDefinition @'
933
+ using System;
934
+ using System.Runtime.InteropServices;
935
+ using System.Text;
936
+ public class CredWriter {
937
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
938
+ private struct CREDENTIAL {
939
+ public int Flags;
940
+ public int Type;
941
+ public string TargetName;
942
+ public string Comment;
943
+ public long LastWritten;
944
+ public int CredentialBlobSize;
945
+ public IntPtr CredentialBlob;
946
+ public int Persist;
947
+ public int AttributeCount;
948
+ public IntPtr Attributes;
949
+ public string TargetAlias;
950
+ public string UserName;
951
+ }
952
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
953
+ private static extern bool CredWriteW(ref CREDENTIAL cred, int flags);
954
+ public static bool Write(string target, string password) {
955
+ byte[] blob = Encoding.Unicode.GetBytes(password);
956
+ CREDENTIAL c = new CREDENTIAL();
957
+ c.Type = 1;
958
+ c.TargetName = target;
959
+ c.CredentialBlobSize = blob.Length;
960
+ c.CredentialBlob = Marshal.AllocHGlobal(blob.Length);
961
+ Marshal.Copy(blob, 0, c.CredentialBlob, blob.Length);
962
+ c.Persist = 2;
963
+ try { return CredWriteW(ref c, 0); }
964
+ finally { Marshal.FreeHGlobal(c.CredentialBlob); }
965
+ }
966
+ }
967
+ '@
968
+ if (-not [CredWriter]::Write($target, $password)) { exit 1 }
969
+ `;
970
+ var WIN32_CRED_DELETE_SCRIPT = `
971
+ Add-Type -TypeDefinition @'
972
+ using System;
973
+ using System.Runtime.InteropServices;
974
+ public class CredDeleter {
975
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
976
+ private static extern bool CredDeleteW(string target, int type, int flags);
977
+ public static bool Delete(string target) { return CredDeleteW(target, 1, 0); }
978
+ }
979
+ '@
980
+ if (-not [CredDeleter]::Delete($target)) { exit 1 }
981
+ `;
982
+ function winTarget(service, account) {
983
+ return `${service}/${account}`;
984
+ }
985
+ var windowsKeychain = {
986
+ async getPassword(service, account) {
987
+ try {
988
+ const params = psParam("target", winTarget(service, account));
989
+ const { stdout } = await execFileAsync(
990
+ "powershell.exe",
991
+ ["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_READ_SCRIPT],
992
+ { timeout: 15e3, windowsHide: true }
993
+ );
994
+ return stdout;
995
+ } catch {
996
+ return null;
997
+ }
998
+ },
999
+ async setPassword(service, account, password) {
1000
+ const params = psParam("target", winTarget(service, account)) + psParam("password", password);
1001
+ await execFileAsync(
1002
+ "powershell.exe",
1003
+ ["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_WRITE_SCRIPT],
1004
+ { timeout: 15e3, windowsHide: true }
1005
+ );
1006
+ },
1007
+ async deletePassword(service, account) {
1008
+ try {
1009
+ const params = psParam("target", winTarget(service, account));
1010
+ await execFileAsync(
1011
+ "powershell.exe",
1012
+ ["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_DELETE_SCRIPT],
1013
+ { timeout: 15e3, windowsHide: true }
1014
+ );
1015
+ return true;
1016
+ } catch {
1017
+ return false;
1018
+ }
1019
+ }
1020
+ };
1021
+ var macKeychain = {
1022
+ async getPassword(service, account) {
1023
+ try {
1024
+ const { stdout } = await execFileAsync(
1025
+ "security",
1026
+ ["find-generic-password", "-s", service, "-a", account, "-w"],
1027
+ { timeout: 15e3 }
1028
+ );
1029
+ return stdout.trimEnd();
1030
+ } catch {
1031
+ return null;
1032
+ }
1033
+ },
1034
+ async setPassword(service, account, password) {
1035
+ await execFileAsync(
1036
+ "security",
1037
+ ["add-generic-password", "-U", "-s", service, "-a", account, "-w", password],
1038
+ { timeout: 15e3 }
1039
+ );
1040
+ },
1041
+ async deletePassword(service, account) {
1042
+ try {
1043
+ await execFileAsync(
1044
+ "security",
1045
+ ["delete-generic-password", "-s", service, "-a", account],
1046
+ { timeout: 15e3 }
1047
+ );
1048
+ return true;
1049
+ } catch {
1050
+ return false;
1051
+ }
1052
+ }
1053
+ };
1054
+ var linuxKeychain = {
1055
+ async getPassword(service, account) {
1056
+ try {
1057
+ const { stdout } = await execFileAsync(
1058
+ "secret-tool",
1059
+ ["lookup", "service", service, "account", account],
1060
+ { timeout: 15e3 }
1061
+ );
1062
+ return stdout.trimEnd();
1063
+ } catch {
1064
+ return null;
1065
+ }
1066
+ },
1067
+ // secret-tool reads password from stdin (avoids exposing it in process args)
1068
+ async setPassword(service, account, password) {
1069
+ const child = spawn(
1070
+ "secret-tool",
1071
+ ["store", "--label", `${service}/${account}`, "service", service, "account", account],
1072
+ { stdio: ["pipe", "pipe", "pipe"] }
1073
+ );
1074
+ child.stdin.write(password);
1075
+ child.stdin.end();
1076
+ await new Promise((resolve, reject) => {
1077
+ child.on(
1078
+ "close",
1079
+ (code) => code === 0 ? resolve() : reject(new Error("secret-tool store failed"))
1080
+ );
1081
+ child.on("error", reject);
1082
+ });
1083
+ },
1084
+ async deletePassword(service, account) {
1085
+ try {
1086
+ await execFileAsync(
1087
+ "secret-tool",
1088
+ ["clear", "service", service, "account", account],
1089
+ { timeout: 15e3 }
1090
+ );
1091
+ return true;
1092
+ } catch {
1093
+ return false;
1094
+ }
1095
+ }
1096
+ };
1097
+ function getBackend() {
1098
+ switch (process.platform) {
1099
+ case "win32":
1100
+ return windowsKeychain;
1101
+ case "darwin":
1102
+ return macKeychain;
1103
+ default:
1104
+ return linuxKeychain;
1105
+ }
1106
+ }
1107
+ var backend = getBackend();
1108
+ async function getPassword(service, account) {
1109
+ return backend.getPassword(service, account);
1110
+ }
1111
+
1112
+ // src/secrets/index.ts
770
1113
  var SecretsLoader = class {
771
1114
  // Same service name as CLI uses
772
1115
  SERVICE_NAME = "sinch-functions-cli";
@@ -780,24 +1123,12 @@ var SecretsLoader = class {
780
1123
  return false;
781
1124
  }
782
1125
  try {
783
- let keytar;
784
- try {
785
- keytar = await import("keytar");
786
- } catch (error) {
787
- if (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_MODULE_NOT_FOUND") {
788
- console.debug("[Secrets] Keytar not available - secrets not loaded");
789
- return false;
790
- } else {
791
- console.error("[Secrets] Error loading keytar:", error.message);
792
- }
793
- return false;
794
- }
795
- const envPath = path4.join(process.cwd(), ".env");
796
- if (!fs3.existsSync(envPath)) {
1126
+ const envPath = path5.join(process.cwd(), ".env");
1127
+ if (!fs4.existsSync(envPath)) {
797
1128
  console.debug("[Secrets] No .env file found, skipping keychain load");
798
1129
  return false;
799
1130
  }
800
- const envContent = fs3.readFileSync(envPath, "utf8");
1131
+ const envContent = fs4.readFileSync(envPath, "utf8");
801
1132
  const envLines = envContent.replace(/\r\n/g, "\n").split("\n");
802
1133
  const secretsToLoad = [];
803
1134
  envLines.forEach((line) => {
@@ -819,7 +1150,7 @@ var SecretsLoader = class {
819
1150
  }
820
1151
  let secretsLoaded = 0;
821
1152
  if (secretsToLoad.includes("PROJECT_ID_API_SECRET")) {
822
- const apiSecret = await keytar.getPassword(this.SERVICE_NAME, `${this.username}-keySecret`);
1153
+ const apiSecret = await getPassword(this.SERVICE_NAME, `${this.username}-keySecret`);
823
1154
  if (apiSecret) {
824
1155
  process.env.PROJECT_ID_API_SECRET = apiSecret;
825
1156
  console.log("\u2705 Loaded PROJECT_ID_API_SECRET from secure storage");
@@ -829,7 +1160,7 @@ var SecretsLoader = class {
829
1160
  if (secretsToLoad.includes("VOICE_APPLICATION_SECRET")) {
830
1161
  const applicationKey = process.env.VOICE_APPLICATION_KEY || this.getApplicationKeyFromConfig();
831
1162
  if (applicationKey) {
832
- const appSecret = await keytar.getPassword(this.SERVICE_NAME, applicationKey);
1163
+ const appSecret = await getPassword(this.SERVICE_NAME, applicationKey);
833
1164
  if (appSecret) {
834
1165
  process.env.VOICE_APPLICATION_SECRET = appSecret;
835
1166
  console.log("\u2705 Loaded VOICE_APPLICATION_SECRET from secure storage");
@@ -843,7 +1174,7 @@ var SecretsLoader = class {
843
1174
  continue;
844
1175
  }
845
1176
  if (functionName) {
846
- const value = await keytar.getPassword(
1177
+ const value = await getPassword(
847
1178
  this.SERVICE_NAME,
848
1179
  `${functionName}-${secretName}`
849
1180
  );
@@ -871,9 +1202,9 @@ var SecretsLoader = class {
871
1202
  */
872
1203
  getApplicationKeyFromConfig() {
873
1204
  try {
874
- const sinchJsonPath = path4.join(process.cwd(), "sinch.json");
875
- if (fs3.existsSync(sinchJsonPath)) {
876
- const sinchConfig = JSON.parse(fs3.readFileSync(sinchJsonPath, "utf8"));
1205
+ const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
1206
+ if (fs4.existsSync(sinchJsonPath)) {
1207
+ const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
877
1208
  return sinchConfig.voiceAppId || sinchConfig.applicationKey || null;
878
1209
  }
879
1210
  } catch (error) {
@@ -886,9 +1217,9 @@ var SecretsLoader = class {
886
1217
  */
887
1218
  getFunctionNameFromConfig() {
888
1219
  try {
889
- const sinchJsonPath = path4.join(process.cwd(), "sinch.json");
890
- if (fs3.existsSync(sinchJsonPath)) {
891
- const sinchConfig = JSON.parse(fs3.readFileSync(sinchJsonPath, "utf8"));
1220
+ const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
1221
+ if (fs4.existsSync(sinchJsonPath)) {
1222
+ const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
892
1223
  return sinchConfig.name || null;
893
1224
  }
894
1225
  } catch (error) {
@@ -902,14 +1233,13 @@ var SecretsLoader = class {
902
1233
  async loadCustomSecrets(secretNames = []) {
903
1234
  const secrets = {};
904
1235
  try {
905
- const keytar = await import("keytar");
906
1236
  const functionName = this.getFunctionNameFromConfig();
907
1237
  if (!functionName) {
908
1238
  console.debug("[Secrets] Could not determine function name for custom secrets");
909
1239
  return secrets;
910
1240
  }
911
1241
  for (const secretName of secretNames) {
912
- const value = await keytar.getPassword(this.SERVICE_NAME, `${functionName}-${secretName}`);
1242
+ const value = await getPassword(this.SERVICE_NAME, `${functionName}-${secretName}`);
913
1243
  if (value) {
914
1244
  secrets[secretName] = value;
915
1245
  process.env[secretName] = value;
@@ -921,15 +1251,13 @@ var SecretsLoader = class {
921
1251
  return secrets;
922
1252
  }
923
1253
  /**
924
- * Check if keytar is available
1254
+ * Check if the native keychain is available.
1255
+ * Always true — no native npm deps required. The underlying OS tool
1256
+ * (secret-tool, security, CredManager) may still be absent, in which
1257
+ * case getPassword() silently returns null per credential.
925
1258
  */
926
1259
  async isAvailable() {
927
- try {
928
- await import("keytar");
929
- return true;
930
- } catch {
931
- return false;
932
- }
1260
+ return true;
933
1261
  }
934
1262
  };
935
1263
  var secretsLoader = new SecretsLoader();
@@ -940,6 +1268,13 @@ import axios from "axios";
940
1268
 
941
1269
  // src/tunnel/webhook-config.ts
942
1270
  import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
1271
+ var SINCH_FN_URL_PATTERN = /\.fn(-\w+)?\.sinch\.com/;
1272
+ function isOurWebhook(target) {
1273
+ return !!target && SINCH_FN_URL_PATTERN.test(target);
1274
+ }
1275
+ function isTunnelUrl(target) {
1276
+ return !!target && target.includes("tunnel.fn");
1277
+ }
943
1278
  async function configureConversationWebhooks(tunnelUrl, config) {
944
1279
  try {
945
1280
  const conversationAppId = process.env.CONVERSATION_APP_ID;
@@ -961,24 +1296,23 @@ async function configureConversationWebhooks(tunnelUrl, config) {
961
1296
  app_id: conversationAppId
962
1297
  });
963
1298
  const existingWebhooks = webhooksResult.webhooks || [];
964
- const tunnelWebhooks = existingWebhooks.filter((w) => w.target?.includes("/api/ingress/"));
965
- for (const staleWebhook of tunnelWebhooks) {
966
- try {
967
- await sinchClient.conversation.webhooks.delete({ webhook_id: staleWebhook.id });
968
- console.log(`\u{1F9F9} Cleaned up stale tunnel webhook: ${staleWebhook.id}`);
969
- } catch (err) {
970
- }
1299
+ const deployedWebhook = existingWebhooks.find(
1300
+ (w) => isOurWebhook(w.target)
1301
+ );
1302
+ if (!deployedWebhook || !deployedWebhook.id) {
1303
+ console.log("\u26A0\uFE0F No deployed webhook found \u2014 deploy first");
1304
+ return;
971
1305
  }
972
- const createResult = await sinchClient.conversation.webhooks.create({
973
- webhookCreateRequestBody: {
974
- app_id: conversationAppId,
975
- target: webhookUrl,
976
- target_type: "HTTP",
977
- triggers: ["MESSAGE_INBOUND"]
978
- }
1306
+ config.conversationWebhookId = deployedWebhook.id;
1307
+ config.originalTarget = deployedWebhook.target;
1308
+ await sinchClient.conversation.webhooks.update({
1309
+ webhook_id: deployedWebhook.id,
1310
+ webhookUpdateRequestBody: {
1311
+ target: webhookUrl
1312
+ },
1313
+ update_mask: ["target"]
979
1314
  });
980
- config.conversationWebhookId = createResult.id;
981
- console.log(`\u2705 Created Conversation webhook: ${webhookUrl}`);
1315
+ console.log(`\u2705 Updated Conversation webhook to tunnel: ${webhookUrl}`);
982
1316
  console.log("\u{1F4AC} Send a message to your Conversation app to test!");
983
1317
  } catch (error) {
984
1318
  console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error.message);
@@ -997,10 +1331,29 @@ async function cleanupConversationWebhook(config) {
997
1331
  keyId,
998
1332
  keySecret
999
1333
  });
1000
- await sinchClient.conversation.webhooks.delete({ webhook_id: config.conversationWebhookId });
1001
- console.log("\u{1F9F9} Cleaned up tunnel webhook");
1334
+ let restoreTarget = config.originalTarget;
1335
+ if (!restoreTarget || isTunnelUrl(restoreTarget)) {
1336
+ const functionName = process.env.FUNCTION_NAME || process.env.FUNCTION_ID;
1337
+ if (functionName) {
1338
+ restoreTarget = `https://${functionName}.fn-dev.sinch.com/webhook/conversation`;
1339
+ console.log(`\u{1F527} Derived restore target from env: ${restoreTarget}`);
1340
+ } else {
1341
+ console.log("\u26A0\uFE0F Cannot restore webhook \u2014 no FUNCTION_NAME or FUNCTION_ID available");
1342
+ return;
1343
+ }
1344
+ }
1345
+ await sinchClient.conversation.webhooks.update({
1346
+ webhook_id: config.conversationWebhookId,
1347
+ webhookUpdateRequestBody: {
1348
+ target: restoreTarget
1349
+ },
1350
+ update_mask: ["target"]
1351
+ });
1352
+ console.log(`\u{1F504} Restored webhook target to: ${restoreTarget}`);
1002
1353
  config.conversationWebhookId = void 0;
1354
+ config.originalTarget = void 0;
1003
1355
  } catch (error) {
1356
+ console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error.message);
1004
1357
  }
1005
1358
  }
1006
1359
  async function configureElevenLabs() {
@@ -1320,9 +1673,9 @@ var TunnelClient = class {
1320
1673
  // src/bin/sinch-runtime.ts
1321
1674
  var requireCjs3 = createRequire3(import.meta.url);
1322
1675
  function findFunctionPath3() {
1323
- const distPath = path5.join(process.cwd(), "dist", "function.js");
1324
- const rootPath = path5.join(process.cwd(), "function.js");
1325
- if (fs4.existsSync(distPath)) {
1676
+ const distPath = path6.join(process.cwd(), "dist", "function.js");
1677
+ const rootPath = path6.join(process.cwd(), "function.js");
1678
+ if (fs5.existsSync(distPath)) {
1326
1679
  return distPath;
1327
1680
  }
1328
1681
  return rootPath;
@@ -1334,6 +1687,8 @@ function loadRuntimeConfig() {
1334
1687
  functionId: process.env.FUNCTION_ID || process.env.SINCH_FUNCTION_ID || "local-dev"
1335
1688
  };
1336
1689
  }
1690
+ var storage = createStorageClient();
1691
+ var databasePath = path6.join(process.cwd(), ".sinch", "data", "app.db");
1337
1692
  function buildLocalContext(req, runtimeConfig) {
1338
1693
  const baseContext = buildBaseContext(req);
1339
1694
  const cache = createCacheClient();
@@ -1341,6 +1696,8 @@ function buildLocalContext(req, runtimeConfig) {
1341
1696
  return {
1342
1697
  ...baseContext,
1343
1698
  cache,
1699
+ storage,
1700
+ database: databasePath,
1344
1701
  ...sinchClients,
1345
1702
  env: process.env,
1346
1703
  config: {
@@ -1367,12 +1724,12 @@ function displayStartupInfo(config, verbose, _port) {
1367
1724
  function displayEnvironmentVariables() {
1368
1725
  console.log("\nEnvironment Variables:");
1369
1726
  try {
1370
- const envPath = path5.join(process.cwd(), ".env");
1371
- if (!fs4.existsSync(envPath)) {
1727
+ const envPath = path6.join(process.cwd(), ".env");
1728
+ if (!fs5.existsSync(envPath)) {
1372
1729
  console.log(" (no .env file found)");
1373
1730
  return;
1374
1731
  }
1375
- const envContent = fs4.readFileSync(envPath, "utf8");
1732
+ const envContent = fs5.readFileSync(envPath, "utf8");
1376
1733
  const envLines = envContent.split("\n");
1377
1734
  const variables = [];
1378
1735
  const secrets = [];
@@ -1432,7 +1789,7 @@ function displayApplicationCredentials() {
1432
1789
  async function displayDetectedFunctions() {
1433
1790
  try {
1434
1791
  const functionPath = findFunctionPath3();
1435
- if (!fs4.existsSync(functionPath)) return;
1792
+ if (!fs5.existsSync(functionPath)) return;
1436
1793
  const functionUrl = pathToFileURL2(functionPath).href;
1437
1794
  const module = await import(functionUrl);
1438
1795
  const userFunction = module.default || module;
@@ -1459,18 +1816,34 @@ async function main() {
1459
1816
  } catch {
1460
1817
  }
1461
1818
  await secretsLoader.loadFromKeychain();
1819
+ fs5.mkdirSync(path6.join(process.cwd(), ".sinch", "storage"), { recursive: true });
1820
+ fs5.mkdirSync(path6.join(process.cwd(), ".sinch", "data"), { recursive: true });
1462
1821
  const config = loadRuntimeConfig();
1463
1822
  const staticDir = process.env.STATIC_DIR;
1464
1823
  const landingPageEnabled = process.env.LANDING_PAGE_ENABLED !== "false";
1465
1824
  const app = createApp({ staticDir, landingPageEnabled });
1825
+ const authKey = process.env.PROJECT_ID_API_KEY;
1826
+ const authSecret = process.env.PROJECT_ID_API_SECRET;
1827
+ let userAuthConfig;
1828
+ const loadUserFunction = async () => {
1829
+ const functionPath = findFunctionPath3();
1830
+ const functionUrl = pathToFileURL2(functionPath).href;
1831
+ const module = await import(functionUrl);
1832
+ if (userAuthConfig === void 0) {
1833
+ userAuthConfig = module.auth || module.default?.auth;
1834
+ if (userAuthConfig && verbose) {
1835
+ console.log(`[AUTH] Auth config loaded: ${JSON.stringify(userAuthConfig)}`);
1836
+ }
1837
+ }
1838
+ return module.default || module;
1839
+ };
1840
+ await loadUserFunction();
1466
1841
  setupRequestHandler(app, {
1467
1842
  landingPageEnabled,
1468
- loadUserFunction: async () => {
1469
- const functionPath = findFunctionPath3();
1470
- const functionUrl = pathToFileURL2(functionPath).href;
1471
- const module = await import(functionUrl);
1472
- return module.default || module;
1473
- },
1843
+ authConfig: userAuthConfig,
1844
+ authKey,
1845
+ authSecret,
1846
+ loadUserFunction,
1474
1847
  buildContext: (req) => buildLocalContext(req, config),
1475
1848
  logger: console.log,
1476
1849
  onRequestStart: ({ req }) => {