@sinch/functions-runtime 0.3.0 → 0.3.2

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 path4 from "path";
4
+ import path5 from "path";
5
5
  import { createRequire as createRequire3 } from "module";
6
6
  import { pathToFileURL as pathToFileURL2 } from "url";
7
- import fs3 from "fs";
7
+ import fs4 from "fs";
8
8
 
9
9
  // ../runtime-shared/dist/ai/connect-agent.js
10
10
  var AgentProvider;
@@ -243,11 +243,11 @@ function isVoiceCallback(functionName) {
243
243
  function isNotificationEvent(functionName) {
244
244
  return NOTIFICATION_EVENTS.includes(functionName);
245
245
  }
246
- function extractFunctionName(path5, body) {
246
+ function extractFunctionName(path6, body) {
247
247
  if (body?.event && isVoiceCallback(body.event)) {
248
248
  return body.event;
249
249
  }
250
- const segments = path5.split("/").filter((s) => s && s !== "*");
250
+ const segments = path6.split("/").filter((s) => s && s !== "*");
251
251
  if (segments.length === 1 && isVoiceCallback(segments[0])) {
252
252
  return segments[0];
253
253
  }
@@ -257,10 +257,10 @@ function generateRequestId() {
257
257
  return `req_${Date.now()}_${Math.random().toString(36).substring(7)}`;
258
258
  }
259
259
  function findFunctionPath() {
260
- const fs4 = requireCjs2("fs");
260
+ const fs5 = requireCjs2("fs");
261
261
  const distPath = nodePath.join(process.cwd(), "dist", "function.js");
262
262
  const rootPath = nodePath.join(process.cwd(), "function.js");
263
- if (fs4.existsSync(distPath)) {
263
+ if (fs5.existsSync(distPath)) {
264
264
  return distPath;
265
265
  }
266
266
  return rootPath;
@@ -393,7 +393,10 @@ async function handleVoiceCallback(functionName, userFunction, context, callback
393
393
  return formatSvamlResponse(result, functionName);
394
394
  }
395
395
  async function handleCustomEndpoint(functionName, userFunction, context, request, logger) {
396
- const handler = userFunction[functionName];
396
+ let handler = userFunction[functionName];
397
+ if ((!handler || typeof handler !== "function") && functionName === "default") {
398
+ handler = userFunction["home"];
399
+ }
397
400
  if (!handler || typeof handler !== "function") {
398
401
  throw new Error(`Function '${functionName}' not found in function.js`);
399
402
  }
@@ -530,6 +533,67 @@ function setupRequestHandler(app, options = {}) {
530
533
 
531
534
  // ../runtime-shared/dist/sinch/index.js
532
535
  import { SinchClient, validateAuthenticationHeader } from "@sinch/sdk-core";
536
+ function createSinchClients() {
537
+ const clients = {};
538
+ const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
539
+ if (!hasCredentials) {
540
+ return clients;
541
+ }
542
+ try {
543
+ const sinchClient = new SinchClient({
544
+ projectId: process.env.PROJECT_ID,
545
+ keyId: process.env.PROJECT_ID_API_KEY,
546
+ keySecret: process.env.PROJECT_ID_API_SECRET
547
+ });
548
+ if (process.env.CONVERSATION_APP_ID) {
549
+ clients.conversation = sinchClient.conversation;
550
+ console.log("[SINCH] Conversation API initialized");
551
+ }
552
+ if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
553
+ const voiceClient = new SinchClient({
554
+ projectId: process.env.PROJECT_ID,
555
+ keyId: process.env.PROJECT_ID_API_KEY,
556
+ keySecret: process.env.PROJECT_ID_API_SECRET,
557
+ applicationKey: process.env.VOICE_APPLICATION_KEY,
558
+ applicationSecret: process.env.VOICE_APPLICATION_SECRET
559
+ });
560
+ clients.voice = voiceClient.voice;
561
+ console.log("[SINCH] Voice API initialized with application credentials");
562
+ }
563
+ if (process.env.SMS_SERVICE_PLAN_ID) {
564
+ clients.sms = sinchClient.sms;
565
+ console.log("[SINCH] SMS API initialized");
566
+ }
567
+ if (process.env.ENABLE_NUMBERS_API === "true") {
568
+ clients.numbers = sinchClient.numbers;
569
+ console.log("[SINCH] Numbers API initialized");
570
+ }
571
+ } catch (error) {
572
+ console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
573
+ return {};
574
+ }
575
+ if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
576
+ clients.validateWebhookSignature = (requestData) => {
577
+ console.log("[SINCH] Validating Voice webhook signature");
578
+ try {
579
+ const result = validateAuthenticationHeader(process.env.VOICE_APPLICATION_KEY, process.env.VOICE_APPLICATION_SECRET, requestData.headers, requestData.body, requestData.path, requestData.method);
580
+ console.log("[SINCH] Validation result:", result ? "VALID" : "INVALID");
581
+ return result;
582
+ } catch (error) {
583
+ console.error("[SINCH] Validation error:", error.message);
584
+ return false;
585
+ }
586
+ };
587
+ }
588
+ return clients;
589
+ }
590
+ var cachedClients = null;
591
+ function getSinchClients() {
592
+ if (!cachedClients) {
593
+ cachedClients = createSinchClients();
594
+ }
595
+ return cachedClients;
596
+ }
533
597
 
534
598
  // ../runtime-shared/dist/ai/elevenlabs/state.js
535
599
  var ElevenLabsStateManager = class {
@@ -676,6 +740,177 @@ function createCacheClient(_projectId, _functionName) {
676
740
  return new LocalCache();
677
741
  }
678
742
 
743
+ // src/secrets/index.ts
744
+ import fs3 from "fs";
745
+ import path4 from "path";
746
+ import os from "os";
747
+ var SecretsLoader = class {
748
+ // Same service name as CLI uses
749
+ SERVICE_NAME = "sinch-functions-cli";
750
+ username = os.userInfo().username;
751
+ /**
752
+ * Load secrets from OS keychain for variables declared in .env
753
+ * Only loads secrets that have empty values in .env (security best practice)
754
+ */
755
+ async loadFromKeychain() {
756
+ if (process.env.NODE_ENV === "production") {
757
+ return false;
758
+ }
759
+ try {
760
+ let keytar;
761
+ try {
762
+ keytar = await import("keytar");
763
+ } catch (error) {
764
+ if (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_MODULE_NOT_FOUND") {
765
+ console.debug("[Secrets] Keytar not available - secrets not loaded");
766
+ return false;
767
+ } else {
768
+ console.error("[Secrets] Error loading keytar:", error.message);
769
+ }
770
+ return false;
771
+ }
772
+ const envPath = path4.join(process.cwd(), ".env");
773
+ if (!fs3.existsSync(envPath)) {
774
+ console.debug("[Secrets] No .env file found, skipping keychain load");
775
+ return false;
776
+ }
777
+ const envContent = fs3.readFileSync(envPath, "utf8");
778
+ const envLines = envContent.replace(/\r\n/g, "\n").split("\n");
779
+ const secretsToLoad = [];
780
+ envLines.forEach((line) => {
781
+ const trimmedLine = line.replace(/\r$/, "").trim();
782
+ if (trimmedLine && !trimmedLine.startsWith("#")) {
783
+ const equalIndex = trimmedLine.indexOf("=");
784
+ if (equalIndex !== -1) {
785
+ const envKey = trimmedLine.substring(0, equalIndex).trim();
786
+ const envValue = trimmedLine.substring(equalIndex + 1).trim();
787
+ if (envKey && envValue === "" && !process.env[envKey]) {
788
+ secretsToLoad.push(envKey);
789
+ }
790
+ }
791
+ }
792
+ });
793
+ if (secretsToLoad.length === 0) {
794
+ console.debug("[Secrets] No empty variables found in .env");
795
+ return false;
796
+ }
797
+ let secretsLoaded = 0;
798
+ if (secretsToLoad.includes("PROJECT_ID_API_SECRET")) {
799
+ const apiSecret = await keytar.getPassword(this.SERVICE_NAME, `${this.username}-keySecret`);
800
+ if (apiSecret) {
801
+ process.env.PROJECT_ID_API_SECRET = apiSecret;
802
+ console.log("\u2705 Loaded PROJECT_ID_API_SECRET from secure storage");
803
+ secretsLoaded++;
804
+ }
805
+ }
806
+ if (secretsToLoad.includes("VOICE_APPLICATION_SECRET")) {
807
+ const applicationKey = process.env.VOICE_APPLICATION_KEY || this.getApplicationKeyFromConfig();
808
+ if (applicationKey) {
809
+ const appSecret = await keytar.getPassword(this.SERVICE_NAME, applicationKey);
810
+ if (appSecret) {
811
+ process.env.VOICE_APPLICATION_SECRET = appSecret;
812
+ console.log("\u2705 Loaded VOICE_APPLICATION_SECRET from secure storage");
813
+ secretsLoaded++;
814
+ }
815
+ }
816
+ }
817
+ const functionName = this.getFunctionNameFromConfig();
818
+ for (const secretName of secretsToLoad) {
819
+ if (secretName === "PROJECT_ID_API_SECRET" || secretName === "VOICE_APPLICATION_SECRET") {
820
+ continue;
821
+ }
822
+ if (functionName) {
823
+ const value = await keytar.getPassword(
824
+ this.SERVICE_NAME,
825
+ `${functionName}-${secretName}`
826
+ );
827
+ if (value) {
828
+ process.env[secretName] = value;
829
+ console.log(`\u2705 Loaded ${secretName} from secure storage`);
830
+ secretsLoaded++;
831
+ }
832
+ }
833
+ }
834
+ if (secretsLoaded === 0) {
835
+ console.log("\u2139\uFE0F No secrets found in secure storage for declared variables");
836
+ console.log("\u{1F4A1} To configure Sinch auth: sinch auth login");
837
+ console.log("\u{1F4A1} To add custom secrets: sinch functions secrets add <KEY> <VALUE>");
838
+ }
839
+ return secretsLoaded > 0;
840
+ } catch (error) {
841
+ console.error("[Secrets] Unexpected error:", error.message);
842
+ console.log("\u{1F4A1} To manage secrets manually, use: sinch functions secrets");
843
+ return false;
844
+ }
845
+ }
846
+ /**
847
+ * Helper to get application key from sinch.json
848
+ */
849
+ getApplicationKeyFromConfig() {
850
+ try {
851
+ const sinchJsonPath = path4.join(process.cwd(), "sinch.json");
852
+ if (fs3.existsSync(sinchJsonPath)) {
853
+ const sinchConfig = JSON.parse(fs3.readFileSync(sinchJsonPath, "utf8"));
854
+ return sinchConfig.voiceAppId || sinchConfig.applicationKey || null;
855
+ }
856
+ } catch (error) {
857
+ console.debug("[Secrets] Could not read sinch.json:", error.message);
858
+ }
859
+ return null;
860
+ }
861
+ /**
862
+ * Helper to get function name from sinch.json
863
+ */
864
+ getFunctionNameFromConfig() {
865
+ try {
866
+ const sinchJsonPath = path4.join(process.cwd(), "sinch.json");
867
+ if (fs3.existsSync(sinchJsonPath)) {
868
+ const sinchConfig = JSON.parse(fs3.readFileSync(sinchJsonPath, "utf8"));
869
+ return sinchConfig.name || null;
870
+ }
871
+ } catch (error) {
872
+ console.debug("[Secrets] Could not read sinch.json:", error.message);
873
+ }
874
+ return null;
875
+ }
876
+ /**
877
+ * Load custom secrets added via 'sinch functions secrets' command
878
+ */
879
+ async loadCustomSecrets(secretNames = []) {
880
+ const secrets = {};
881
+ try {
882
+ const keytar = await import("keytar");
883
+ const functionName = this.getFunctionNameFromConfig();
884
+ if (!functionName) {
885
+ console.debug("[Secrets] Could not determine function name for custom secrets");
886
+ return secrets;
887
+ }
888
+ for (const secretName of secretNames) {
889
+ const value = await keytar.getPassword(this.SERVICE_NAME, `${functionName}-${secretName}`);
890
+ if (value) {
891
+ secrets[secretName] = value;
892
+ process.env[secretName] = value;
893
+ }
894
+ }
895
+ } catch (error) {
896
+ console.debug("[Secrets] Could not load custom secrets:", error.message);
897
+ }
898
+ return secrets;
899
+ }
900
+ /**
901
+ * Check if keytar is available
902
+ */
903
+ async isAvailable() {
904
+ try {
905
+ await import("keytar");
906
+ return true;
907
+ } catch {
908
+ return false;
909
+ }
910
+ }
911
+ };
912
+ var secretsLoader = new SecretsLoader();
913
+
679
914
  // src/tunnel/index.ts
680
915
  import WebSocket from "ws";
681
916
  import axios from "axios";
@@ -686,8 +921,8 @@ async function configureConversationWebhooks(tunnelUrl, config) {
686
921
  try {
687
922
  const conversationAppId = process.env.CONVERSATION_APP_ID;
688
923
  const projectId = process.env.PROJECT_ID;
689
- const keyId = process.env.KEY_ID;
690
- const keySecret = process.env.KEY_SECRET;
924
+ const keyId = process.env.PROJECT_ID_API_KEY;
925
+ const keySecret = process.env.PROJECT_ID_API_SECRET;
691
926
  if (!conversationAppId || !projectId || !keyId || !keySecret) {
692
927
  console.log("\u{1F4A1} Conversation API not fully configured - skipping webhook setup");
693
928
  return;
@@ -731,8 +966,8 @@ async function cleanupConversationWebhook(config) {
731
966
  try {
732
967
  const conversationAppId = process.env.CONVERSATION_APP_ID;
733
968
  const projectId = process.env.PROJECT_ID;
734
- const keyId = process.env.KEY_ID;
735
- const keySecret = process.env.KEY_SECRET;
969
+ const keyId = process.env.PROJECT_ID_API_KEY;
970
+ const keySecret = process.env.PROJECT_ID_API_SECRET;
736
971
  if (!conversationAppId || !projectId || !keyId || !keySecret) return;
737
972
  const sinchClient = new SinchClient2({
738
973
  projectId,
@@ -1059,9 +1294,9 @@ var TunnelClient = class {
1059
1294
  // src/bin/sinch-runtime.ts
1060
1295
  var requireCjs3 = createRequire3(import.meta.url);
1061
1296
  function findFunctionPath3() {
1062
- const distPath = path4.join(process.cwd(), "dist", "function.js");
1063
- const rootPath = path4.join(process.cwd(), "function.js");
1064
- if (fs3.existsSync(distPath)) {
1297
+ const distPath = path5.join(process.cwd(), "dist", "function.js");
1298
+ const rootPath = path5.join(process.cwd(), "function.js");
1299
+ if (fs4.existsSync(distPath)) {
1065
1300
  return distPath;
1066
1301
  }
1067
1302
  return rootPath;
@@ -1076,9 +1311,11 @@ function loadRuntimeConfig() {
1076
1311
  function buildLocalContext(req, runtimeConfig) {
1077
1312
  const baseContext = buildBaseContext(req);
1078
1313
  const cache = createCacheClient();
1314
+ const sinchClients = getSinchClients();
1079
1315
  return {
1080
1316
  ...baseContext,
1081
1317
  cache,
1318
+ ...sinchClients,
1082
1319
  env: process.env,
1083
1320
  config: {
1084
1321
  projectId: runtimeConfig.projectId,
@@ -1086,7 +1323,6 @@ function buildLocalContext(req, runtimeConfig) {
1086
1323
  environment: "development",
1087
1324
  variables: process.env
1088
1325
  }
1089
- // Sinch clients would be added here if SDK is available
1090
1326
  };
1091
1327
  }
1092
1328
  function displayStartupInfo(config, verbose, _port) {
@@ -1105,12 +1341,12 @@ function displayStartupInfo(config, verbose, _port) {
1105
1341
  function displayEnvironmentVariables() {
1106
1342
  console.log("\nEnvironment Variables:");
1107
1343
  try {
1108
- const envPath = path4.join(process.cwd(), ".env");
1109
- if (!fs3.existsSync(envPath)) {
1344
+ const envPath = path5.join(process.cwd(), ".env");
1345
+ if (!fs4.existsSync(envPath)) {
1110
1346
  console.log(" (no .env file found)");
1111
1347
  return;
1112
1348
  }
1113
- const envContent = fs3.readFileSync(envPath, "utf8");
1349
+ const envContent = fs4.readFileSync(envPath, "utf8");
1114
1350
  const envLines = envContent.split("\n");
1115
1351
  const variables = [];
1116
1352
  const secrets = [];
@@ -1170,7 +1406,7 @@ function displayApplicationCredentials() {
1170
1406
  async function displayDetectedFunctions() {
1171
1407
  try {
1172
1408
  const functionPath = findFunctionPath3();
1173
- if (!fs3.existsSync(functionPath)) return;
1409
+ if (!fs4.existsSync(functionPath)) return;
1174
1410
  const functionUrl = pathToFileURL2(functionPath).href;
1175
1411
  const module = await import(functionUrl);
1176
1412
  const userFunction = module.default || module;
@@ -1196,10 +1432,13 @@ async function main() {
1196
1432
  dotenv.config();
1197
1433
  } catch {
1198
1434
  }
1435
+ await secretsLoader.loadFromKeychain();
1199
1436
  const config = loadRuntimeConfig();
1200
1437
  const staticDir = process.env.STATIC_DIR;
1201
- const app = createApp({ staticDir });
1438
+ const landingPageEnabled = process.env.LANDING_PAGE_ENABLED !== "false";
1439
+ const app = createApp({ staticDir, landingPageEnabled });
1202
1440
  setupRequestHandler(app, {
1441
+ landingPageEnabled,
1203
1442
  loadUserFunction: async () => {
1204
1443
  const functionPath = findFunctionPath3();
1205
1444
  const functionUrl = pathToFileURL2(functionPath).href;