@sinch/functions-runtime 0.4.0 → 0.4.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 +358 -117
- package/dist/bin/sinch-runtime.js.map +1 -1
- package/dist/index.d.ts +48 -0
- package/dist/index.js +272 -100
- 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
|
*/
|
|
@@ -1292,6 +1321,12 @@ export declare function createLenientJsonParser(options?: JsonParsingOptions): (
|
|
|
1292
1321
|
* @internal
|
|
1293
1322
|
*/
|
|
1294
1323
|
export declare function setupJsonParsing(app: Express, options?: JsonParsingOptions): Express;
|
|
1324
|
+
/**
|
|
1325
|
+
* Declarative auth configuration exported by user functions.
|
|
1326
|
+
* - string[] — protect specific handler names
|
|
1327
|
+
* - '*' — protect all handlers
|
|
1328
|
+
*/
|
|
1329
|
+
export type AuthConfig = string[] | "*";
|
|
1295
1330
|
/**
|
|
1296
1331
|
* Get the landing page HTML content
|
|
1297
1332
|
* Exported so production runtime can also use it
|
|
@@ -1329,6 +1364,12 @@ export interface RequestHandlerOptions {
|
|
|
1329
1364
|
logger?: (...args: unknown[]) => void;
|
|
1330
1365
|
/** Enable landing page for browser requests at root (default: true) */
|
|
1331
1366
|
landingPageEnabled?: boolean;
|
|
1367
|
+
/** Declarative auth config from user module (string[] or '*') */
|
|
1368
|
+
authConfig?: AuthConfig;
|
|
1369
|
+
/** API key for Basic Auth validation */
|
|
1370
|
+
authKey?: string;
|
|
1371
|
+
/** API secret for Basic Auth validation */
|
|
1372
|
+
authSecret?: string;
|
|
1332
1373
|
/** Called when request starts */
|
|
1333
1374
|
onRequestStart?: (data: {
|
|
1334
1375
|
functionName: string;
|
|
@@ -1443,6 +1484,7 @@ export interface SinchClients {
|
|
|
1443
1484
|
sms?: SmsService;
|
|
1444
1485
|
numbers?: NumbersService;
|
|
1445
1486
|
validateWebhookSignature?: (requestData: WebhookRequestData) => boolean;
|
|
1487
|
+
validateConversationWebhook?: (headers: Record<string, string | string[] | undefined>, body: unknown) => boolean;
|
|
1446
1488
|
}
|
|
1447
1489
|
export interface WebhookRequestData {
|
|
1448
1490
|
method: string;
|
|
@@ -2408,6 +2450,12 @@ export declare class TunnelClient {
|
|
|
2408
2450
|
private generateTunnelId;
|
|
2409
2451
|
connect(): Promise<void>;
|
|
2410
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;
|
|
2411
2459
|
private handleWelcomeMessage;
|
|
2412
2460
|
private handleRequest;
|
|
2413
2461
|
private sendPong;
|
package/dist/index.js
CHANGED
|
@@ -934,6 +934,147 @@ function setupJsonParsing(app, options = {}) {
|
|
|
934
934
|
return app;
|
|
935
935
|
}
|
|
936
936
|
|
|
937
|
+
// ../runtime-shared/dist/auth/basic-auth.js
|
|
938
|
+
import { timingSafeEqual } from "crypto";
|
|
939
|
+
function validateBasicAuth(authHeader, expectedKey, expectedSecret) {
|
|
940
|
+
if (!authHeader) {
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
if (!authHeader.toLowerCase().startsWith("basic ")) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
const decoded = Buffer.from(authHeader.slice(6), "base64").toString("utf-8");
|
|
947
|
+
const colonIndex = decoded.indexOf(":");
|
|
948
|
+
if (colonIndex === -1) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
const providedKey = decoded.slice(0, colonIndex);
|
|
952
|
+
const providedSecret = decoded.slice(colonIndex + 1);
|
|
953
|
+
const expectedKeyBuf = Buffer.from(expectedKey);
|
|
954
|
+
const providedKeyBuf = Buffer.from(providedKey);
|
|
955
|
+
const expectedSecretBuf = Buffer.from(expectedSecret);
|
|
956
|
+
const providedSecretBuf = Buffer.from(providedSecret);
|
|
957
|
+
const keyMatch = expectedKeyBuf.length === providedKeyBuf.length && timingSafeEqual(expectedKeyBuf, providedKeyBuf);
|
|
958
|
+
const secretMatch = expectedSecretBuf.length === providedSecretBuf.length && timingSafeEqual(expectedSecretBuf, providedSecretBuf);
|
|
959
|
+
return keyMatch && secretMatch;
|
|
960
|
+
}
|
|
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
|
+
|
|
937
1078
|
// ../runtime-shared/dist/host/app.js
|
|
938
1079
|
import { createRequire as createRequire2 } from "module";
|
|
939
1080
|
import { pathToFileURL } from "url";
|
|
@@ -1155,7 +1296,13 @@ function buildBaseContext(req, config = {}) {
|
|
|
1155
1296
|
async function handleVoiceCallback(functionName, userFunction, context, callbackData, logger) {
|
|
1156
1297
|
const handler = userFunction[functionName];
|
|
1157
1298
|
if (!handler || typeof handler !== "function") {
|
|
1158
|
-
|
|
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: {} };
|
|
1159
1306
|
}
|
|
1160
1307
|
let result;
|
|
1161
1308
|
switch (functionName) {
|
|
@@ -1198,7 +1345,9 @@ async function handleCustomEndpoint(functionName, userFunction, context, request
|
|
|
1198
1345
|
handler = userFunction["home"];
|
|
1199
1346
|
}
|
|
1200
1347
|
if (!handler || typeof handler !== "function") {
|
|
1201
|
-
|
|
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.`);
|
|
1202
1351
|
}
|
|
1203
1352
|
const result = await handler(context, request);
|
|
1204
1353
|
if (logger) {
|
|
@@ -1236,7 +1385,7 @@ function setupRequestHandler(app, options = {}) {
|
|
|
1236
1385
|
const functionUrl = pathToFileURL(functionPath).href;
|
|
1237
1386
|
const module = await import(functionUrl);
|
|
1238
1387
|
return module.default || module;
|
|
1239
|
-
}, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, onRequestStart = () => {
|
|
1388
|
+
}, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, authConfig, authKey, authSecret, onRequestStart = () => {
|
|
1240
1389
|
}, onRequestEnd = () => {
|
|
1241
1390
|
} } = options;
|
|
1242
1391
|
app.use("/{*splat}", async (req, res) => {
|
|
@@ -1264,6 +1413,44 @@ function setupRequestHandler(app, options = {}) {
|
|
|
1264
1413
|
try {
|
|
1265
1414
|
const functionName = extractFunctionName(req.originalUrl, req.body);
|
|
1266
1415
|
logger(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${req.method} ${req.path} -> ${functionName}`);
|
|
1416
|
+
if (authConfig && authKey && authSecret) {
|
|
1417
|
+
const needsAuth = authConfig === "*" || Array.isArray(authConfig) && authConfig.includes(functionName);
|
|
1418
|
+
if (needsAuth) {
|
|
1419
|
+
const isValid = validateBasicAuth(req.headers.authorization, authKey, authSecret);
|
|
1420
|
+
if (!isValid) {
|
|
1421
|
+
logger(`[AUTH] Rejected unauthorized request to ${functionName}`);
|
|
1422
|
+
res.status(401).set("WWW-Authenticate", 'Basic realm="sinch-function"').json({ error: "Unauthorized" });
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
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
|
+
}
|
|
1267
1454
|
onRequestStart({ functionName, req });
|
|
1268
1455
|
const context = buildContext(req);
|
|
1269
1456
|
const userFunction = await Promise.resolve(loadUserFunction());
|
|
@@ -1339,73 +1526,6 @@ function setupRequestHandler(app, options = {}) {
|
|
|
1339
1526
|
});
|
|
1340
1527
|
}
|
|
1341
1528
|
|
|
1342
|
-
// ../runtime-shared/dist/sinch/index.js
|
|
1343
|
-
import { SinchClient, validateAuthenticationHeader } from "@sinch/sdk-core";
|
|
1344
|
-
function createSinchClients() {
|
|
1345
|
-
const clients = {};
|
|
1346
|
-
const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
|
|
1347
|
-
if (!hasCredentials) {
|
|
1348
|
-
return clients;
|
|
1349
|
-
}
|
|
1350
|
-
try {
|
|
1351
|
-
const sinchClient = new SinchClient({
|
|
1352
|
-
projectId: process.env.PROJECT_ID,
|
|
1353
|
-
keyId: process.env.PROJECT_ID_API_KEY,
|
|
1354
|
-
keySecret: process.env.PROJECT_ID_API_SECRET
|
|
1355
|
-
});
|
|
1356
|
-
if (process.env.CONVERSATION_APP_ID) {
|
|
1357
|
-
clients.conversation = sinchClient.conversation;
|
|
1358
|
-
console.log("[SINCH] Conversation API initialized");
|
|
1359
|
-
}
|
|
1360
|
-
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
1361
|
-
const voiceClient = new SinchClient({
|
|
1362
|
-
projectId: process.env.PROJECT_ID,
|
|
1363
|
-
keyId: process.env.PROJECT_ID_API_KEY,
|
|
1364
|
-
keySecret: process.env.PROJECT_ID_API_SECRET,
|
|
1365
|
-
applicationKey: process.env.VOICE_APPLICATION_KEY,
|
|
1366
|
-
applicationSecret: process.env.VOICE_APPLICATION_SECRET
|
|
1367
|
-
});
|
|
1368
|
-
clients.voice = voiceClient.voice;
|
|
1369
|
-
console.log("[SINCH] Voice API initialized with application credentials");
|
|
1370
|
-
}
|
|
1371
|
-
if (process.env.SMS_SERVICE_PLAN_ID) {
|
|
1372
|
-
clients.sms = sinchClient.sms;
|
|
1373
|
-
console.log("[SINCH] SMS API initialized");
|
|
1374
|
-
}
|
|
1375
|
-
if (process.env.ENABLE_NUMBERS_API === "true") {
|
|
1376
|
-
clients.numbers = sinchClient.numbers;
|
|
1377
|
-
console.log("[SINCH] Numbers API initialized");
|
|
1378
|
-
}
|
|
1379
|
-
} catch (error) {
|
|
1380
|
-
console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
|
|
1381
|
-
return {};
|
|
1382
|
-
}
|
|
1383
|
-
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
1384
|
-
clients.validateWebhookSignature = (requestData) => {
|
|
1385
|
-
console.log("[SINCH] Validating Voice webhook signature");
|
|
1386
|
-
try {
|
|
1387
|
-
const result = validateAuthenticationHeader(process.env.VOICE_APPLICATION_KEY, process.env.VOICE_APPLICATION_SECRET, requestData.headers, requestData.body, requestData.path, requestData.method);
|
|
1388
|
-
console.log("[SINCH] Validation result:", result ? "VALID" : "INVALID");
|
|
1389
|
-
return result;
|
|
1390
|
-
} catch (error) {
|
|
1391
|
-
console.error("[SINCH] Validation error:", error.message);
|
|
1392
|
-
return false;
|
|
1393
|
-
}
|
|
1394
|
-
};
|
|
1395
|
-
}
|
|
1396
|
-
return clients;
|
|
1397
|
-
}
|
|
1398
|
-
var cachedClients = null;
|
|
1399
|
-
function getSinchClients() {
|
|
1400
|
-
if (!cachedClients) {
|
|
1401
|
-
cachedClients = createSinchClients();
|
|
1402
|
-
}
|
|
1403
|
-
return cachedClients;
|
|
1404
|
-
}
|
|
1405
|
-
function resetSinchClients() {
|
|
1406
|
-
cachedClients = null;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
1529
|
// ../runtime-shared/dist/ai/elevenlabs/state.js
|
|
1410
1530
|
var ElevenLabsStateManager = class {
|
|
1411
1531
|
state = {
|
|
@@ -2457,7 +2577,7 @@ import * as path4 from "path";
|
|
|
2457
2577
|
var LocalStorage = class {
|
|
2458
2578
|
baseDir;
|
|
2459
2579
|
constructor(baseDir) {
|
|
2460
|
-
this.baseDir = baseDir ?? path4.join(process.cwd(), "storage");
|
|
2580
|
+
this.baseDir = baseDir ?? path4.join(process.cwd(), ".sinch", "storage");
|
|
2461
2581
|
}
|
|
2462
2582
|
resolvePath(key) {
|
|
2463
2583
|
const sanitized = key.replace(/^\/+/, "").replace(/\.\./g, "_");
|
|
@@ -2520,7 +2640,15 @@ import axios from "axios";
|
|
|
2520
2640
|
|
|
2521
2641
|
// src/tunnel/webhook-config.ts
|
|
2522
2642
|
import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
|
|
2523
|
-
|
|
2643
|
+
import { randomBytes } from "crypto";
|
|
2644
|
+
var SINCH_FN_URL_PATTERN = /\.fn(-\w+)?\.sinch\.com/;
|
|
2645
|
+
function isOurWebhook(target) {
|
|
2646
|
+
return !!target && SINCH_FN_URL_PATTERN.test(target);
|
|
2647
|
+
}
|
|
2648
|
+
function isTunnelUrl(target) {
|
|
2649
|
+
return !!target && target.includes("tunnel.fn");
|
|
2650
|
+
}
|
|
2651
|
+
async function configureConversationWebhooks(webhookUrl, config) {
|
|
2524
2652
|
try {
|
|
2525
2653
|
const conversationAppId = process.env.CONVERSATION_APP_ID;
|
|
2526
2654
|
const projectId = process.env.PROJECT_ID;
|
|
@@ -2530,7 +2658,6 @@ async function configureConversationWebhooks(tunnelUrl, config) {
|
|
|
2530
2658
|
console.log("\u{1F4A1} Conversation API not fully configured - skipping webhook setup");
|
|
2531
2659
|
return;
|
|
2532
2660
|
}
|
|
2533
|
-
const webhookUrl = `${tunnelUrl}/webhook/conversation`;
|
|
2534
2661
|
console.log(`\u{1F4AC} Conversation webhook URL: ${webhookUrl}`);
|
|
2535
2662
|
const sinchClient = new SinchClient2({
|
|
2536
2663
|
projectId,
|
|
@@ -2541,27 +2668,31 @@ async function configureConversationWebhooks(tunnelUrl, config) {
|
|
|
2541
2668
|
app_id: conversationAppId
|
|
2542
2669
|
});
|
|
2543
2670
|
const existingWebhooks = webhooksResult.webhooks || [];
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
}
|
|
2671
|
+
const deployedWebhook = existingWebhooks.find(
|
|
2672
|
+
(w) => isOurWebhook(w.target)
|
|
2673
|
+
);
|
|
2674
|
+
if (!deployedWebhook || !deployedWebhook.id) {
|
|
2675
|
+
console.log("\u26A0\uFE0F No deployed webhook found \u2014 deploy first");
|
|
2676
|
+
return;
|
|
2551
2677
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2678
|
+
config.conversationWebhookId = deployedWebhook.id;
|
|
2679
|
+
config.originalTarget = deployedWebhook.target;
|
|
2680
|
+
const hmacSecret = randomBytes(32).toString("hex");
|
|
2681
|
+
process.env.CONVERSATION_WEBHOOK_SECRET = hmacSecret;
|
|
2682
|
+
resetSinchClients();
|
|
2683
|
+
await sinchClient.conversation.webhooks.update({
|
|
2684
|
+
webhook_id: deployedWebhook.id,
|
|
2685
|
+
webhookUpdateRequestBody: {
|
|
2555
2686
|
target: webhookUrl,
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2687
|
+
secret: hmacSecret
|
|
2688
|
+
},
|
|
2689
|
+
update_mask: ["target", "secret"]
|
|
2559
2690
|
});
|
|
2560
|
-
|
|
2561
|
-
console.log(
|
|
2691
|
+
console.log(`\u2705 Updated Conversation webhook to tunnel: ${webhookUrl}`);
|
|
2692
|
+
console.log("\u{1F512} HMAC secret configured for webhook signature validation");
|
|
2562
2693
|
console.log("\u{1F4AC} Send a message to your Conversation app to test!");
|
|
2563
2694
|
} catch (error) {
|
|
2564
|
-
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);
|
|
2565
2696
|
}
|
|
2566
2697
|
}
|
|
2567
2698
|
async function cleanupConversationWebhook(config) {
|
|
@@ -2577,10 +2708,31 @@ async function cleanupConversationWebhook(config) {
|
|
|
2577
2708
|
keyId,
|
|
2578
2709
|
keySecret
|
|
2579
2710
|
});
|
|
2580
|
-
|
|
2581
|
-
|
|
2711
|
+
let restoreTarget = config.originalTarget;
|
|
2712
|
+
if (!restoreTarget || isTunnelUrl(restoreTarget)) {
|
|
2713
|
+
const functionName = process.env.FUNCTION_NAME || process.env.FUNCTION_ID;
|
|
2714
|
+
if (functionName) {
|
|
2715
|
+
restoreTarget = `https://${functionName}.fn-dev.sinch.com/webhook/conversation`;
|
|
2716
|
+
console.log(`\u{1F527} Derived restore target from env: ${restoreTarget}`);
|
|
2717
|
+
} else {
|
|
2718
|
+
console.log("\u26A0\uFE0F Cannot restore webhook \u2014 no FUNCTION_NAME or FUNCTION_ID available");
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
await sinchClient.conversation.webhooks.update({
|
|
2723
|
+
webhook_id: config.conversationWebhookId,
|
|
2724
|
+
webhookUpdateRequestBody: {
|
|
2725
|
+
target: restoreTarget
|
|
2726
|
+
},
|
|
2727
|
+
update_mask: ["target"]
|
|
2728
|
+
});
|
|
2729
|
+
console.log(`\u{1F504} Restored webhook target to: ${restoreTarget}`);
|
|
2582
2730
|
config.conversationWebhookId = void 0;
|
|
2731
|
+
config.originalTarget = void 0;
|
|
2732
|
+
delete process.env.CONVERSATION_WEBHOOK_SECRET;
|
|
2733
|
+
resetSinchClients();
|
|
2583
2734
|
} catch (error) {
|
|
2735
|
+
console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error instanceof Error ? error.message : error);
|
|
2584
2736
|
}
|
|
2585
2737
|
}
|
|
2586
2738
|
async function configureElevenLabs() {
|
|
@@ -2595,7 +2747,7 @@ async function configureElevenLabs() {
|
|
|
2595
2747
|
console.log("\u{1F916} ElevenLabs auto-configuration enabled");
|
|
2596
2748
|
console.log(` Agent ID: ${agentId}`);
|
|
2597
2749
|
} catch (error) {
|
|
2598
|
-
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);
|
|
2599
2751
|
}
|
|
2600
2752
|
}
|
|
2601
2753
|
|
|
@@ -2632,14 +2784,14 @@ var TunnelClient = class {
|
|
|
2632
2784
|
timestampPart = ENCODING[t % 32] + timestampPart;
|
|
2633
2785
|
t = Math.floor(t / 32);
|
|
2634
2786
|
}
|
|
2635
|
-
const
|
|
2636
|
-
crypto.getRandomValues(
|
|
2787
|
+
const randomBytes2 = new Uint8Array(10);
|
|
2788
|
+
crypto.getRandomValues(randomBytes2);
|
|
2637
2789
|
let randomPart = "";
|
|
2638
2790
|
for (let i = 0; i < 10; i++) {
|
|
2639
|
-
const byte =
|
|
2791
|
+
const byte = randomBytes2[i];
|
|
2640
2792
|
randomPart += ENCODING[byte >> 3];
|
|
2641
2793
|
if (randomPart.length < 16) {
|
|
2642
|
-
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ?
|
|
2794
|
+
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ? randomBytes2[i + 1] >> 6 : 0)];
|
|
2643
2795
|
}
|
|
2644
2796
|
}
|
|
2645
2797
|
randomPart = randomPart.substring(0, 16);
|
|
@@ -2712,6 +2864,15 @@ var TunnelClient = class {
|
|
|
2712
2864
|
break;
|
|
2713
2865
|
}
|
|
2714
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
|
+
}
|
|
2715
2876
|
handleWelcomeMessage(message) {
|
|
2716
2877
|
this.tunnelId = message.tunnelId || null;
|
|
2717
2878
|
this.tunnelUrl = message.publicUrl || null;
|
|
@@ -2722,7 +2883,11 @@ var TunnelClient = class {
|
|
|
2722
2883
|
}
|
|
2723
2884
|
}
|
|
2724
2885
|
async handleRequest(message) {
|
|
2886
|
+
const verbose = process.env.VERBOSE === "true";
|
|
2725
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
|
+
}
|
|
2726
2891
|
try {
|
|
2727
2892
|
const localUrl = `http://localhost:${this.localPort}${message.path}${message.query || ""}`;
|
|
2728
2893
|
const axiosConfig = {
|
|
@@ -2755,9 +2920,15 @@ var TunnelClient = class {
|
|
|
2755
2920
|
headers,
|
|
2756
2921
|
body
|
|
2757
2922
|
};
|
|
2923
|
+
if (verbose) {
|
|
2924
|
+
console.log(` \u2192 Response ${response.status}: ${body.substring(0, 2e3)}`);
|
|
2925
|
+
}
|
|
2758
2926
|
this.ws?.send(JSON.stringify(responseMessage));
|
|
2759
2927
|
} catch (error) {
|
|
2760
|
-
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
|
+
}
|
|
2761
2932
|
const errorResponse = {
|
|
2762
2933
|
type: "response",
|
|
2763
2934
|
id: message.id,
|
|
@@ -2801,7 +2972,7 @@ var TunnelClient = class {
|
|
|
2801
2972
|
await this.configureVoiceWebhooks();
|
|
2802
2973
|
}
|
|
2803
2974
|
if (autoConfigConversation && process.env.CONVERSATION_APP_ID) {
|
|
2804
|
-
await configureConversationWebhooks(this.
|
|
2975
|
+
await configureConversationWebhooks(this.buildTunnelUrl("/webhook/conversation"), this.webhookConfig);
|
|
2805
2976
|
}
|
|
2806
2977
|
if (process.env.ELEVENLABS_AUTO_CONFIGURE === "true") {
|
|
2807
2978
|
await configureElevenLabs();
|
|
@@ -2828,7 +2999,7 @@ var TunnelClient = class {
|
|
|
2828
2999
|
updateUrl,
|
|
2829
3000
|
{
|
|
2830
3001
|
url: {
|
|
2831
|
-
primary: this.
|
|
3002
|
+
primary: this.buildTunnelUrl(),
|
|
2832
3003
|
fallback: null
|
|
2833
3004
|
}
|
|
2834
3005
|
},
|
|
@@ -2890,7 +3061,8 @@ var TunnelClient = class {
|
|
|
2890
3061
|
this.isConnected = false;
|
|
2891
3062
|
}
|
|
2892
3063
|
getTunnelUrl() {
|
|
2893
|
-
|
|
3064
|
+
if (!this.tunnelUrl || !this.tunnelId) return null;
|
|
3065
|
+
return this.buildTunnelUrl();
|
|
2894
3066
|
}
|
|
2895
3067
|
getIsConnected() {
|
|
2896
3068
|
return this.isConnected;
|