@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
|
@@ -160,6 +160,147 @@ function setupJsonParsing(app, options = {}) {
|
|
|
160
160
|
return app;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// ../runtime-shared/dist/auth/basic-auth.js
|
|
164
|
+
import { timingSafeEqual } from "crypto";
|
|
165
|
+
function validateBasicAuth(authHeader, expectedKey, expectedSecret) {
|
|
166
|
+
if (!authHeader) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (!authHeader.toLowerCase().startsWith("basic ")) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const decoded = Buffer.from(authHeader.slice(6), "base64").toString("utf-8");
|
|
173
|
+
const colonIndex = decoded.indexOf(":");
|
|
174
|
+
if (colonIndex === -1) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const providedKey = decoded.slice(0, colonIndex);
|
|
178
|
+
const providedSecret = decoded.slice(colonIndex + 1);
|
|
179
|
+
const expectedKeyBuf = Buffer.from(expectedKey);
|
|
180
|
+
const providedKeyBuf = Buffer.from(providedKey);
|
|
181
|
+
const expectedSecretBuf = Buffer.from(expectedSecret);
|
|
182
|
+
const providedSecretBuf = Buffer.from(providedSecret);
|
|
183
|
+
const keyMatch = expectedKeyBuf.length === providedKeyBuf.length && timingSafeEqual(expectedKeyBuf, providedKeyBuf);
|
|
184
|
+
const secretMatch = expectedSecretBuf.length === providedSecretBuf.length && timingSafeEqual(expectedSecretBuf, providedSecretBuf);
|
|
185
|
+
return keyMatch && secretMatch;
|
|
186
|
+
}
|
|
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
|
+
|
|
163
304
|
// ../runtime-shared/dist/host/app.js
|
|
164
305
|
import { createRequire as createRequire2 } from "module";
|
|
165
306
|
import { pathToFileURL } from "url";
|
|
@@ -381,7 +522,13 @@ function buildBaseContext(req, config = {}) {
|
|
|
381
522
|
async function handleVoiceCallback(functionName, userFunction, context, callbackData, logger) {
|
|
382
523
|
const handler = userFunction[functionName];
|
|
383
524
|
if (!handler || typeof handler !== "function") {
|
|
384
|
-
|
|
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: {} };
|
|
385
532
|
}
|
|
386
533
|
let result;
|
|
387
534
|
switch (functionName) {
|
|
@@ -424,7 +571,9 @@ async function handleCustomEndpoint(functionName, userFunction, context, request
|
|
|
424
571
|
handler = userFunction["home"];
|
|
425
572
|
}
|
|
426
573
|
if (!handler || typeof handler !== "function") {
|
|
427
|
-
|
|
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.`);
|
|
428
577
|
}
|
|
429
578
|
const result = await handler(context, request);
|
|
430
579
|
if (logger) {
|
|
@@ -456,13 +605,49 @@ function createApp(options = {}) {
|
|
|
456
605
|
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
|
457
606
|
return app;
|
|
458
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
|
+
}
|
|
459
644
|
function setupRequestHandler(app, options = {}) {
|
|
460
645
|
const { loadUserFunction = async () => {
|
|
461
646
|
const functionPath = findFunctionPath();
|
|
462
647
|
const functionUrl = pathToFileURL(functionPath).href;
|
|
463
648
|
const module = await import(functionUrl);
|
|
464
649
|
return module.default || module;
|
|
465
|
-
}, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, onRequestStart = () => {
|
|
650
|
+
}, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, authConfig, authKey, authSecret, onRequestStart = () => {
|
|
466
651
|
}, onRequestEnd = () => {
|
|
467
652
|
} } = options;
|
|
468
653
|
app.use("/{*splat}", async (req, res) => {
|
|
@@ -490,6 +675,44 @@ function setupRequestHandler(app, options = {}) {
|
|
|
490
675
|
try {
|
|
491
676
|
const functionName = extractFunctionName(req.originalUrl, req.body);
|
|
492
677
|
logger(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${req.method} ${req.path} -> ${functionName}`);
|
|
678
|
+
if (authConfig && authKey && authSecret) {
|
|
679
|
+
const needsAuth = authConfig === "*" || Array.isArray(authConfig) && authConfig.includes(functionName);
|
|
680
|
+
if (needsAuth) {
|
|
681
|
+
const isValid = validateBasicAuth(req.headers.authorization, authKey, authSecret);
|
|
682
|
+
if (!isValid) {
|
|
683
|
+
logger(`[AUTH] Rejected unauthorized request to ${functionName}`);
|
|
684
|
+
res.status(401).set("WWW-Authenticate", 'Basic realm="sinch-function"').json({ error: "Unauthorized" });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
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
|
+
}
|
|
493
716
|
onRequestStart({ functionName, req });
|
|
494
717
|
const context = buildContext(req);
|
|
495
718
|
const userFunction = await Promise.resolve(loadUserFunction());
|
|
@@ -565,70 +788,6 @@ function setupRequestHandler(app, options = {}) {
|
|
|
565
788
|
});
|
|
566
789
|
}
|
|
567
790
|
|
|
568
|
-
// ../runtime-shared/dist/sinch/index.js
|
|
569
|
-
import { SinchClient, validateAuthenticationHeader } from "@sinch/sdk-core";
|
|
570
|
-
function createSinchClients() {
|
|
571
|
-
const clients = {};
|
|
572
|
-
const hasCredentials = process.env.PROJECT_ID && process.env.PROJECT_ID_API_KEY && process.env.PROJECT_ID_API_SECRET;
|
|
573
|
-
if (!hasCredentials) {
|
|
574
|
-
return clients;
|
|
575
|
-
}
|
|
576
|
-
try {
|
|
577
|
-
const sinchClient = new SinchClient({
|
|
578
|
-
projectId: process.env.PROJECT_ID,
|
|
579
|
-
keyId: process.env.PROJECT_ID_API_KEY,
|
|
580
|
-
keySecret: process.env.PROJECT_ID_API_SECRET
|
|
581
|
-
});
|
|
582
|
-
if (process.env.CONVERSATION_APP_ID) {
|
|
583
|
-
clients.conversation = sinchClient.conversation;
|
|
584
|
-
console.log("[SINCH] Conversation API initialized");
|
|
585
|
-
}
|
|
586
|
-
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
587
|
-
const voiceClient = new SinchClient({
|
|
588
|
-
projectId: process.env.PROJECT_ID,
|
|
589
|
-
keyId: process.env.PROJECT_ID_API_KEY,
|
|
590
|
-
keySecret: process.env.PROJECT_ID_API_SECRET,
|
|
591
|
-
applicationKey: process.env.VOICE_APPLICATION_KEY,
|
|
592
|
-
applicationSecret: process.env.VOICE_APPLICATION_SECRET
|
|
593
|
-
});
|
|
594
|
-
clients.voice = voiceClient.voice;
|
|
595
|
-
console.log("[SINCH] Voice API initialized with application credentials");
|
|
596
|
-
}
|
|
597
|
-
if (process.env.SMS_SERVICE_PLAN_ID) {
|
|
598
|
-
clients.sms = sinchClient.sms;
|
|
599
|
-
console.log("[SINCH] SMS API initialized");
|
|
600
|
-
}
|
|
601
|
-
if (process.env.ENABLE_NUMBERS_API === "true") {
|
|
602
|
-
clients.numbers = sinchClient.numbers;
|
|
603
|
-
console.log("[SINCH] Numbers API initialized");
|
|
604
|
-
}
|
|
605
|
-
} catch (error) {
|
|
606
|
-
console.error("[SINCH] Failed to initialize Sinch clients:", error.message);
|
|
607
|
-
return {};
|
|
608
|
-
}
|
|
609
|
-
if (process.env.VOICE_APPLICATION_KEY && process.env.VOICE_APPLICATION_SECRET) {
|
|
610
|
-
clients.validateWebhookSignature = (requestData) => {
|
|
611
|
-
console.log("[SINCH] Validating Voice webhook signature");
|
|
612
|
-
try {
|
|
613
|
-
const result = validateAuthenticationHeader(process.env.VOICE_APPLICATION_KEY, process.env.VOICE_APPLICATION_SECRET, requestData.headers, requestData.body, requestData.path, requestData.method);
|
|
614
|
-
console.log("[SINCH] Validation result:", result ? "VALID" : "INVALID");
|
|
615
|
-
return result;
|
|
616
|
-
} catch (error) {
|
|
617
|
-
console.error("[SINCH] Validation error:", error.message);
|
|
618
|
-
return false;
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
return clients;
|
|
623
|
-
}
|
|
624
|
-
var cachedClients = null;
|
|
625
|
-
function getSinchClients() {
|
|
626
|
-
if (!cachedClients) {
|
|
627
|
-
cachedClients = createSinchClients();
|
|
628
|
-
}
|
|
629
|
-
return cachedClients;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
791
|
// ../runtime-shared/dist/ai/elevenlabs/state.js
|
|
633
792
|
var ElevenLabsStateManager = class {
|
|
634
793
|
state = {
|
|
@@ -780,7 +939,7 @@ import * as path4 from "path";
|
|
|
780
939
|
var LocalStorage = class {
|
|
781
940
|
baseDir;
|
|
782
941
|
constructor(baseDir) {
|
|
783
|
-
this.baseDir = baseDir ?? path4.join(process.cwd(), "storage");
|
|
942
|
+
this.baseDir = baseDir ?? path4.join(process.cwd(), ".sinch", "storage");
|
|
784
943
|
}
|
|
785
944
|
resolvePath(key) {
|
|
786
945
|
const sanitized = key.replace(/^\/+/, "").replace(/\.\./g, "_");
|
|
@@ -1232,7 +1391,15 @@ import axios from "axios";
|
|
|
1232
1391
|
|
|
1233
1392
|
// src/tunnel/webhook-config.ts
|
|
1234
1393
|
import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
|
|
1235
|
-
|
|
1394
|
+
import { randomBytes } from "crypto";
|
|
1395
|
+
var SINCH_FN_URL_PATTERN = /\.fn(-\w+)?\.sinch\.com/;
|
|
1396
|
+
function isOurWebhook(target) {
|
|
1397
|
+
return !!target && SINCH_FN_URL_PATTERN.test(target);
|
|
1398
|
+
}
|
|
1399
|
+
function isTunnelUrl(target) {
|
|
1400
|
+
return !!target && target.includes("tunnel.fn");
|
|
1401
|
+
}
|
|
1402
|
+
async function configureConversationWebhooks(webhookUrl, config) {
|
|
1236
1403
|
try {
|
|
1237
1404
|
const conversationAppId = process.env.CONVERSATION_APP_ID;
|
|
1238
1405
|
const projectId = process.env.PROJECT_ID;
|
|
@@ -1242,7 +1409,6 @@ async function configureConversationWebhooks(tunnelUrl, config) {
|
|
|
1242
1409
|
console.log("\u{1F4A1} Conversation API not fully configured - skipping webhook setup");
|
|
1243
1410
|
return;
|
|
1244
1411
|
}
|
|
1245
|
-
const webhookUrl = `${tunnelUrl}/webhook/conversation`;
|
|
1246
1412
|
console.log(`\u{1F4AC} Conversation webhook URL: ${webhookUrl}`);
|
|
1247
1413
|
const sinchClient = new SinchClient2({
|
|
1248
1414
|
projectId,
|
|
@@ -1253,27 +1419,31 @@ async function configureConversationWebhooks(tunnelUrl, config) {
|
|
|
1253
1419
|
app_id: conversationAppId
|
|
1254
1420
|
});
|
|
1255
1421
|
const existingWebhooks = webhooksResult.webhooks || [];
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
}
|
|
1422
|
+
const deployedWebhook = existingWebhooks.find(
|
|
1423
|
+
(w) => isOurWebhook(w.target)
|
|
1424
|
+
);
|
|
1425
|
+
if (!deployedWebhook || !deployedWebhook.id) {
|
|
1426
|
+
console.log("\u26A0\uFE0F No deployed webhook found \u2014 deploy first");
|
|
1427
|
+
return;
|
|
1263
1428
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1429
|
+
config.conversationWebhookId = deployedWebhook.id;
|
|
1430
|
+
config.originalTarget = deployedWebhook.target;
|
|
1431
|
+
const hmacSecret = randomBytes(32).toString("hex");
|
|
1432
|
+
process.env.CONVERSATION_WEBHOOK_SECRET = hmacSecret;
|
|
1433
|
+
resetSinchClients();
|
|
1434
|
+
await sinchClient.conversation.webhooks.update({
|
|
1435
|
+
webhook_id: deployedWebhook.id,
|
|
1436
|
+
webhookUpdateRequestBody: {
|
|
1267
1437
|
target: webhookUrl,
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1438
|
+
secret: hmacSecret
|
|
1439
|
+
},
|
|
1440
|
+
update_mask: ["target", "secret"]
|
|
1271
1441
|
});
|
|
1272
|
-
|
|
1273
|
-
console.log(
|
|
1442
|
+
console.log(`\u2705 Updated Conversation webhook to tunnel: ${webhookUrl}`);
|
|
1443
|
+
console.log("\u{1F512} HMAC secret configured for webhook signature validation");
|
|
1274
1444
|
console.log("\u{1F4AC} Send a message to your Conversation app to test!");
|
|
1275
1445
|
} catch (error) {
|
|
1276
|
-
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);
|
|
1277
1447
|
}
|
|
1278
1448
|
}
|
|
1279
1449
|
async function cleanupConversationWebhook(config) {
|
|
@@ -1289,10 +1459,31 @@ async function cleanupConversationWebhook(config) {
|
|
|
1289
1459
|
keyId,
|
|
1290
1460
|
keySecret
|
|
1291
1461
|
});
|
|
1292
|
-
|
|
1293
|
-
|
|
1462
|
+
let restoreTarget = config.originalTarget;
|
|
1463
|
+
if (!restoreTarget || isTunnelUrl(restoreTarget)) {
|
|
1464
|
+
const functionName = process.env.FUNCTION_NAME || process.env.FUNCTION_ID;
|
|
1465
|
+
if (functionName) {
|
|
1466
|
+
restoreTarget = `https://${functionName}.fn-dev.sinch.com/webhook/conversation`;
|
|
1467
|
+
console.log(`\u{1F527} Derived restore target from env: ${restoreTarget}`);
|
|
1468
|
+
} else {
|
|
1469
|
+
console.log("\u26A0\uFE0F Cannot restore webhook \u2014 no FUNCTION_NAME or FUNCTION_ID available");
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
await sinchClient.conversation.webhooks.update({
|
|
1474
|
+
webhook_id: config.conversationWebhookId,
|
|
1475
|
+
webhookUpdateRequestBody: {
|
|
1476
|
+
target: restoreTarget
|
|
1477
|
+
},
|
|
1478
|
+
update_mask: ["target"]
|
|
1479
|
+
});
|
|
1480
|
+
console.log(`\u{1F504} Restored webhook target to: ${restoreTarget}`);
|
|
1294
1481
|
config.conversationWebhookId = void 0;
|
|
1482
|
+
config.originalTarget = void 0;
|
|
1483
|
+
delete process.env.CONVERSATION_WEBHOOK_SECRET;
|
|
1484
|
+
resetSinchClients();
|
|
1295
1485
|
} catch (error) {
|
|
1486
|
+
console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error instanceof Error ? error.message : error);
|
|
1296
1487
|
}
|
|
1297
1488
|
}
|
|
1298
1489
|
async function configureElevenLabs() {
|
|
@@ -1307,7 +1498,7 @@ async function configureElevenLabs() {
|
|
|
1307
1498
|
console.log("\u{1F916} ElevenLabs auto-configuration enabled");
|
|
1308
1499
|
console.log(` Agent ID: ${agentId}`);
|
|
1309
1500
|
} catch (error) {
|
|
1310
|
-
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);
|
|
1311
1502
|
}
|
|
1312
1503
|
}
|
|
1313
1504
|
|
|
@@ -1344,14 +1535,14 @@ var TunnelClient = class {
|
|
|
1344
1535
|
timestampPart = ENCODING[t % 32] + timestampPart;
|
|
1345
1536
|
t = Math.floor(t / 32);
|
|
1346
1537
|
}
|
|
1347
|
-
const
|
|
1348
|
-
crypto.getRandomValues(
|
|
1538
|
+
const randomBytes2 = new Uint8Array(10);
|
|
1539
|
+
crypto.getRandomValues(randomBytes2);
|
|
1349
1540
|
let randomPart = "";
|
|
1350
1541
|
for (let i = 0; i < 10; i++) {
|
|
1351
|
-
const byte =
|
|
1542
|
+
const byte = randomBytes2[i];
|
|
1352
1543
|
randomPart += ENCODING[byte >> 3];
|
|
1353
1544
|
if (randomPart.length < 16) {
|
|
1354
|
-
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ?
|
|
1545
|
+
randomPart += ENCODING[(byte & 7) << 2 | (i + 1 < 10 ? randomBytes2[i + 1] >> 6 : 0)];
|
|
1355
1546
|
}
|
|
1356
1547
|
}
|
|
1357
1548
|
randomPart = randomPart.substring(0, 16);
|
|
@@ -1424,6 +1615,15 @@ var TunnelClient = class {
|
|
|
1424
1615
|
break;
|
|
1425
1616
|
}
|
|
1426
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
|
+
}
|
|
1427
1627
|
handleWelcomeMessage(message) {
|
|
1428
1628
|
this.tunnelId = message.tunnelId || null;
|
|
1429
1629
|
this.tunnelUrl = message.publicUrl || null;
|
|
@@ -1434,7 +1634,11 @@ var TunnelClient = class {
|
|
|
1434
1634
|
}
|
|
1435
1635
|
}
|
|
1436
1636
|
async handleRequest(message) {
|
|
1637
|
+
const verbose = process.env.VERBOSE === "true";
|
|
1437
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
|
+
}
|
|
1438
1642
|
try {
|
|
1439
1643
|
const localUrl = `http://localhost:${this.localPort}${message.path}${message.query || ""}`;
|
|
1440
1644
|
const axiosConfig = {
|
|
@@ -1467,9 +1671,15 @@ var TunnelClient = class {
|
|
|
1467
1671
|
headers,
|
|
1468
1672
|
body
|
|
1469
1673
|
};
|
|
1674
|
+
if (verbose) {
|
|
1675
|
+
console.log(` \u2192 Response ${response.status}: ${body.substring(0, 2e3)}`);
|
|
1676
|
+
}
|
|
1470
1677
|
this.ws?.send(JSON.stringify(responseMessage));
|
|
1471
1678
|
} catch (error) {
|
|
1472
|
-
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
|
+
}
|
|
1473
1683
|
const errorResponse = {
|
|
1474
1684
|
type: "response",
|
|
1475
1685
|
id: message.id,
|
|
@@ -1513,7 +1723,7 @@ var TunnelClient = class {
|
|
|
1513
1723
|
await this.configureVoiceWebhooks();
|
|
1514
1724
|
}
|
|
1515
1725
|
if (autoConfigConversation && process.env.CONVERSATION_APP_ID) {
|
|
1516
|
-
await configureConversationWebhooks(this.
|
|
1726
|
+
await configureConversationWebhooks(this.buildTunnelUrl("/webhook/conversation"), this.webhookConfig);
|
|
1517
1727
|
}
|
|
1518
1728
|
if (process.env.ELEVENLABS_AUTO_CONFIGURE === "true") {
|
|
1519
1729
|
await configureElevenLabs();
|
|
@@ -1540,7 +1750,7 @@ var TunnelClient = class {
|
|
|
1540
1750
|
updateUrl,
|
|
1541
1751
|
{
|
|
1542
1752
|
url: {
|
|
1543
|
-
primary: this.
|
|
1753
|
+
primary: this.buildTunnelUrl(),
|
|
1544
1754
|
fallback: null
|
|
1545
1755
|
}
|
|
1546
1756
|
},
|
|
@@ -1602,7 +1812,8 @@ var TunnelClient = class {
|
|
|
1602
1812
|
this.isConnected = false;
|
|
1603
1813
|
}
|
|
1604
1814
|
getTunnelUrl() {
|
|
1605
|
-
|
|
1815
|
+
if (!this.tunnelUrl || !this.tunnelId) return null;
|
|
1816
|
+
return this.buildTunnelUrl();
|
|
1606
1817
|
}
|
|
1607
1818
|
getIsConnected() {
|
|
1608
1819
|
return this.isConnected;
|
|
@@ -1627,7 +1838,7 @@ function loadRuntimeConfig() {
|
|
|
1627
1838
|
};
|
|
1628
1839
|
}
|
|
1629
1840
|
var storage = createStorageClient();
|
|
1630
|
-
var databasePath = path6.join(process.cwd(), "data", "app.db");
|
|
1841
|
+
var databasePath = path6.join(process.cwd(), ".sinch", "data", "app.db");
|
|
1631
1842
|
function buildLocalContext(req, runtimeConfig) {
|
|
1632
1843
|
const baseContext = buildBaseContext(req);
|
|
1633
1844
|
const cache = createCacheClient();
|
|
@@ -1725,19 +1936,19 @@ function displayApplicationCredentials() {
|
|
|
1725
1936
|
console.log(' Tip: Add VOICE_APPLICATION_KEY to .env and run "sinch auth login"');
|
|
1726
1937
|
}
|
|
1727
1938
|
}
|
|
1728
|
-
async function displayDetectedFunctions() {
|
|
1939
|
+
async function displayDetectedFunctions(port) {
|
|
1729
1940
|
try {
|
|
1730
1941
|
const functionPath = findFunctionPath3();
|
|
1731
1942
|
if (!fs5.existsSync(functionPath)) return;
|
|
1732
1943
|
const functionUrl = pathToFileURL2(functionPath).href;
|
|
1733
1944
|
const module = await import(functionUrl);
|
|
1734
1945
|
const userFunction = module.default || module;
|
|
1735
|
-
const functions = Object.keys(userFunction);
|
|
1946
|
+
const functions = Object.keys(userFunction).filter((k) => typeof userFunction[k] === "function");
|
|
1736
1947
|
if (functions.length > 0) {
|
|
1737
|
-
console.log("\nDetected functions
|
|
1948
|
+
console.log("\nDetected functions:");
|
|
1738
1949
|
for (const fn of functions) {
|
|
1739
1950
|
const type = isVoiceCallback(fn) ? "voice" : "custom";
|
|
1740
|
-
console.log(`
|
|
1951
|
+
console.log(` ${fn} (${type}): POST http://localhost:${port}/${fn}`);
|
|
1741
1952
|
}
|
|
1742
1953
|
}
|
|
1743
1954
|
} catch {
|
|
@@ -1755,20 +1966,45 @@ async function main() {
|
|
|
1755
1966
|
} catch {
|
|
1756
1967
|
}
|
|
1757
1968
|
await secretsLoader.loadFromKeychain();
|
|
1758
|
-
fs5.mkdirSync(path6.join(process.cwd(), "storage"), { recursive: true });
|
|
1759
|
-
fs5.mkdirSync(path6.join(process.cwd(), "data"), { recursive: true });
|
|
1969
|
+
fs5.mkdirSync(path6.join(process.cwd(), ".sinch", "storage"), { recursive: true });
|
|
1970
|
+
fs5.mkdirSync(path6.join(process.cwd(), ".sinch", "data"), { recursive: true });
|
|
1760
1971
|
const config = loadRuntimeConfig();
|
|
1761
1972
|
const staticDir = process.env.STATIC_DIR;
|
|
1762
1973
|
const landingPageEnabled = process.env.LANDING_PAGE_ENABLED !== "false";
|
|
1763
1974
|
const app = createApp({ staticDir, landingPageEnabled });
|
|
1975
|
+
const authKey = process.env.PROJECT_ID_API_KEY;
|
|
1976
|
+
const authSecret = process.env.PROJECT_ID_API_SECRET;
|
|
1977
|
+
let userAuthConfig;
|
|
1978
|
+
const loadUserFunction = async () => {
|
|
1979
|
+
const functionPath = findFunctionPath3();
|
|
1980
|
+
const functionUrl = pathToFileURL2(functionPath).href;
|
|
1981
|
+
const module = await import(functionUrl);
|
|
1982
|
+
if (userAuthConfig === void 0) {
|
|
1983
|
+
userAuthConfig = module.auth || module.default?.auth;
|
|
1984
|
+
if (userAuthConfig && verbose) {
|
|
1985
|
+
console.log(`[AUTH] Auth config loaded: ${JSON.stringify(userAuthConfig)}`);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
return module.default || module;
|
|
1989
|
+
};
|
|
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
|
+
}
|
|
1764
2002
|
setupRequestHandler(app, {
|
|
1765
2003
|
landingPageEnabled,
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
return module.default || module;
|
|
1771
|
-
},
|
|
2004
|
+
authConfig: userAuthConfig,
|
|
2005
|
+
authKey,
|
|
2006
|
+
authSecret,
|
|
2007
|
+
loadUserFunction,
|
|
1772
2008
|
buildContext: (req) => buildLocalContext(req, config),
|
|
1773
2009
|
logger: console.log,
|
|
1774
2010
|
onRequestStart: ({ req }) => {
|
|
@@ -1791,17 +2027,22 @@ async function main() {
|
|
|
1791
2027
|
});
|
|
1792
2028
|
});
|
|
1793
2029
|
displayStartupInfo(config, verbose, port);
|
|
1794
|
-
|
|
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 () => {
|
|
1795
2038
|
console.log(`Function server running on http://localhost:${port}`);
|
|
1796
|
-
|
|
1797
|
-
console.log(` ICE: POST http://localhost:${port}/ice`);
|
|
1798
|
-
console.log(` PIE: POST http://localhost:${port}/pie`);
|
|
1799
|
-
console.log(` ACE: POST http://localhost:${port}/ace`);
|
|
1800
|
-
console.log(` DICE: POST http://localhost:${port}/dice`);
|
|
1801
|
-
await displayDetectedFunctions();
|
|
2039
|
+
await displayDetectedFunctions(port);
|
|
1802
2040
|
if (!verbose) {
|
|
1803
2041
|
console.log("\nTip: Set VERBOSE=true or use --verbose for detailed output");
|
|
1804
2042
|
}
|
|
2043
|
+
if (setupResult?.wsHandlers.size) {
|
|
2044
|
+
installWebSocketHandlers(server, setupResult.wsHandlers, console.log);
|
|
2045
|
+
}
|
|
1805
2046
|
if (process.env.SINCH_TUNNEL === "true") {
|
|
1806
2047
|
console.log("\nStarting tunnel...");
|
|
1807
2048
|
const tunnelClient = new TunnelClient(port);
|