@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
package/dist/index.d.ts
CHANGED
|
@@ -1006,6 +1006,35 @@ export interface FunctionContext {
|
|
|
1006
1006
|
/** Read a file from the assets/ directory (private, not served over HTTP) */
|
|
1007
1007
|
assets(filename: string): Promise<string>;
|
|
1008
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* WebSocket handler callback
|
|
1011
|
+
*/
|
|
1012
|
+
export type WebSocketHandler = (ws: import("ws").WebSocket, req: import("http").IncomingMessage) => void;
|
|
1013
|
+
/**
|
|
1014
|
+
* Runtime configuration object passed to the optional `setup()` export.
|
|
1015
|
+
*
|
|
1016
|
+
* Provides hooks for startup initialization and WebSocket endpoints
|
|
1017
|
+
* without exposing the raw Express app or HTTP server.
|
|
1018
|
+
*
|
|
1019
|
+
* @example
|
|
1020
|
+
* ```typescript
|
|
1021
|
+
* export function setup(runtime: SinchRuntime) {
|
|
1022
|
+
* runtime.onStartup(async (context) => {
|
|
1023
|
+
* // Initialize database, warm caches, etc.
|
|
1024
|
+
* });
|
|
1025
|
+
*
|
|
1026
|
+
* runtime.onWebSocket('/stream', (ws, req) => {
|
|
1027
|
+
* // Handle Sinch connectStream binary audio
|
|
1028
|
+
* });
|
|
1029
|
+
* }
|
|
1030
|
+
* ```
|
|
1031
|
+
*/
|
|
1032
|
+
export interface SinchRuntime {
|
|
1033
|
+
/** Register a callback to run once at startup, before the server accepts requests. */
|
|
1034
|
+
onStartup(handler: (context: FunctionContext) => Promise<void> | void): void;
|
|
1035
|
+
/** Register a WebSocket upgrade handler at the given path. */
|
|
1036
|
+
onWebSocket(path: string, handler: WebSocketHandler): void;
|
|
1037
|
+
}
|
|
1009
1038
|
/**
|
|
1010
1039
|
* Application credentials structure
|
|
1011
1040
|
*/
|
|
@@ -1455,6 +1484,7 @@ export interface SinchClients {
|
|
|
1455
1484
|
sms?: SmsService;
|
|
1456
1485
|
numbers?: NumbersService;
|
|
1457
1486
|
validateWebhookSignature?: (requestData: WebhookRequestData) => boolean;
|
|
1487
|
+
validateConversationWebhook?: (headers: Record<string, string | string[] | undefined>, body: unknown) => boolean;
|
|
1458
1488
|
}
|
|
1459
1489
|
export interface WebhookRequestData {
|
|
1460
1490
|
method: string;
|
|
@@ -2420,6 +2450,12 @@ export declare class TunnelClient {
|
|
|
2420
2450
|
private generateTunnelId;
|
|
2421
2451
|
connect(): Promise<void>;
|
|
2422
2452
|
private handleMessage;
|
|
2453
|
+
/**
|
|
2454
|
+
* Build a full tunnel URL with optional sub-path and tunnel query param.
|
|
2455
|
+
* e.g. buildTunnelUrl('/webhook/conversation') →
|
|
2456
|
+
* https://tunnel.fn.sinch.com/ingress/webhook/conversation?tunnel=01KKT...
|
|
2457
|
+
*/
|
|
2458
|
+
private buildTunnelUrl;
|
|
2423
2459
|
private handleWelcomeMessage;
|
|
2424
2460
|
private handleRequest;
|
|
2425
2461
|
private sendPong;
|
package/dist/index.js
CHANGED
|
@@ -959,6 +959,122 @@ function validateBasicAuth(authHeader, expectedKey, expectedSecret) {
|
|
|
959
959
|
return keyMatch && secretMatch;
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
+
// ../runtime-shared/dist/security/index.js
|
|
963
|
+
function shouldValidateWebhook(mode, isDevelopment) {
|
|
964
|
+
const normalizedMode = (mode || "deploy").toLowerCase();
|
|
965
|
+
switch (normalizedMode) {
|
|
966
|
+
case "never":
|
|
967
|
+
return false;
|
|
968
|
+
case "always":
|
|
969
|
+
return true;
|
|
970
|
+
case "deploy":
|
|
971
|
+
default:
|
|
972
|
+
return !isDevelopment;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
var VALID_MODES = ["never", "deploy", "always"];
|
|
976
|
+
function getProtectionMode(config) {
|
|
977
|
+
const envValue = process.env.WEBHOOK_PROTECTION ?? process.env.PROTECT_VOICE_CALLBACKS;
|
|
978
|
+
if (envValue) {
|
|
979
|
+
const normalized = envValue.toLowerCase();
|
|
980
|
+
if (VALID_MODES.includes(normalized)) {
|
|
981
|
+
return normalized;
|
|
982
|
+
}
|
|
983
|
+
console.warn(`[SECURITY] Unknown WEBHOOK_PROTECTION value "${envValue}", defaulting to "deploy"`);
|
|
984
|
+
return "deploy";
|
|
985
|
+
}
|
|
986
|
+
const configValue = config?.WebhookProtection ?? config?.webhookProtection ?? config?.ProtectVoiceCallbacks ?? config?.protectVoiceCallbacks;
|
|
987
|
+
if (typeof configValue === "string") {
|
|
988
|
+
const normalized = configValue.toLowerCase();
|
|
989
|
+
if (VALID_MODES.includes(normalized)) {
|
|
990
|
+
return normalized;
|
|
991
|
+
}
|
|
992
|
+
console.warn(`[SECURITY] Unknown webhook protection value "${configValue}", defaulting to "deploy"`);
|
|
993
|
+
return "deploy";
|
|
994
|
+
}
|
|
995
|
+
return "deploy";
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ../runtime-shared/dist/sinch/index.js
|
|
999
|
+
import { SinchClient, validateAuthenticationHeader, ConversationCallbackWebhooks } from "@sinch/sdk-core";
|
|
1000
|
+
function createSinchClients() {
|
|
1001
|
+
const clients = {};
|
|
1002
|
+
const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
|
|
1003
|
+
if (process.env.CONVERSATION_WEBHOOK_SECRET) {
|
|
1004
|
+
const callbackProcessor = new ConversationCallbackWebhooks(process.env.CONVERSATION_WEBHOOK_SECRET);
|
|
1005
|
+
clients.validateConversationWebhook = (headers, body) => {
|
|
1006
|
+
try {
|
|
1007
|
+
const result = callbackProcessor.validateAuthenticationHeader(headers, body);
|
|
1008
|
+
console.log("[SINCH] Conversation webhook validation:", result ? "VALID" : "INVALID");
|
|
1009
|
+
return result;
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
console.error("[SINCH] Conversation validation error:", error instanceof Error ? error.message : error);
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
if (!hasCredentials) {
|
|
1017
|
+
return clients;
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
const sinchClient = new SinchClient({
|
|
1021
|
+
projectId: process.env.PROJECT_ID,
|
|
1022
|
+
keyId: process.env.PROJECT_ID_API_KEY,
|
|
1023
|
+
keySecret: process.env.PROJECT_ID_API_SECRET
|
|
1024
|
+
});
|
|
1025
|
+
if (process.env.CONVERSATION_APP_ID) {
|
|
1026
|
+
clients.conversation = sinchClient.conversation;
|
|
1027
|
+
console.log("[SINCH] Conversation API initialized");
|
|
1028
|
+
}
|
|
1029
|
+
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
1030
|
+
const voiceClient = new SinchClient({
|
|
1031
|
+
projectId: process.env.PROJECT_ID,
|
|
1032
|
+
keyId: process.env.PROJECT_ID_API_KEY,
|
|
1033
|
+
keySecret: process.env.PROJECT_ID_API_SECRET,
|
|
1034
|
+
applicationKey: process.env.VOICE_APPLICATION_KEY,
|
|
1035
|
+
applicationSecret: process.env.VOICE_APPLICATION_SECRET
|
|
1036
|
+
});
|
|
1037
|
+
clients.voice = voiceClient.voice;
|
|
1038
|
+
console.log("[SINCH] Voice API initialized with application credentials");
|
|
1039
|
+
}
|
|
1040
|
+
if (process.env.SMS_SERVICE_PLAN_ID) {
|
|
1041
|
+
clients.sms = sinchClient.sms;
|
|
1042
|
+
console.log("[SINCH] SMS API initialized");
|
|
1043
|
+
}
|
|
1044
|
+
if (process.env.ENABLE_NUMBERS_API === "true") {
|
|
1045
|
+
clients.numbers = sinchClient.numbers;
|
|
1046
|
+
console.log("[SINCH] Numbers API initialized");
|
|
1047
|
+
}
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
|
|
1050
|
+
return {};
|
|
1051
|
+
}
|
|
1052
|
+
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
1053
|
+
clients.validateWebhookSignature = (requestData) => {
|
|
1054
|
+
console.log("[SINCH] Validating Voice webhook signature");
|
|
1055
|
+
try {
|
|
1056
|
+
const result = validateAuthenticationHeader(process.env.VOICE_APPLICATION_KEY, process.env.VOICE_APPLICATION_SECRET, requestData.headers, requestData.body, requestData.path, requestData.method);
|
|
1057
|
+
console.log("[SINCH] Validation result:", result ? "VALID" : "INVALID");
|
|
1058
|
+
return result;
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
console.error("[SINCH] Validation error:", error.message);
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
return clients;
|
|
1066
|
+
}
|
|
1067
|
+
var cachedClients = null;
|
|
1068
|
+
function getSinchClients() {
|
|
1069
|
+
if (!cachedClients) {
|
|
1070
|
+
cachedClients = createSinchClients();
|
|
1071
|
+
}
|
|
1072
|
+
return cachedClients;
|
|
1073
|
+
}
|
|
1074
|
+
function resetSinchClients() {
|
|
1075
|
+
cachedClients = null;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
962
1078
|
// ../runtime-shared/dist/host/app.js
|
|
963
1079
|
import { createRequire as createRequire2 } from "module";
|
|
964
1080
|
import { pathToFileURL } from "url";
|
|
@@ -1159,7 +1275,7 @@ var noOpStorage = {
|
|
|
1159
1275
|
};
|
|
1160
1276
|
function buildBaseContext(req, config = {}) {
|
|
1161
1277
|
return {
|
|
1162
|
-
requestId: req
|
|
1278
|
+
requestId: req?.headers?.["x-request-id"] || generateRequestId(),
|
|
1163
1279
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1164
1280
|
env: process.env,
|
|
1165
1281
|
config: {
|
|
@@ -1180,7 +1296,13 @@ function buildBaseContext(req, config = {}) {
|
|
|
1180
1296
|
async function handleVoiceCallback(functionName, userFunction, context, callbackData, logger) {
|
|
1181
1297
|
const handler = userFunction[functionName];
|
|
1182
1298
|
if (!handler || typeof handler !== "function") {
|
|
1183
|
-
|
|
1299
|
+
if (functionName === "ice") {
|
|
1300
|
+
throw new Error(`Voice callback 'ice' not found \u2014 export an ice() function in function.ts`);
|
|
1301
|
+
}
|
|
1302
|
+
if (logger) {
|
|
1303
|
+
logger(`${functionName.toUpperCase()} callback not implemented \u2014 returning 200`);
|
|
1304
|
+
}
|
|
1305
|
+
return { statusCode: 200, body: {}, headers: {} };
|
|
1184
1306
|
}
|
|
1185
1307
|
let result;
|
|
1186
1308
|
switch (functionName) {
|
|
@@ -1223,7 +1345,9 @@ async function handleCustomEndpoint(functionName, userFunction, context, request
|
|
|
1223
1345
|
handler = userFunction["home"];
|
|
1224
1346
|
}
|
|
1225
1347
|
if (!handler || typeof handler !== "function") {
|
|
1226
|
-
|
|
1348
|
+
const available = Object.keys(userFunction).filter((k) => typeof userFunction[k] === "function");
|
|
1349
|
+
const pathHint = functionName.endsWith("Webhook") ? `/webhook/${functionName.replace("Webhook", "")}` : `/${functionName}`;
|
|
1350
|
+
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.`);
|
|
1227
1351
|
}
|
|
1228
1352
|
const result = await handler(context, request);
|
|
1229
1353
|
if (logger) {
|
|
@@ -1300,6 +1424,33 @@ function setupRequestHandler(app, options = {}) {
|
|
|
1300
1424
|
}
|
|
1301
1425
|
}
|
|
1302
1426
|
}
|
|
1427
|
+
const protectionMode = getProtectionMode();
|
|
1428
|
+
const isDev = process.env.NODE_ENV !== "production" && process.env.ASPNETCORE_ENVIRONMENT !== "Production";
|
|
1429
|
+
if (shouldValidateWebhook(protectionMode, isDev)) {
|
|
1430
|
+
const sinchClients = getSinchClients();
|
|
1431
|
+
if (isVoiceCallback(functionName) && sinchClients.validateWebhookSignature) {
|
|
1432
|
+
const rawBody = req.rawBody ?? JSON.stringify(req.body);
|
|
1433
|
+
const isValid = sinchClients.validateWebhookSignature({
|
|
1434
|
+
method: req.method,
|
|
1435
|
+
path: req.path,
|
|
1436
|
+
headers: req.headers,
|
|
1437
|
+
body: rawBody
|
|
1438
|
+
});
|
|
1439
|
+
if (!isValid) {
|
|
1440
|
+
logger("[SECURITY] Voice webhook signature validation failed");
|
|
1441
|
+
res.status(401).json({ error: "Invalid webhook signature" });
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (functionName === "conversationWebhook" && sinchClients.validateConversationWebhook) {
|
|
1446
|
+
const isValid = sinchClients.validateConversationWebhook(req.headers, req.body);
|
|
1447
|
+
if (!isValid) {
|
|
1448
|
+
logger("[SECURITY] Conversation webhook signature validation failed");
|
|
1449
|
+
res.status(401).json({ error: "Invalid webhook signature" });
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1303
1454
|
onRequestStart({ functionName, req });
|
|
1304
1455
|
const context = buildContext(req);
|
|
1305
1456
|
const userFunction = await Promise.resolve(loadUserFunction());
|
|
@@ -1375,73 +1526,6 @@ function setupRequestHandler(app, options = {}) {
|
|
|
1375
1526
|
});
|
|
1376
1527
|
}
|
|
1377
1528
|
|
|
1378
|
-
// ../runtime-shared/dist/sinch/index.js
|
|
1379
|
-
import { SinchClient, validateAuthenticationHeader } from "@sinch/sdk-core";
|
|
1380
|
-
function createSinchClients() {
|
|
1381
|
-
const clients = {};
|
|
1382
|
-
const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
|
|
1383
|
-
if (!hasCredentials) {
|
|
1384
|
-
return clients;
|
|
1385
|
-
}
|
|
1386
|
-
try {
|
|
1387
|
-
const sinchClient = new SinchClient({
|
|
1388
|
-
projectId: process.env.PROJECT_ID,
|
|
1389
|
-
keyId: process.env.PROJECT_ID_API_KEY,
|
|
1390
|
-
keySecret: process.env.PROJECT_ID_API_SECRET
|
|
1391
|
-
});
|
|
1392
|
-
if (process.env.CONVERSATION_APP_ID) {
|
|
1393
|
-
clients.conversation = sinchClient.conversation;
|
|
1394
|
-
console.log("[SINCH] Conversation API initialized");
|
|
1395
|
-
}
|
|
1396
|
-
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
1397
|
-
const voiceClient = new SinchClient({
|
|
1398
|
-
projectId: process.env.PROJECT_ID,
|
|
1399
|
-
keyId: process.env.PROJECT_ID_API_KEY,
|
|
1400
|
-
keySecret: process.env.PROJECT_ID_API_SECRET,
|
|
1401
|
-
applicationKey: process.env.VOICE_APPLICATION_KEY,
|
|
1402
|
-
applicationSecret: process.env.VOICE_APPLICATION_SECRET
|
|
1403
|
-
});
|
|
1404
|
-
clients.voice = voiceClient.voice;
|
|
1405
|
-
console.log("[SINCH] Voice API initialized with application credentials");
|
|
1406
|
-
}
|
|
1407
|
-
if (process.env.SMS_SERVICE_PLAN_ID) {
|
|
1408
|
-
clients.sms = sinchClient.sms;
|
|
1409
|
-
console.log("[SINCH] SMS API initialized");
|
|
1410
|
-
}
|
|
1411
|
-
if (process.env.ENABLE_NUMBERS_API === "true") {
|
|
1412
|
-
clients.numbers = sinchClient.numbers;
|
|
1413
|
-
console.log("[SINCH] Numbers API initialized");
|
|
1414
|
-
}
|
|
1415
|
-
} catch (error) {
|
|
1416
|
-
console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
|
|
1417
|
-
return {};
|
|
1418
|
-
}
|
|
1419
|
-
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
1420
|
-
clients.validateWebhookSignature = (requestData) => {
|
|
1421
|
-
console.log("[SINCH] Validating Voice webhook signature");
|
|
1422
|
-
try {
|
|
1423
|
-
const result = validateAuthenticationHeader(process.env.VOICE_APPLICATION_KEY, process.env.VOICE_APPLICATION_SECRET, requestData.headers, requestData.body, requestData.path, requestData.method);
|
|
1424
|
-
console.log("[SINCH] Validation result:", result ? "VALID" : "INVALID");
|
|
1425
|
-
return result;
|
|
1426
|
-
} catch (error) {
|
|
1427
|
-
console.error("[SINCH] Validation error:", error.message);
|
|
1428
|
-
return false;
|
|
1429
|
-
}
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
return clients;
|
|
1433
|
-
}
|
|
1434
|
-
var cachedClients = null;
|
|
1435
|
-
function getSinchClients() {
|
|
1436
|
-
if (!cachedClients) {
|
|
1437
|
-
cachedClients = createSinchClients();
|
|
1438
|
-
}
|
|
1439
|
-
return cachedClients;
|
|
1440
|
-
}
|
|
1441
|
-
function resetSinchClients() {
|
|
1442
|
-
cachedClients = null;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
1529
|
// ../runtime-shared/dist/ai/elevenlabs/state.js
|
|
1446
1530
|
var ElevenLabsStateManager = class {
|
|
1447
1531
|
state = {
|
|
@@ -2556,6 +2640,7 @@ import axios from "axios";
|
|
|
2556
2640
|
|
|
2557
2641
|
// src/tunnel/webhook-config.ts
|
|
2558
2642
|
import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
|
|
2643
|
+
import { randomBytes } from "crypto";
|
|
2559
2644
|
var SINCH_FN_URL_PATTERN = /\.fn(-\w+)?\.sinch\.com/;
|
|
2560
2645
|
function isOurWebhook(target) {
|
|
2561
2646
|
return !!target && SINCH_FN_URL_PATTERN.test(target);
|
|
@@ -2563,7 +2648,7 @@ function isOurWebhook(target) {
|
|
|
2563
2648
|
function isTunnelUrl(target) {
|
|
2564
2649
|
return !!target && target.includes("tunnel.fn");
|
|
2565
2650
|
}
|
|
2566
|
-
async function configureConversationWebhooks(
|
|
2651
|
+
async function configureConversationWebhooks(webhookUrl, config) {
|
|
2567
2652
|
try {
|
|
2568
2653
|
const conversationAppId = process.env.CONVERSATION_APP_ID;
|
|
2569
2654
|
const projectId = process.env.PROJECT_ID;
|
|
@@ -2573,7 +2658,6 @@ async function configureConversationWebhooks(tunnelUrl, config) {
|
|
|
2573
2658
|
console.log("\u{1F4A1} Conversation API not fully configured - skipping webhook setup");
|
|
2574
2659
|
return;
|
|
2575
2660
|
}
|
|
2576
|
-
const webhookUrl = `${tunnelUrl}/webhook/conversation`;
|
|
2577
2661
|
console.log(`\u{1F4AC} Conversation webhook URL: ${webhookUrl}`);
|
|
2578
2662
|
const sinchClient = new SinchClient2({
|
|
2579
2663
|
projectId,
|
|
@@ -2593,17 +2677,22 @@ async function configureConversationWebhooks(tunnelUrl, config) {
|
|
|
2593
2677
|
}
|
|
2594
2678
|
config.conversationWebhookId = deployedWebhook.id;
|
|
2595
2679
|
config.originalTarget = deployedWebhook.target;
|
|
2680
|
+
const hmacSecret = randomBytes(32).toString("hex");
|
|
2681
|
+
process.env.CONVERSATION_WEBHOOK_SECRET = hmacSecret;
|
|
2682
|
+
resetSinchClients();
|
|
2596
2683
|
await sinchClient.conversation.webhooks.update({
|
|
2597
2684
|
webhook_id: deployedWebhook.id,
|
|
2598
2685
|
webhookUpdateRequestBody: {
|
|
2599
|
-
target: webhookUrl
|
|
2686
|
+
target: webhookUrl,
|
|
2687
|
+
secret: hmacSecret
|
|
2600
2688
|
},
|
|
2601
|
-
update_mask: ["target"]
|
|
2689
|
+
update_mask: ["target", "secret"]
|
|
2602
2690
|
});
|
|
2603
2691
|
console.log(`\u2705 Updated Conversation webhook to tunnel: ${webhookUrl}`);
|
|
2692
|
+
console.log("\u{1F512} HMAC secret configured for webhook signature validation");
|
|
2604
2693
|
console.log("\u{1F4AC} Send a message to your Conversation app to test!");
|
|
2605
2694
|
} catch (error) {
|
|
2606
|
-
console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error.message);
|
|
2695
|
+
console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error instanceof Error ? error.message : error);
|
|
2607
2696
|
}
|
|
2608
2697
|
}
|
|
2609
2698
|
async function cleanupConversationWebhook(config) {
|
|
@@ -2640,8 +2729,10 @@ async function cleanupConversationWebhook(config) {
|
|
|
2640
2729
|
console.log(`\u{1F504} Restored webhook target to: ${restoreTarget}`);
|
|
2641
2730
|
config.conversationWebhookId = void 0;
|
|
2642
2731
|
config.originalTarget = void 0;
|
|
2732
|
+
delete process.env.CONVERSATION_WEBHOOK_SECRET;
|
|
2733
|
+
resetSinchClients();
|
|
2643
2734
|
} catch (error) {
|
|
2644
|
-
console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error.message);
|
|
2735
|
+
console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error instanceof Error ? error.message : error);
|
|
2645
2736
|
}
|
|
2646
2737
|
}
|
|
2647
2738
|
async function configureElevenLabs() {
|
|
@@ -2656,7 +2747,7 @@ async function configureElevenLabs() {
|
|
|
2656
2747
|
console.log("\u{1F916} ElevenLabs auto-configuration enabled");
|
|
2657
2748
|
console.log(` Agent ID: ${agentId}`);
|
|
2658
2749
|
} catch (error) {
|
|
2659
|
-
console.log("\u26A0\uFE0F Could not configure ElevenLabs:", error.message);
|
|
2750
|
+
console.log("\u26A0\uFE0F Could not configure ElevenLabs:", error instanceof Error ? error.message : error);
|
|
2660
2751
|
}
|
|
2661
2752
|
}
|
|
2662
2753
|
|
|
@@ -2693,14 +2784,14 @@ var TunnelClient = class {
|
|
|
2693
2784
|
timestampPart = ENCODING[t % 32] + timestampPart;
|
|
2694
2785
|
t = Math.floor(t / 32);
|
|
2695
2786
|
}
|
|
2696
|
-
const
|
|
2697
|
-
crypto.getRandomValues(
|
|
2787
|
+
const randomBytes2 = new Uint8Array(10);
|
|
2788
|
+
crypto.getRandomValues(randomBytes2);
|
|
2698
2789
|
let randomPart = "";
|
|
2699
2790
|
for (let i = 0; i < 10; i++) {
|
|
2700
|
-
const byte =
|
|
2791
|
+
const byte = randomBytes2[i];
|
|
2701
2792
|
randomPart += ENCODING[byte >> 3];
|
|
2702
2793
|
if (randomPart.length < 16) {
|
|
2703
|
-
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ?
|
|
2794
|
+
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ? randomBytes2[i + 1] >> 6 : 0)];
|
|
2704
2795
|
}
|
|
2705
2796
|
}
|
|
2706
2797
|
randomPart = randomPart.substring(0, 16);
|
|
@@ -2773,6 +2864,15 @@ var TunnelClient = class {
|
|
|
2773
2864
|
break;
|
|
2774
2865
|
}
|
|
2775
2866
|
}
|
|
2867
|
+
/**
|
|
2868
|
+
* Build a full tunnel URL with optional sub-path and tunnel query param.
|
|
2869
|
+
* e.g. buildTunnelUrl('/webhook/conversation') →
|
|
2870
|
+
* https://tunnel.fn.sinch.com/ingress/webhook/conversation?tunnel=01KKT...
|
|
2871
|
+
*/
|
|
2872
|
+
buildTunnelUrl(path6) {
|
|
2873
|
+
const base = this.tunnelUrl.replace(/\/$/, "");
|
|
2874
|
+
return `${base}${path6 || ""}?tunnel=${this.tunnelId}`;
|
|
2875
|
+
}
|
|
2776
2876
|
handleWelcomeMessage(message) {
|
|
2777
2877
|
this.tunnelId = message.tunnelId || null;
|
|
2778
2878
|
this.tunnelUrl = message.publicUrl || null;
|
|
@@ -2783,7 +2883,11 @@ var TunnelClient = class {
|
|
|
2783
2883
|
}
|
|
2784
2884
|
}
|
|
2785
2885
|
async handleRequest(message) {
|
|
2886
|
+
const verbose = process.env.VERBOSE === "true";
|
|
2786
2887
|
console.log(`Forwarding ${message.method} request to ${message.path}`);
|
|
2888
|
+
if (verbose && message.body) {
|
|
2889
|
+
console.log(` \u2190 Request body: ${message.body.substring(0, 2e3)}`);
|
|
2890
|
+
}
|
|
2787
2891
|
try {
|
|
2788
2892
|
const localUrl = `http://localhost:${this.localPort}${message.path}${message.query || ""}`;
|
|
2789
2893
|
const axiosConfig = {
|
|
@@ -2816,9 +2920,15 @@ var TunnelClient = class {
|
|
|
2816
2920
|
headers,
|
|
2817
2921
|
body
|
|
2818
2922
|
};
|
|
2923
|
+
if (verbose) {
|
|
2924
|
+
console.log(` \u2192 Response ${response.status}: ${body.substring(0, 2e3)}`);
|
|
2925
|
+
}
|
|
2819
2926
|
this.ws?.send(JSON.stringify(responseMessage));
|
|
2820
2927
|
} catch (error) {
|
|
2821
|
-
console.error(
|
|
2928
|
+
console.error(`Error forwarding request: ${error.message} (${error.response?.status || "no response"})`);
|
|
2929
|
+
if (verbose && error.response?.data) {
|
|
2930
|
+
console.error(` \u2192 Error body: ${typeof error.response.data === "string" ? error.response.data : JSON.stringify(error.response.data)}`.substring(0, 2e3));
|
|
2931
|
+
}
|
|
2822
2932
|
const errorResponse = {
|
|
2823
2933
|
type: "response",
|
|
2824
2934
|
id: message.id,
|
|
@@ -2862,7 +2972,7 @@ var TunnelClient = class {
|
|
|
2862
2972
|
await this.configureVoiceWebhooks();
|
|
2863
2973
|
}
|
|
2864
2974
|
if (autoConfigConversation && process.env.CONVERSATION_APP_ID) {
|
|
2865
|
-
await configureConversationWebhooks(this.
|
|
2975
|
+
await configureConversationWebhooks(this.buildTunnelUrl("/webhook/conversation"), this.webhookConfig);
|
|
2866
2976
|
}
|
|
2867
2977
|
if (process.env.ELEVENLABS_AUTO_CONFIGURE === "true") {
|
|
2868
2978
|
await configureElevenLabs();
|
|
@@ -2889,7 +2999,7 @@ var TunnelClient = class {
|
|
|
2889
2999
|
updateUrl,
|
|
2890
3000
|
{
|
|
2891
3001
|
url: {
|
|
2892
|
-
primary: this.
|
|
3002
|
+
primary: this.buildTunnelUrl(),
|
|
2893
3003
|
fallback: null
|
|
2894
3004
|
}
|
|
2895
3005
|
},
|
|
@@ -2951,7 +3061,8 @@ var TunnelClient = class {
|
|
|
2951
3061
|
this.isConnected = false;
|
|
2952
3062
|
}
|
|
2953
3063
|
getTunnelUrl() {
|
|
2954
|
-
|
|
3064
|
+
if (!this.tunnelUrl || !this.tunnelId) return null;
|
|
3065
|
+
return this.buildTunnelUrl();
|
|
2955
3066
|
}
|
|
2956
3067
|
getIsConnected() {
|
|
2957
3068
|
return this.isConnected;
|