@sinch/functions-runtime 0.3.8 → 0.4.0

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;
@@ -247,11 +247,11 @@ function isVoiceCallback(functionName) {
247
247
  function isNotificationEvent(functionName) {
248
248
  return NOTIFICATION_EVENTS.includes(functionName);
249
249
  }
250
- function extractFunctionName(path6, body) {
250
+ function extractFunctionName(path7, body) {
251
251
  if (body?.event && isVoiceCallback(body.event)) {
252
252
  return body.event;
253
253
  }
254
- const pathname = path6.split("?")[0];
254
+ const pathname = path7.split("?")[0];
255
255
  const segments = pathname.split("/").filter((s) => s && s !== "*");
256
256
  if (segments.length === 1 && isVoiceCallback(segments[0])) {
257
257
  return segments[0];
@@ -268,10 +268,10 @@ function generateRequestId() {
268
268
  return `req_${Date.now()}_${Math.random().toString(36).substring(7)}`;
269
269
  }
270
270
  function findFunctionPath() {
271
- const fs5 = requireCjs2("fs");
271
+ const fs6 = requireCjs2("fs");
272
272
  const distPath = nodePath.join(process.cwd(), "dist", "function.js");
273
273
  const rootPath = nodePath.join(process.cwd(), "function.js");
274
- if (fs5.existsSync(distPath)) {
274
+ if (fs6.existsSync(distPath)) {
275
275
  return distPath;
276
276
  }
277
277
  return rootPath;
@@ -349,6 +349,15 @@ var noOpCache = {
349
349
  keys: async () => [],
350
350
  getMany: async () => ({})
351
351
  };
352
+ var noOpStorage = {
353
+ write: async () => {
354
+ },
355
+ read: async () => Buffer.alloc(0),
356
+ list: async () => [],
357
+ exists: async () => false,
358
+ delete: async () => {
359
+ }
360
+ };
352
361
  function buildBaseContext(req, config = {}) {
353
362
  return {
354
363
  requestId: req.headers["x-request-id"] || generateRequestId(),
@@ -361,6 +370,8 @@ function buildBaseContext(req, config = {}) {
361
370
  variables: config.variables
362
371
  },
363
372
  cache: noOpCache,
373
+ storage: noOpStorage,
374
+ database: "",
364
375
  assets: (filename) => {
365
376
  const filePath = nodePath.join(process.cwd(), "assets", filename);
366
377
  return nodeFs.promises.readFile(filePath, "utf-8");
@@ -763,10 +774,306 @@ function createCacheClient(_projectId, _functionName) {
763
774
  return new LocalCache();
764
775
  }
765
776
 
777
+ // src/storage/local.ts
778
+ import * as fs3 from "fs/promises";
779
+ import * as path4 from "path";
780
+ var LocalStorage = class {
781
+ baseDir;
782
+ constructor(baseDir) {
783
+ this.baseDir = baseDir ?? path4.join(process.cwd(), "storage");
784
+ }
785
+ resolvePath(key) {
786
+ const sanitized = key.replace(/^\/+/, "").replace(/\.\./g, "_");
787
+ return path4.join(this.baseDir, sanitized);
788
+ }
789
+ async write(key, data) {
790
+ const filePath = this.resolvePath(key);
791
+ await fs3.mkdir(path4.dirname(filePath), { recursive: true });
792
+ await fs3.writeFile(filePath, data);
793
+ }
794
+ async read(key) {
795
+ const filePath = this.resolvePath(key);
796
+ return fs3.readFile(filePath);
797
+ }
798
+ async list(prefix) {
799
+ const results = [];
800
+ await this.walkDir(this.baseDir, "", results);
801
+ if (prefix) {
802
+ return results.filter((f) => f.startsWith(prefix));
803
+ }
804
+ return results;
805
+ }
806
+ async exists(key) {
807
+ const filePath = this.resolvePath(key);
808
+ try {
809
+ await fs3.access(filePath);
810
+ return true;
811
+ } catch {
812
+ return false;
813
+ }
814
+ }
815
+ async delete(key) {
816
+ const filePath = this.resolvePath(key);
817
+ await fs3.rm(filePath, { force: true });
818
+ }
819
+ async walkDir(dir, relative, results) {
820
+ let entries;
821
+ try {
822
+ entries = await fs3.readdir(dir, { withFileTypes: true });
823
+ } catch {
824
+ return;
825
+ }
826
+ for (const entry of entries) {
827
+ const rel = relative ? `${relative}/${entry.name}` : entry.name;
828
+ if (entry.isDirectory()) {
829
+ await this.walkDir(path4.join(dir, entry.name), rel, results);
830
+ } else {
831
+ results.push(rel);
832
+ }
833
+ }
834
+ }
835
+ };
836
+ function createStorageClient(baseDir) {
837
+ return new LocalStorage(baseDir);
838
+ }
839
+
766
840
  // src/secrets/index.ts
767
- import fs3 from "fs";
768
- import path4 from "path";
841
+ import fs4 from "fs";
842
+ import path5 from "path";
769
843
  import os from "os";
844
+
845
+ // src/secrets/keychain.ts
846
+ import { execFile, spawn } from "child_process";
847
+ import { promisify } from "util";
848
+ var execFileAsync = promisify(execFile);
849
+ function b64(value) {
850
+ return Buffer.from(value, "utf8").toString("base64");
851
+ }
852
+ function psParam(name, value) {
853
+ return `$${name} = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${b64(value)}'));`;
854
+ }
855
+ var WIN32_CRED_READ_SCRIPT = `
856
+ Add-Type -TypeDefinition @'
857
+ using System;
858
+ using System.Runtime.InteropServices;
859
+ public class CredManager {
860
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
861
+ private struct CREDENTIAL {
862
+ public int Flags;
863
+ public int Type;
864
+ public IntPtr TargetName;
865
+ public IntPtr Comment;
866
+ public long LastWritten;
867
+ public int CredentialBlobSize;
868
+ public IntPtr CredentialBlob;
869
+ public int Persist;
870
+ public int AttributeCount;
871
+ public IntPtr Attributes;
872
+ public IntPtr TargetAlias;
873
+ public IntPtr UserName;
874
+ }
875
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
876
+ private static extern bool CredReadW(string target, int type, int flags, out IntPtr cred);
877
+ [DllImport("advapi32.dll")]
878
+ private static extern void CredFree(IntPtr cred);
879
+ public static string Read(string target) {
880
+ IntPtr credPtr;
881
+ if (!CredReadW(target, 1, 0, out credPtr)) return null;
882
+ try {
883
+ CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(credPtr, typeof(CREDENTIAL));
884
+ if (c.CredentialBlobSize > 0 && c.CredentialBlob != IntPtr.Zero)
885
+ return Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
886
+ return "";
887
+ } finally { CredFree(credPtr); }
888
+ }
889
+ }
890
+ '@
891
+ $r = [CredManager]::Read($target)
892
+ if ($r -ne $null) { [Console]::Write($r) }
893
+ else { exit 1 }
894
+ `;
895
+ var WIN32_CRED_WRITE_SCRIPT = `
896
+ Add-Type -TypeDefinition @'
897
+ using System;
898
+ using System.Runtime.InteropServices;
899
+ using System.Text;
900
+ public class CredWriter {
901
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
902
+ private struct CREDENTIAL {
903
+ public int Flags;
904
+ public int Type;
905
+ public string TargetName;
906
+ public string Comment;
907
+ public long LastWritten;
908
+ public int CredentialBlobSize;
909
+ public IntPtr CredentialBlob;
910
+ public int Persist;
911
+ public int AttributeCount;
912
+ public IntPtr Attributes;
913
+ public string TargetAlias;
914
+ public string UserName;
915
+ }
916
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
917
+ private static extern bool CredWriteW(ref CREDENTIAL cred, int flags);
918
+ public static bool Write(string target, string password) {
919
+ byte[] blob = Encoding.Unicode.GetBytes(password);
920
+ CREDENTIAL c = new CREDENTIAL();
921
+ c.Type = 1;
922
+ c.TargetName = target;
923
+ c.CredentialBlobSize = blob.Length;
924
+ c.CredentialBlob = Marshal.AllocHGlobal(blob.Length);
925
+ Marshal.Copy(blob, 0, c.CredentialBlob, blob.Length);
926
+ c.Persist = 2;
927
+ try { return CredWriteW(ref c, 0); }
928
+ finally { Marshal.FreeHGlobal(c.CredentialBlob); }
929
+ }
930
+ }
931
+ '@
932
+ if (-not [CredWriter]::Write($target, $password)) { exit 1 }
933
+ `;
934
+ var WIN32_CRED_DELETE_SCRIPT = `
935
+ Add-Type -TypeDefinition @'
936
+ using System;
937
+ using System.Runtime.InteropServices;
938
+ public class CredDeleter {
939
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
940
+ private static extern bool CredDeleteW(string target, int type, int flags);
941
+ public static bool Delete(string target) { return CredDeleteW(target, 1, 0); }
942
+ }
943
+ '@
944
+ if (-not [CredDeleter]::Delete($target)) { exit 1 }
945
+ `;
946
+ function winTarget(service, account) {
947
+ return `${service}/${account}`;
948
+ }
949
+ var windowsKeychain = {
950
+ async getPassword(service, account) {
951
+ try {
952
+ const params = psParam("target", winTarget(service, account));
953
+ const { stdout } = await execFileAsync(
954
+ "powershell.exe",
955
+ ["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_READ_SCRIPT],
956
+ { timeout: 15e3, windowsHide: true }
957
+ );
958
+ return stdout;
959
+ } catch {
960
+ return null;
961
+ }
962
+ },
963
+ async setPassword(service, account, password) {
964
+ const params = psParam("target", winTarget(service, account)) + psParam("password", password);
965
+ await execFileAsync(
966
+ "powershell.exe",
967
+ ["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_WRITE_SCRIPT],
968
+ { timeout: 15e3, windowsHide: true }
969
+ );
970
+ },
971
+ async deletePassword(service, account) {
972
+ try {
973
+ const params = psParam("target", winTarget(service, account));
974
+ await execFileAsync(
975
+ "powershell.exe",
976
+ ["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_DELETE_SCRIPT],
977
+ { timeout: 15e3, windowsHide: true }
978
+ );
979
+ return true;
980
+ } catch {
981
+ return false;
982
+ }
983
+ }
984
+ };
985
+ var macKeychain = {
986
+ async getPassword(service, account) {
987
+ try {
988
+ const { stdout } = await execFileAsync(
989
+ "security",
990
+ ["find-generic-password", "-s", service, "-a", account, "-w"],
991
+ { timeout: 15e3 }
992
+ );
993
+ return stdout.trimEnd();
994
+ } catch {
995
+ return null;
996
+ }
997
+ },
998
+ async setPassword(service, account, password) {
999
+ await execFileAsync(
1000
+ "security",
1001
+ ["add-generic-password", "-U", "-s", service, "-a", account, "-w", password],
1002
+ { timeout: 15e3 }
1003
+ );
1004
+ },
1005
+ async deletePassword(service, account) {
1006
+ try {
1007
+ await execFileAsync(
1008
+ "security",
1009
+ ["delete-generic-password", "-s", service, "-a", account],
1010
+ { timeout: 15e3 }
1011
+ );
1012
+ return true;
1013
+ } catch {
1014
+ return false;
1015
+ }
1016
+ }
1017
+ };
1018
+ var linuxKeychain = {
1019
+ async getPassword(service, account) {
1020
+ try {
1021
+ const { stdout } = await execFileAsync(
1022
+ "secret-tool",
1023
+ ["lookup", "service", service, "account", account],
1024
+ { timeout: 15e3 }
1025
+ );
1026
+ return stdout.trimEnd();
1027
+ } catch {
1028
+ return null;
1029
+ }
1030
+ },
1031
+ // secret-tool reads password from stdin (avoids exposing it in process args)
1032
+ async setPassword(service, account, password) {
1033
+ const child = spawn(
1034
+ "secret-tool",
1035
+ ["store", "--label", `${service}/${account}`, "service", service, "account", account],
1036
+ { stdio: ["pipe", "pipe", "pipe"] }
1037
+ );
1038
+ child.stdin.write(password);
1039
+ child.stdin.end();
1040
+ await new Promise((resolve, reject) => {
1041
+ child.on(
1042
+ "close",
1043
+ (code) => code === 0 ? resolve() : reject(new Error("secret-tool store failed"))
1044
+ );
1045
+ child.on("error", reject);
1046
+ });
1047
+ },
1048
+ async deletePassword(service, account) {
1049
+ try {
1050
+ await execFileAsync(
1051
+ "secret-tool",
1052
+ ["clear", "service", service, "account", account],
1053
+ { timeout: 15e3 }
1054
+ );
1055
+ return true;
1056
+ } catch {
1057
+ return false;
1058
+ }
1059
+ }
1060
+ };
1061
+ function getBackend() {
1062
+ switch (process.platform) {
1063
+ case "win32":
1064
+ return windowsKeychain;
1065
+ case "darwin":
1066
+ return macKeychain;
1067
+ default:
1068
+ return linuxKeychain;
1069
+ }
1070
+ }
1071
+ var backend = getBackend();
1072
+ async function getPassword(service, account) {
1073
+ return backend.getPassword(service, account);
1074
+ }
1075
+
1076
+ // src/secrets/index.ts
770
1077
  var SecretsLoader = class {
771
1078
  // Same service name as CLI uses
772
1079
  SERVICE_NAME = "sinch-functions-cli";
@@ -780,24 +1087,12 @@ var SecretsLoader = class {
780
1087
  return false;
781
1088
  }
782
1089
  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)) {
1090
+ const envPath = path5.join(process.cwd(), ".env");
1091
+ if (!fs4.existsSync(envPath)) {
797
1092
  console.debug("[Secrets] No .env file found, skipping keychain load");
798
1093
  return false;
799
1094
  }
800
- const envContent = fs3.readFileSync(envPath, "utf8");
1095
+ const envContent = fs4.readFileSync(envPath, "utf8");
801
1096
  const envLines = envContent.replace(/\r\n/g, "\n").split("\n");
802
1097
  const secretsToLoad = [];
803
1098
  envLines.forEach((line) => {
@@ -819,7 +1114,7 @@ var SecretsLoader = class {
819
1114
  }
820
1115
  let secretsLoaded = 0;
821
1116
  if (secretsToLoad.includes("PROJECT_ID_API_SECRET")) {
822
- const apiSecret = await keytar.getPassword(this.SERVICE_NAME, `${this.username}-keySecret`);
1117
+ const apiSecret = await getPassword(this.SERVICE_NAME, `${this.username}-keySecret`);
823
1118
  if (apiSecret) {
824
1119
  process.env.PROJECT_ID_API_SECRET = apiSecret;
825
1120
  console.log("\u2705 Loaded PROJECT_ID_API_SECRET from secure storage");
@@ -829,7 +1124,7 @@ var SecretsLoader = class {
829
1124
  if (secretsToLoad.includes("VOICE_APPLICATION_SECRET")) {
830
1125
  const applicationKey = process.env.VOICE_APPLICATION_KEY || this.getApplicationKeyFromConfig();
831
1126
  if (applicationKey) {
832
- const appSecret = await keytar.getPassword(this.SERVICE_NAME, applicationKey);
1127
+ const appSecret = await getPassword(this.SERVICE_NAME, applicationKey);
833
1128
  if (appSecret) {
834
1129
  process.env.VOICE_APPLICATION_SECRET = appSecret;
835
1130
  console.log("\u2705 Loaded VOICE_APPLICATION_SECRET from secure storage");
@@ -843,7 +1138,7 @@ var SecretsLoader = class {
843
1138
  continue;
844
1139
  }
845
1140
  if (functionName) {
846
- const value = await keytar.getPassword(
1141
+ const value = await getPassword(
847
1142
  this.SERVICE_NAME,
848
1143
  `${functionName}-${secretName}`
849
1144
  );
@@ -871,9 +1166,9 @@ var SecretsLoader = class {
871
1166
  */
872
1167
  getApplicationKeyFromConfig() {
873
1168
  try {
874
- const sinchJsonPath = path4.join(process.cwd(), "sinch.json");
875
- if (fs3.existsSync(sinchJsonPath)) {
876
- const sinchConfig = JSON.parse(fs3.readFileSync(sinchJsonPath, "utf8"));
1169
+ const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
1170
+ if (fs4.existsSync(sinchJsonPath)) {
1171
+ const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
877
1172
  return sinchConfig.voiceAppId || sinchConfig.applicationKey || null;
878
1173
  }
879
1174
  } catch (error) {
@@ -886,9 +1181,9 @@ var SecretsLoader = class {
886
1181
  */
887
1182
  getFunctionNameFromConfig() {
888
1183
  try {
889
- const sinchJsonPath = path4.join(process.cwd(), "sinch.json");
890
- if (fs3.existsSync(sinchJsonPath)) {
891
- const sinchConfig = JSON.parse(fs3.readFileSync(sinchJsonPath, "utf8"));
1184
+ const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
1185
+ if (fs4.existsSync(sinchJsonPath)) {
1186
+ const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
892
1187
  return sinchConfig.name || null;
893
1188
  }
894
1189
  } catch (error) {
@@ -902,14 +1197,13 @@ var SecretsLoader = class {
902
1197
  async loadCustomSecrets(secretNames = []) {
903
1198
  const secrets = {};
904
1199
  try {
905
- const keytar = await import("keytar");
906
1200
  const functionName = this.getFunctionNameFromConfig();
907
1201
  if (!functionName) {
908
1202
  console.debug("[Secrets] Could not determine function name for custom secrets");
909
1203
  return secrets;
910
1204
  }
911
1205
  for (const secretName of secretNames) {
912
- const value = await keytar.getPassword(this.SERVICE_NAME, `${functionName}-${secretName}`);
1206
+ const value = await getPassword(this.SERVICE_NAME, `${functionName}-${secretName}`);
913
1207
  if (value) {
914
1208
  secrets[secretName] = value;
915
1209
  process.env[secretName] = value;
@@ -921,15 +1215,13 @@ var SecretsLoader = class {
921
1215
  return secrets;
922
1216
  }
923
1217
  /**
924
- * Check if keytar is available
1218
+ * Check if the native keychain is available.
1219
+ * Always true — no native npm deps required. The underlying OS tool
1220
+ * (secret-tool, security, CredManager) may still be absent, in which
1221
+ * case getPassword() silently returns null per credential.
925
1222
  */
926
1223
  async isAvailable() {
927
- try {
928
- await import("keytar");
929
- return true;
930
- } catch {
931
- return false;
932
- }
1224
+ return true;
933
1225
  }
934
1226
  };
935
1227
  var secretsLoader = new SecretsLoader();
@@ -1320,9 +1612,9 @@ var TunnelClient = class {
1320
1612
  // src/bin/sinch-runtime.ts
1321
1613
  var requireCjs3 = createRequire3(import.meta.url);
1322
1614
  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)) {
1615
+ const distPath = path6.join(process.cwd(), "dist", "function.js");
1616
+ const rootPath = path6.join(process.cwd(), "function.js");
1617
+ if (fs5.existsSync(distPath)) {
1326
1618
  return distPath;
1327
1619
  }
1328
1620
  return rootPath;
@@ -1334,6 +1626,8 @@ function loadRuntimeConfig() {
1334
1626
  functionId: process.env.FUNCTION_ID || process.env.SINCH_FUNCTION_ID || "local-dev"
1335
1627
  };
1336
1628
  }
1629
+ var storage = createStorageClient();
1630
+ var databasePath = path6.join(process.cwd(), "data", "app.db");
1337
1631
  function buildLocalContext(req, runtimeConfig) {
1338
1632
  const baseContext = buildBaseContext(req);
1339
1633
  const cache = createCacheClient();
@@ -1341,6 +1635,8 @@ function buildLocalContext(req, runtimeConfig) {
1341
1635
  return {
1342
1636
  ...baseContext,
1343
1637
  cache,
1638
+ storage,
1639
+ database: databasePath,
1344
1640
  ...sinchClients,
1345
1641
  env: process.env,
1346
1642
  config: {
@@ -1367,12 +1663,12 @@ function displayStartupInfo(config, verbose, _port) {
1367
1663
  function displayEnvironmentVariables() {
1368
1664
  console.log("\nEnvironment Variables:");
1369
1665
  try {
1370
- const envPath = path5.join(process.cwd(), ".env");
1371
- if (!fs4.existsSync(envPath)) {
1666
+ const envPath = path6.join(process.cwd(), ".env");
1667
+ if (!fs5.existsSync(envPath)) {
1372
1668
  console.log(" (no .env file found)");
1373
1669
  return;
1374
1670
  }
1375
- const envContent = fs4.readFileSync(envPath, "utf8");
1671
+ const envContent = fs5.readFileSync(envPath, "utf8");
1376
1672
  const envLines = envContent.split("\n");
1377
1673
  const variables = [];
1378
1674
  const secrets = [];
@@ -1432,7 +1728,7 @@ function displayApplicationCredentials() {
1432
1728
  async function displayDetectedFunctions() {
1433
1729
  try {
1434
1730
  const functionPath = findFunctionPath3();
1435
- if (!fs4.existsSync(functionPath)) return;
1731
+ if (!fs5.existsSync(functionPath)) return;
1436
1732
  const functionUrl = pathToFileURL2(functionPath).href;
1437
1733
  const module = await import(functionUrl);
1438
1734
  const userFunction = module.default || module;
@@ -1459,6 +1755,8 @@ async function main() {
1459
1755
  } catch {
1460
1756
  }
1461
1757
  await secretsLoader.loadFromKeychain();
1758
+ fs5.mkdirSync(path6.join(process.cwd(), "storage"), { recursive: true });
1759
+ fs5.mkdirSync(path6.join(process.cwd(), "data"), { recursive: true });
1462
1760
  const config = loadRuntimeConfig();
1463
1761
  const staticDir = process.env.STATIC_DIR;
1464
1762
  const landingPageEnabled = process.env.LANDING_PAGE_ENABLED !== "false";