@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/bin/sinch-runtime.js +259 -93
- package/dist/bin/sinch-runtime.js.map +1 -1
- package/dist/index.d.ts +36 -0
- package/dist/index.js +196 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -185,6 +185,122 @@ function validateBasicAuth(authHeader, expectedKey, expectedSecret) {
|
|
|
185
185
|
return keyMatch && secretMatch;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// ../runtime-shared/dist/security/index.js
|
|
189
|
+
function shouldValidateWebhook(mode, isDevelopment) {
|
|
190
|
+
const normalizedMode = (mode || "deploy").toLowerCase();
|
|
191
|
+
switch (normalizedMode) {
|
|
192
|
+
case "never":
|
|
193
|
+
return false;
|
|
194
|
+
case "always":
|
|
195
|
+
return true;
|
|
196
|
+
case "deploy":
|
|
197
|
+
default:
|
|
198
|
+
return !isDevelopment;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
var VALID_MODES = ["never", "deploy", "always"];
|
|
202
|
+
function getProtectionMode(config) {
|
|
203
|
+
const envValue = process.env.WEBHOOK_PROTECTION ?? process.env.PROTECT_VOICE_CALLBACKS;
|
|
204
|
+
if (envValue) {
|
|
205
|
+
const normalized = envValue.toLowerCase();
|
|
206
|
+
if (VALID_MODES.includes(normalized)) {
|
|
207
|
+
return normalized;
|
|
208
|
+
}
|
|
209
|
+
console.warn(`[SECURITY] Unknown WEBHOOK_PROTECTION value "${envValue}", defaulting to "deploy"`);
|
|
210
|
+
return "deploy";
|
|
211
|
+
}
|
|
212
|
+
const configValue = config?.WebhookProtection ?? config?.webhookProtection ?? config?.ProtectVoiceCallbacks ?? config?.protectVoiceCallbacks;
|
|
213
|
+
if (typeof configValue === "string") {
|
|
214
|
+
const normalized = configValue.toLowerCase();
|
|
215
|
+
if (VALID_MODES.includes(normalized)) {
|
|
216
|
+
return normalized;
|
|
217
|
+
}
|
|
218
|
+
console.warn(`[SECURITY] Unknown webhook protection value "${configValue}", defaulting to "deploy"`);
|
|
219
|
+
return "deploy";
|
|
220
|
+
}
|
|
221
|
+
return "deploy";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ../runtime-shared/dist/sinch/index.js
|
|
225
|
+
import { SinchClient, validateAuthenticationHeader, ConversationCallbackWebhooks } from "@sinch/sdk-core";
|
|
226
|
+
function createSinchClients() {
|
|
227
|
+
const clients = {};
|
|
228
|
+
const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
|
|
229
|
+
if (process.env.CONVERSATION_WEBHOOK_SECRET) {
|
|
230
|
+
const callbackProcessor = new ConversationCallbackWebhooks(process.env.CONVERSATION_WEBHOOK_SECRET);
|
|
231
|
+
clients.validateConversationWebhook = (headers, body) => {
|
|
232
|
+
try {
|
|
233
|
+
const result = callbackProcessor.validateAuthenticationHeader(headers, body);
|
|
234
|
+
console.log("[SINCH] Conversation webhook validation:", result ? "VALID" : "INVALID");
|
|
235
|
+
return result;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error("[SINCH] Conversation validation error:", error instanceof Error ? error.message : error);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (!hasCredentials) {
|
|
243
|
+
return clients;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const sinchClient = new SinchClient({
|
|
247
|
+
projectId: process.env.PROJECT_ID,
|
|
248
|
+
keyId: process.env.PROJECT_ID_API_KEY,
|
|
249
|
+
keySecret: process.env.PROJECT_ID_API_SECRET
|
|
250
|
+
});
|
|
251
|
+
if (process.env.CONVERSATION_APP_ID) {
|
|
252
|
+
clients.conversation = sinchClient.conversation;
|
|
253
|
+
console.log("[SINCH] Conversation API initialized");
|
|
254
|
+
}
|
|
255
|
+
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
256
|
+
const voiceClient = new SinchClient({
|
|
257
|
+
projectId: process.env.PROJECT_ID,
|
|
258
|
+
keyId: process.env.PROJECT_ID_API_KEY,
|
|
259
|
+
keySecret: process.env.PROJECT_ID_API_SECRET,
|
|
260
|
+
applicationKey: process.env.VOICE_APPLICATION_KEY,
|
|
261
|
+
applicationSecret: process.env.VOICE_APPLICATION_SECRET
|
|
262
|
+
});
|
|
263
|
+
clients.voice = voiceClient.voice;
|
|
264
|
+
console.log("[SINCH] Voice API initialized with application credentials");
|
|
265
|
+
}
|
|
266
|
+
if (process.env.SMS_SERVICE_PLAN_ID) {
|
|
267
|
+
clients.sms = sinchClient.sms;
|
|
268
|
+
console.log("[SINCH] SMS API initialized");
|
|
269
|
+
}
|
|
270
|
+
if (process.env.ENABLE_NUMBERS_API === "true") {
|
|
271
|
+
clients.numbers = sinchClient.numbers;
|
|
272
|
+
console.log("[SINCH] Numbers API initialized");
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
279
|
+
clients.validateWebhookSignature = (requestData) => {
|
|
280
|
+
console.log("[SINCH] Validating Voice webhook signature");
|
|
281
|
+
try {
|
|
282
|
+
const result = validateAuthenticationHeader(process.env.VOICE_APPLICATION_KEY, process.env.VOICE_APPLICATION_SECRET, requestData.headers, requestData.body, requestData.path, requestData.method);
|
|
283
|
+
console.log("[SINCH] Validation result:", result ? "VALID" : "INVALID");
|
|
284
|
+
return result;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error("[SINCH] Validation error:", error.message);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return clients;
|
|
292
|
+
}
|
|
293
|
+
var cachedClients = null;
|
|
294
|
+
function getSinchClients() {
|
|
295
|
+
if (!cachedClients) {
|
|
296
|
+
cachedClients = createSinchClients();
|
|
297
|
+
}
|
|
298
|
+
return cachedClients;
|
|
299
|
+
}
|
|
300
|
+
function resetSinchClients() {
|
|
301
|
+
cachedClients = null;
|
|
302
|
+
}
|
|
303
|
+
|
|
188
304
|
// ../runtime-shared/dist/host/app.js
|
|
189
305
|
import { createRequire as createRequire2 } from "module";
|
|
190
306
|
import { pathToFileURL } from "url";
|
|
@@ -385,7 +501,7 @@ var noOpStorage = {
|
|
|
385
501
|
};
|
|
386
502
|
function buildBaseContext(req, config = {}) {
|
|
387
503
|
return {
|
|
388
|
-
requestId: req
|
|
504
|
+
requestId: req?.headers?.["x-request-id"] || generateRequestId(),
|
|
389
505
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
390
506
|
env: process.env,
|
|
391
507
|
config: {
|
|
@@ -406,7 +522,13 @@ function buildBaseContext(req, config = {}) {
|
|
|
406
522
|
async function handleVoiceCallback(functionName, userFunction, context, callbackData, logger) {
|
|
407
523
|
const handler = userFunction[functionName];
|
|
408
524
|
if (!handler || typeof handler !== "function") {
|
|
409
|
-
|
|
525
|
+
if (functionName === "ice") {
|
|
526
|
+
throw new Error(`Voice callback 'ice' not found \u2014 export an ice() function in function.ts`);
|
|
527
|
+
}
|
|
528
|
+
if (logger) {
|
|
529
|
+
logger(`${functionName.toUpperCase()} callback not implemented \u2014 returning 200`);
|
|
530
|
+
}
|
|
531
|
+
return { statusCode: 200, body: {}, headers: {} };
|
|
410
532
|
}
|
|
411
533
|
let result;
|
|
412
534
|
switch (functionName) {
|
|
@@ -449,7 +571,9 @@ async function handleCustomEndpoint(functionName, userFunction, context, request
|
|
|
449
571
|
handler = userFunction["home"];
|
|
450
572
|
}
|
|
451
573
|
if (!handler || typeof handler !== "function") {
|
|
452
|
-
|
|
574
|
+
const available = Object.keys(userFunction).filter((k) => typeof userFunction[k] === "function");
|
|
575
|
+
const pathHint = functionName.endsWith("Webhook") ? `/webhook/${functionName.replace("Webhook", "")}` : `/${functionName}`;
|
|
576
|
+
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.`);
|
|
453
577
|
}
|
|
454
578
|
const result = await handler(context, request);
|
|
455
579
|
if (logger) {
|
|
@@ -481,6 +605,42 @@ function createApp(options = {}) {
|
|
|
481
605
|
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
|
482
606
|
return app;
|
|
483
607
|
}
|
|
608
|
+
function createSinchRuntime() {
|
|
609
|
+
const result = {
|
|
610
|
+
startupHandlers: [],
|
|
611
|
+
wsHandlers: /* @__PURE__ */ new Map()
|
|
612
|
+
};
|
|
613
|
+
const runtime = {
|
|
614
|
+
onStartup(handler) {
|
|
615
|
+
result.startupHandlers.push(handler);
|
|
616
|
+
},
|
|
617
|
+
onWebSocket(path7, handler) {
|
|
618
|
+
const normalizedPath = path7.startsWith("/") ? path7 : `/${path7}`;
|
|
619
|
+
result.wsHandlers.set(normalizedPath, handler);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
return { runtime, result };
|
|
623
|
+
}
|
|
624
|
+
function installWebSocketHandlers(server, wsHandlers, logger = console.log) {
|
|
625
|
+
if (wsHandlers.size === 0)
|
|
626
|
+
return;
|
|
627
|
+
const { WebSocketServer } = requireCjs2("ws");
|
|
628
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
629
|
+
server.on("upgrade", (req, socket, head) => {
|
|
630
|
+
const pathname = new URL(req.url || "/", `http://${req.headers.host}`).pathname;
|
|
631
|
+
const handler = wsHandlers.get(pathname);
|
|
632
|
+
if (handler) {
|
|
633
|
+
logger(`[WS] Upgrade request for ${pathname}`);
|
|
634
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
635
|
+
handler(ws, req);
|
|
636
|
+
});
|
|
637
|
+
} else {
|
|
638
|
+
logger(`[WS] No handler for ${pathname} \u2014 rejecting upgrade`);
|
|
639
|
+
socket.destroy();
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
logger(`[WS] WebSocket endpoints registered: ${[...wsHandlers.keys()].join(", ")}`);
|
|
643
|
+
}
|
|
484
644
|
function setupRequestHandler(app, options = {}) {
|
|
485
645
|
const { loadUserFunction = async () => {
|
|
486
646
|
const functionPath = findFunctionPath();
|
|
@@ -526,6 +686,33 @@ function setupRequestHandler(app, options = {}) {
|
|
|
526
686
|
}
|
|
527
687
|
}
|
|
528
688
|
}
|
|
689
|
+
const protectionMode = getProtectionMode();
|
|
690
|
+
const isDev = process.env.NODE_ENV !== "production" && process.env.ASPNETCORE_ENVIRONMENT !== "Production";
|
|
691
|
+
if (shouldValidateWebhook(protectionMode, isDev)) {
|
|
692
|
+
const sinchClients = getSinchClients();
|
|
693
|
+
if (isVoiceCallback(functionName) && sinchClients.validateWebhookSignature) {
|
|
694
|
+
const rawBody = req.rawBody ?? JSON.stringify(req.body);
|
|
695
|
+
const isValid = sinchClients.validateWebhookSignature({
|
|
696
|
+
method: req.method,
|
|
697
|
+
path: req.path,
|
|
698
|
+
headers: req.headers,
|
|
699
|
+
body: rawBody
|
|
700
|
+
});
|
|
701
|
+
if (!isValid) {
|
|
702
|
+
logger("[SECURITY] Voice webhook signature validation failed");
|
|
703
|
+
res.status(401).json({ error: "Invalid webhook signature" });
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (functionName === "conversationWebhook" && sinchClients.validateConversationWebhook) {
|
|
708
|
+
const isValid = sinchClients.validateConversationWebhook(req.headers, req.body);
|
|
709
|
+
if (!isValid) {
|
|
710
|
+
logger("[SECURITY] Conversation webhook signature validation failed");
|
|
711
|
+
res.status(401).json({ error: "Invalid webhook signature" });
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
529
716
|
onRequestStart({ functionName, req });
|
|
530
717
|
const context = buildContext(req);
|
|
531
718
|
const userFunction = await Promise.resolve(loadUserFunction());
|
|
@@ -601,70 +788,6 @@ function setupRequestHandler(app, options = {}) {
|
|
|
601
788
|
});
|
|
602
789
|
}
|
|
603
790
|
|
|
604
|
-
// ../runtime-shared/dist/sinch/index.js
|
|
605
|
-
import { SinchClient, validateAuthenticationHeader } from "@sinch/sdk-core";
|
|
606
|
-
function createSinchClients() {
|
|
607
|
-
const clients = {};
|
|
608
|
-
const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
|
|
609
|
-
if (!hasCredentials) {
|
|
610
|
-
return clients;
|
|
611
|
-
}
|
|
612
|
-
try {
|
|
613
|
-
const sinchClient = new SinchClient({
|
|
614
|
-
projectId: process.env.PROJECT_ID,
|
|
615
|
-
keyId: process.env.PROJECT_ID_API_KEY,
|
|
616
|
-
keySecret: process.env.PROJECT_ID_API_SECRET
|
|
617
|
-
});
|
|
618
|
-
if (process.env.CONVERSATION_APP_ID) {
|
|
619
|
-
clients.conversation = sinchClient.conversation;
|
|
620
|
-
console.log("[SINCH] Conversation API initialized");
|
|
621
|
-
}
|
|
622
|
-
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
623
|
-
const voiceClient = new SinchClient({
|
|
624
|
-
projectId: process.env.PROJECT_ID,
|
|
625
|
-
keyId: process.env.PROJECT_ID_API_KEY,
|
|
626
|
-
keySecret: process.env.PROJECT_ID_API_SECRET,
|
|
627
|
-
applicationKey: process.env.VOICE_APPLICATION_KEY,
|
|
628
|
-
applicationSecret: process.env.VOICE_APPLICATION_SECRET
|
|
629
|
-
});
|
|
630
|
-
clients.voice = voiceClient.voice;
|
|
631
|
-
console.log("[SINCH] Voice API initialized with application credentials");
|
|
632
|
-
}
|
|
633
|
-
if (process.env.SMS_SERVICE_PLAN_ID) {
|
|
634
|
-
clients.sms = sinchClient.sms;
|
|
635
|
-
console.log("[SINCH] SMS API initialized");
|
|
636
|
-
}
|
|
637
|
-
if (process.env.ENABLE_NUMBERS_API === "true") {
|
|
638
|
-
clients.numbers = sinchClient.numbers;
|
|
639
|
-
console.log("[SINCH] Numbers API initialized");
|
|
640
|
-
}
|
|
641
|
-
} catch (error) {
|
|
642
|
-
console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
|
|
643
|
-
return {};
|
|
644
|
-
}
|
|
645
|
-
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
646
|
-
clients.validateWebhookSignature = (requestData) => {
|
|
647
|
-
console.log("[SINCH] Validating Voice webhook signature");
|
|
648
|
-
try {
|
|
649
|
-
const result = validateAuthenticationHeader(process.env.VOICE_APPLICATION_KEY, process.env.VOICE_APPLICATION_SECRET, requestData.headers, requestData.body, requestData.path, requestData.method);
|
|
650
|
-
console.log("[SINCH] Validation result:", result ? "VALID" : "INVALID");
|
|
651
|
-
return result;
|
|
652
|
-
} catch (error) {
|
|
653
|
-
console.error("[SINCH] Validation error:", error.message);
|
|
654
|
-
return false;
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
return clients;
|
|
659
|
-
}
|
|
660
|
-
var cachedClients = null;
|
|
661
|
-
function getSinchClients() {
|
|
662
|
-
if (!cachedClients) {
|
|
663
|
-
cachedClients = createSinchClients();
|
|
664
|
-
}
|
|
665
|
-
return cachedClients;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
791
|
// ../runtime-shared/dist/ai/elevenlabs/state.js
|
|
669
792
|
var ElevenLabsStateManager = class {
|
|
670
793
|
state = {
|
|
@@ -1268,6 +1391,7 @@ import axios from "axios";
|
|
|
1268
1391
|
|
|
1269
1392
|
// src/tunnel/webhook-config.ts
|
|
1270
1393
|
import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
|
|
1394
|
+
import { randomBytes } from "crypto";
|
|
1271
1395
|
var SINCH_FN_URL_PATTERN = /\.fn(-\w+)?\.sinch\.com/;
|
|
1272
1396
|
function isOurWebhook(target) {
|
|
1273
1397
|
return !!target && SINCH_FN_URL_PATTERN.test(target);
|
|
@@ -1275,7 +1399,7 @@ function isOurWebhook(target) {
|
|
|
1275
1399
|
function isTunnelUrl(target) {
|
|
1276
1400
|
return !!target && target.includes("tunnel.fn");
|
|
1277
1401
|
}
|
|
1278
|
-
async function configureConversationWebhooks(
|
|
1402
|
+
async function configureConversationWebhooks(webhookUrl, config) {
|
|
1279
1403
|
try {
|
|
1280
1404
|
const conversationAppId = process.env.CONVERSATION_APP_ID;
|
|
1281
1405
|
const projectId = process.env.PROJECT_ID;
|
|
@@ -1285,7 +1409,6 @@ async function configureConversationWebhooks(tunnelUrl, config) {
|
|
|
1285
1409
|
console.log("\u{1F4A1} Conversation API not fully configured - skipping webhook setup");
|
|
1286
1410
|
return;
|
|
1287
1411
|
}
|
|
1288
|
-
const webhookUrl = `${tunnelUrl}/webhook/conversation`;
|
|
1289
1412
|
console.log(`\u{1F4AC} Conversation webhook URL: ${webhookUrl}`);
|
|
1290
1413
|
const sinchClient = new SinchClient2({
|
|
1291
1414
|
projectId,
|
|
@@ -1305,17 +1428,22 @@ async function configureConversationWebhooks(tunnelUrl, config) {
|
|
|
1305
1428
|
}
|
|
1306
1429
|
config.conversationWebhookId = deployedWebhook.id;
|
|
1307
1430
|
config.originalTarget = deployedWebhook.target;
|
|
1431
|
+
const hmacSecret = randomBytes(32).toString("hex");
|
|
1432
|
+
process.env.CONVERSATION_WEBHOOK_SECRET = hmacSecret;
|
|
1433
|
+
resetSinchClients();
|
|
1308
1434
|
await sinchClient.conversation.webhooks.update({
|
|
1309
1435
|
webhook_id: deployedWebhook.id,
|
|
1310
1436
|
webhookUpdateRequestBody: {
|
|
1311
|
-
target: webhookUrl
|
|
1437
|
+
target: webhookUrl,
|
|
1438
|
+
secret: hmacSecret
|
|
1312
1439
|
},
|
|
1313
|
-
update_mask: ["target"]
|
|
1440
|
+
update_mask: ["target", "secret"]
|
|
1314
1441
|
});
|
|
1315
1442
|
console.log(`\u2705 Updated Conversation webhook to tunnel: ${webhookUrl}`);
|
|
1443
|
+
console.log("\u{1F512} HMAC secret configured for webhook signature validation");
|
|
1316
1444
|
console.log("\u{1F4AC} Send a message to your Conversation app to test!");
|
|
1317
1445
|
} catch (error) {
|
|
1318
|
-
console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error.message);
|
|
1446
|
+
console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error instanceof Error ? error.message : error);
|
|
1319
1447
|
}
|
|
1320
1448
|
}
|
|
1321
1449
|
async function cleanupConversationWebhook(config) {
|
|
@@ -1352,8 +1480,10 @@ async function cleanupConversationWebhook(config) {
|
|
|
1352
1480
|
console.log(`\u{1F504} Restored webhook target to: ${restoreTarget}`);
|
|
1353
1481
|
config.conversationWebhookId = void 0;
|
|
1354
1482
|
config.originalTarget = void 0;
|
|
1483
|
+
delete process.env.CONVERSATION_WEBHOOK_SECRET;
|
|
1484
|
+
resetSinchClients();
|
|
1355
1485
|
} catch (error) {
|
|
1356
|
-
console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error.message);
|
|
1486
|
+
console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error instanceof Error ? error.message : error);
|
|
1357
1487
|
}
|
|
1358
1488
|
}
|
|
1359
1489
|
async function configureElevenLabs() {
|
|
@@ -1368,7 +1498,7 @@ async function configureElevenLabs() {
|
|
|
1368
1498
|
console.log("\u{1F916} ElevenLabs auto-configuration enabled");
|
|
1369
1499
|
console.log(` Agent ID: ${agentId}`);
|
|
1370
1500
|
} catch (error) {
|
|
1371
|
-
console.log("\u26A0\uFE0F Could not configure ElevenLabs:", error.message);
|
|
1501
|
+
console.log("\u26A0\uFE0F Could not configure ElevenLabs:", error instanceof Error ? error.message : error);
|
|
1372
1502
|
}
|
|
1373
1503
|
}
|
|
1374
1504
|
|
|
@@ -1405,14 +1535,14 @@ var TunnelClient = class {
|
|
|
1405
1535
|
timestampPart = ENCODING[t % 32] + timestampPart;
|
|
1406
1536
|
t = Math.floor(t / 32);
|
|
1407
1537
|
}
|
|
1408
|
-
const
|
|
1409
|
-
crypto.getRandomValues(
|
|
1538
|
+
const randomBytes2 = new Uint8Array(10);
|
|
1539
|
+
crypto.getRandomValues(randomBytes2);
|
|
1410
1540
|
let randomPart = "";
|
|
1411
1541
|
for (let i = 0; i < 10; i++) {
|
|
1412
|
-
const byte =
|
|
1542
|
+
const byte = randomBytes2[i];
|
|
1413
1543
|
randomPart += ENCODING[byte >> 3];
|
|
1414
1544
|
if (randomPart.length < 16) {
|
|
1415
|
-
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ?
|
|
1545
|
+
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ? randomBytes2[i + 1] >> 6 : 0)];
|
|
1416
1546
|
}
|
|
1417
1547
|
}
|
|
1418
1548
|
randomPart = randomPart.substring(0, 16);
|
|
@@ -1485,6 +1615,15 @@ var TunnelClient = class {
|
|
|
1485
1615
|
break;
|
|
1486
1616
|
}
|
|
1487
1617
|
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Build a full tunnel URL with optional sub-path and tunnel query param.
|
|
1620
|
+
* e.g. buildTunnelUrl('/webhook/conversation') →
|
|
1621
|
+
* https://tunnel.fn.sinch.com/ingress/webhook/conversation?tunnel=01KKT...
|
|
1622
|
+
*/
|
|
1623
|
+
buildTunnelUrl(path7) {
|
|
1624
|
+
const base = this.tunnelUrl.replace(/\/$/, "");
|
|
1625
|
+
return `${base}${path7 || ""}?tunnel=${this.tunnelId}`;
|
|
1626
|
+
}
|
|
1488
1627
|
handleWelcomeMessage(message) {
|
|
1489
1628
|
this.tunnelId = message.tunnelId || null;
|
|
1490
1629
|
this.tunnelUrl = message.publicUrl || null;
|
|
@@ -1495,7 +1634,11 @@ var TunnelClient = class {
|
|
|
1495
1634
|
}
|
|
1496
1635
|
}
|
|
1497
1636
|
async handleRequest(message) {
|
|
1637
|
+
const verbose = process.env.VERBOSE === "true";
|
|
1498
1638
|
console.log(`Forwarding ${message.method} request to ${message.path}`);
|
|
1639
|
+
if (verbose && message.body) {
|
|
1640
|
+
console.log(` \u2190 Request body: ${message.body.substring(0, 2e3)}`);
|
|
1641
|
+
}
|
|
1499
1642
|
try {
|
|
1500
1643
|
const localUrl = `http://localhost:${this.localPort}${message.path}${message.query || ""}`;
|
|
1501
1644
|
const axiosConfig = {
|
|
@@ -1528,9 +1671,15 @@ var TunnelClient = class {
|
|
|
1528
1671
|
headers,
|
|
1529
1672
|
body
|
|
1530
1673
|
};
|
|
1674
|
+
if (verbose) {
|
|
1675
|
+
console.log(` \u2192 Response ${response.status}: ${body.substring(0, 2e3)}`);
|
|
1676
|
+
}
|
|
1531
1677
|
this.ws?.send(JSON.stringify(responseMessage));
|
|
1532
1678
|
} catch (error) {
|
|
1533
|
-
console.error(
|
|
1679
|
+
console.error(`Error forwarding request: ${error.message} (${error.response?.status || "no response"})`);
|
|
1680
|
+
if (verbose && error.response?.data) {
|
|
1681
|
+
console.error(` \u2192 Error body: ${typeof error.response.data === "string" ? error.response.data : JSON.stringify(error.response.data)}`.substring(0, 2e3));
|
|
1682
|
+
}
|
|
1534
1683
|
const errorResponse = {
|
|
1535
1684
|
type: "response",
|
|
1536
1685
|
id: message.id,
|
|
@@ -1574,7 +1723,7 @@ var TunnelClient = class {
|
|
|
1574
1723
|
await this.configureVoiceWebhooks();
|
|
1575
1724
|
}
|
|
1576
1725
|
if (autoConfigConversation && process.env.CONVERSATION_APP_ID) {
|
|
1577
|
-
await configureConversationWebhooks(this.
|
|
1726
|
+
await configureConversationWebhooks(this.buildTunnelUrl("/webhook/conversation"), this.webhookConfig);
|
|
1578
1727
|
}
|
|
1579
1728
|
if (process.env.ELEVENLABS_AUTO_CONFIGURE === "true") {
|
|
1580
1729
|
await configureElevenLabs();
|
|
@@ -1601,7 +1750,7 @@ var TunnelClient = class {
|
|
|
1601
1750
|
updateUrl,
|
|
1602
1751
|
{
|
|
1603
1752
|
url: {
|
|
1604
|
-
primary: this.
|
|
1753
|
+
primary: this.buildTunnelUrl(),
|
|
1605
1754
|
fallback: null
|
|
1606
1755
|
}
|
|
1607
1756
|
},
|
|
@@ -1663,7 +1812,8 @@ var TunnelClient = class {
|
|
|
1663
1812
|
this.isConnected = false;
|
|
1664
1813
|
}
|
|
1665
1814
|
getTunnelUrl() {
|
|
1666
|
-
|
|
1815
|
+
if (!this.tunnelUrl || !this.tunnelId) return null;
|
|
1816
|
+
return this.buildTunnelUrl();
|
|
1667
1817
|
}
|
|
1668
1818
|
getIsConnected() {
|
|
1669
1819
|
return this.isConnected;
|
|
@@ -1786,19 +1936,19 @@ function displayApplicationCredentials() {
|
|
|
1786
1936
|
console.log(' Tip: Add VOICE_APPLICATION_KEY to .env and run "sinch auth login"');
|
|
1787
1937
|
}
|
|
1788
1938
|
}
|
|
1789
|
-
async function displayDetectedFunctions() {
|
|
1939
|
+
async function displayDetectedFunctions(port) {
|
|
1790
1940
|
try {
|
|
1791
1941
|
const functionPath = findFunctionPath3();
|
|
1792
1942
|
if (!fs5.existsSync(functionPath)) return;
|
|
1793
1943
|
const functionUrl = pathToFileURL2(functionPath).href;
|
|
1794
1944
|
const module = await import(functionUrl);
|
|
1795
1945
|
const userFunction = module.default || module;
|
|
1796
|
-
const functions = Object.keys(userFunction);
|
|
1946
|
+
const functions = Object.keys(userFunction).filter((k) => typeof userFunction[k] === "function");
|
|
1797
1947
|
if (functions.length > 0) {
|
|
1798
|
-
console.log("\nDetected functions
|
|
1948
|
+
console.log("\nDetected functions:");
|
|
1799
1949
|
for (const fn of functions) {
|
|
1800
1950
|
const type = isVoiceCallback(fn) ? "voice" : "custom";
|
|
1801
|
-
console.log(`
|
|
1951
|
+
console.log(` ${fn} (${type}): POST http://localhost:${port}/${fn}`);
|
|
1802
1952
|
}
|
|
1803
1953
|
}
|
|
1804
1954
|
} catch {
|
|
@@ -1838,6 +1988,17 @@ async function main() {
|
|
|
1838
1988
|
return module.default || module;
|
|
1839
1989
|
};
|
|
1840
1990
|
await loadUserFunction();
|
|
1991
|
+
let setupResult;
|
|
1992
|
+
const rawModule = await import(pathToFileURL2(findFunctionPath3()).href);
|
|
1993
|
+
const setupFn = rawModule.setup || rawModule.default?.setup;
|
|
1994
|
+
if (typeof setupFn === "function") {
|
|
1995
|
+
const { runtime, result } = createSinchRuntime();
|
|
1996
|
+
await Promise.resolve(setupFn(runtime));
|
|
1997
|
+
setupResult = result;
|
|
1998
|
+
if (verbose) {
|
|
1999
|
+
console.log(`[SETUP] Registered ${result.startupHandlers.length} startup handler(s), ${result.wsHandlers.size} WebSocket endpoint(s)`);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
1841
2002
|
setupRequestHandler(app, {
|
|
1842
2003
|
landingPageEnabled,
|
|
1843
2004
|
authConfig: userAuthConfig,
|
|
@@ -1866,17 +2027,22 @@ async function main() {
|
|
|
1866
2027
|
});
|
|
1867
2028
|
});
|
|
1868
2029
|
displayStartupInfo(config, verbose, port);
|
|
1869
|
-
|
|
2030
|
+
if (setupResult?.startupHandlers.length) {
|
|
2031
|
+
const context = buildLocalContext({}, config);
|
|
2032
|
+
for (const handler of setupResult.startupHandlers) {
|
|
2033
|
+
await handler(context);
|
|
2034
|
+
}
|
|
2035
|
+
console.log(`[SETUP] Startup hooks completed`);
|
|
2036
|
+
}
|
|
2037
|
+
const server = app.listen(port, async () => {
|
|
1870
2038
|
console.log(`Function server running on http://localhost:${port}`);
|
|
1871
|
-
|
|
1872
|
-
console.log(` ICE: POST http://localhost:${port}/ice`);
|
|
1873
|
-
console.log(` PIE: POST http://localhost:${port}/pie`);
|
|
1874
|
-
console.log(` ACE: POST http://localhost:${port}/ace`);
|
|
1875
|
-
console.log(` DICE: POST http://localhost:${port}/dice`);
|
|
1876
|
-
await displayDetectedFunctions();
|
|
2039
|
+
await displayDetectedFunctions(port);
|
|
1877
2040
|
if (!verbose) {
|
|
1878
2041
|
console.log("\nTip: Set VERBOSE=true or use --verbose for detailed output");
|
|
1879
2042
|
}
|
|
2043
|
+
if (setupResult?.wsHandlers.size) {
|
|
2044
|
+
installWebSocketHandlers(server, setupResult.wsHandlers, console.log);
|
|
2045
|
+
}
|
|
1880
2046
|
if (process.env.SINCH_TUNNEL === "true") {
|
|
1881
2047
|
console.log("\nStarting tunnel...");
|
|
1882
2048
|
const tunnelClient = new TunnelClient(port);
|