@sinch/functions-runtime 0.3.1 → 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.
- package/dist/bin/sinch-runtime.js +255 -16
- package/dist/bin/sinch-runtime.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin/sinch-runtime.ts
|
|
4
|
-
import
|
|
4
|
+
import path5 from "path";
|
|
5
5
|
import { createRequire as createRequire3 } from "module";
|
|
6
6
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
7
|
-
import
|
|
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(
|
|
246
|
+
function extractFunctionName(path6, body) {
|
|
247
247
|
if (body?.event && isVoiceCallback(body.event)) {
|
|
248
248
|
return body.event;
|
|
249
249
|
}
|
|
250
|
-
const segments =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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";
|
|
@@ -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 =
|
|
1063
|
-
const rootPath =
|
|
1064
|
-
if (
|
|
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 =
|
|
1109
|
-
if (!
|
|
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 =
|
|
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 (!
|
|
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
|
|
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;
|