@sinch/functions-runtime 0.4.0 → 0.4.1

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.
@@ -160,6 +160,31 @@ 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
+
163
188
  // ../runtime-shared/dist/host/app.js
164
189
  import { createRequire as createRequire2 } from "module";
165
190
  import { pathToFileURL } from "url";
@@ -462,7 +487,7 @@ function setupRequestHandler(app, options = {}) {
462
487
  const functionUrl = pathToFileURL(functionPath).href;
463
488
  const module = await import(functionUrl);
464
489
  return module.default || module;
465
- }, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, onRequestStart = () => {
490
+ }, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, authConfig, authKey, authSecret, onRequestStart = () => {
466
491
  }, onRequestEnd = () => {
467
492
  } } = options;
468
493
  app.use("/{*splat}", async (req, res) => {
@@ -490,6 +515,17 @@ function setupRequestHandler(app, options = {}) {
490
515
  try {
491
516
  const functionName = extractFunctionName(req.originalUrl, req.body);
492
517
  logger(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${req.method} ${req.path} -> ${functionName}`);
518
+ if (authConfig && authKey && authSecret) {
519
+ const needsAuth = authConfig === "*" || Array.isArray(authConfig) && authConfig.includes(functionName);
520
+ if (needsAuth) {
521
+ const isValid = validateBasicAuth(req.headers.authorization, authKey, authSecret);
522
+ if (!isValid) {
523
+ logger(`[AUTH] Rejected unauthorized request to ${functionName}`);
524
+ res.status(401).set("WWW-Authenticate", 'Basic realm="sinch-function"').json({ error: "Unauthorized" });
525
+ return;
526
+ }
527
+ }
528
+ }
493
529
  onRequestStart({ functionName, req });
494
530
  const context = buildContext(req);
495
531
  const userFunction = await Promise.resolve(loadUserFunction());
@@ -780,7 +816,7 @@ import * as path4 from "path";
780
816
  var LocalStorage = class {
781
817
  baseDir;
782
818
  constructor(baseDir) {
783
- this.baseDir = baseDir ?? path4.join(process.cwd(), "storage");
819
+ this.baseDir = baseDir ?? path4.join(process.cwd(), ".sinch", "storage");
784
820
  }
785
821
  resolvePath(key) {
786
822
  const sanitized = key.replace(/^\/+/, "").replace(/\.\./g, "_");
@@ -1232,6 +1268,13 @@ import axios from "axios";
1232
1268
 
1233
1269
  // src/tunnel/webhook-config.ts
1234
1270
  import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
1271
+ var SINCH_FN_URL_PATTERN = /\.fn(-\w+)?\.sinch\.com/;
1272
+ function isOurWebhook(target) {
1273
+ return !!target && SINCH_FN_URL_PATTERN.test(target);
1274
+ }
1275
+ function isTunnelUrl(target) {
1276
+ return !!target && target.includes("tunnel.fn");
1277
+ }
1235
1278
  async function configureConversationWebhooks(tunnelUrl, config) {
1236
1279
  try {
1237
1280
  const conversationAppId = process.env.CONVERSATION_APP_ID;
@@ -1253,24 +1296,23 @@ async function configureConversationWebhooks(tunnelUrl, config) {
1253
1296
  app_id: conversationAppId
1254
1297
  });
1255
1298
  const existingWebhooks = webhooksResult.webhooks || [];
1256
- const tunnelWebhooks = existingWebhooks.filter((w) => w.target?.includes("/api/ingress/"));
1257
- for (const staleWebhook of tunnelWebhooks) {
1258
- try {
1259
- await sinchClient.conversation.webhooks.delete({ webhook_id: staleWebhook.id });
1260
- console.log(`\u{1F9F9} Cleaned up stale tunnel webhook: ${staleWebhook.id}`);
1261
- } catch (err) {
1262
- }
1299
+ const deployedWebhook = existingWebhooks.find(
1300
+ (w) => isOurWebhook(w.target)
1301
+ );
1302
+ if (!deployedWebhook || !deployedWebhook.id) {
1303
+ console.log("\u26A0\uFE0F No deployed webhook found \u2014 deploy first");
1304
+ return;
1263
1305
  }
1264
- const createResult = await sinchClient.conversation.webhooks.create({
1265
- webhookCreateRequestBody: {
1266
- app_id: conversationAppId,
1267
- target: webhookUrl,
1268
- target_type: "HTTP",
1269
- triggers: ["MESSAGE_INBOUND"]
1270
- }
1306
+ config.conversationWebhookId = deployedWebhook.id;
1307
+ config.originalTarget = deployedWebhook.target;
1308
+ await sinchClient.conversation.webhooks.update({
1309
+ webhook_id: deployedWebhook.id,
1310
+ webhookUpdateRequestBody: {
1311
+ target: webhookUrl
1312
+ },
1313
+ update_mask: ["target"]
1271
1314
  });
1272
- config.conversationWebhookId = createResult.id;
1273
- console.log(`\u2705 Created Conversation webhook: ${webhookUrl}`);
1315
+ console.log(`\u2705 Updated Conversation webhook to tunnel: ${webhookUrl}`);
1274
1316
  console.log("\u{1F4AC} Send a message to your Conversation app to test!");
1275
1317
  } catch (error) {
1276
1318
  console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error.message);
@@ -1289,10 +1331,29 @@ async function cleanupConversationWebhook(config) {
1289
1331
  keyId,
1290
1332
  keySecret
1291
1333
  });
1292
- await sinchClient.conversation.webhooks.delete({ webhook_id: config.conversationWebhookId });
1293
- console.log("\u{1F9F9} Cleaned up tunnel webhook");
1334
+ let restoreTarget = config.originalTarget;
1335
+ if (!restoreTarget || isTunnelUrl(restoreTarget)) {
1336
+ const functionName = process.env.FUNCTION_NAME || process.env.FUNCTION_ID;
1337
+ if (functionName) {
1338
+ restoreTarget = `https://${functionName}.fn-dev.sinch.com/webhook/conversation`;
1339
+ console.log(`\u{1F527} Derived restore target from env: ${restoreTarget}`);
1340
+ } else {
1341
+ console.log("\u26A0\uFE0F Cannot restore webhook \u2014 no FUNCTION_NAME or FUNCTION_ID available");
1342
+ return;
1343
+ }
1344
+ }
1345
+ await sinchClient.conversation.webhooks.update({
1346
+ webhook_id: config.conversationWebhookId,
1347
+ webhookUpdateRequestBody: {
1348
+ target: restoreTarget
1349
+ },
1350
+ update_mask: ["target"]
1351
+ });
1352
+ console.log(`\u{1F504} Restored webhook target to: ${restoreTarget}`);
1294
1353
  config.conversationWebhookId = void 0;
1354
+ config.originalTarget = void 0;
1295
1355
  } catch (error) {
1356
+ console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error.message);
1296
1357
  }
1297
1358
  }
1298
1359
  async function configureElevenLabs() {
@@ -1627,7 +1688,7 @@ function loadRuntimeConfig() {
1627
1688
  };
1628
1689
  }
1629
1690
  var storage = createStorageClient();
1630
- var databasePath = path6.join(process.cwd(), "data", "app.db");
1691
+ var databasePath = path6.join(process.cwd(), ".sinch", "data", "app.db");
1631
1692
  function buildLocalContext(req, runtimeConfig) {
1632
1693
  const baseContext = buildBaseContext(req);
1633
1694
  const cache = createCacheClient();
@@ -1755,20 +1816,34 @@ async function main() {
1755
1816
  } catch {
1756
1817
  }
1757
1818
  await secretsLoader.loadFromKeychain();
1758
- fs5.mkdirSync(path6.join(process.cwd(), "storage"), { recursive: true });
1759
- fs5.mkdirSync(path6.join(process.cwd(), "data"), { recursive: true });
1819
+ fs5.mkdirSync(path6.join(process.cwd(), ".sinch", "storage"), { recursive: true });
1820
+ fs5.mkdirSync(path6.join(process.cwd(), ".sinch", "data"), { recursive: true });
1760
1821
  const config = loadRuntimeConfig();
1761
1822
  const staticDir = process.env.STATIC_DIR;
1762
1823
  const landingPageEnabled = process.env.LANDING_PAGE_ENABLED !== "false";
1763
1824
  const app = createApp({ staticDir, landingPageEnabled });
1825
+ const authKey = process.env.PROJECT_ID_API_KEY;
1826
+ const authSecret = process.env.PROJECT_ID_API_SECRET;
1827
+ let userAuthConfig;
1828
+ const loadUserFunction = async () => {
1829
+ const functionPath = findFunctionPath3();
1830
+ const functionUrl = pathToFileURL2(functionPath).href;
1831
+ const module = await import(functionUrl);
1832
+ if (userAuthConfig === void 0) {
1833
+ userAuthConfig = module.auth || module.default?.auth;
1834
+ if (userAuthConfig && verbose) {
1835
+ console.log(`[AUTH] Auth config loaded: ${JSON.stringify(userAuthConfig)}`);
1836
+ }
1837
+ }
1838
+ return module.default || module;
1839
+ };
1840
+ await loadUserFunction();
1764
1841
  setupRequestHandler(app, {
1765
1842
  landingPageEnabled,
1766
- loadUserFunction: async () => {
1767
- const functionPath = findFunctionPath3();
1768
- const functionUrl = pathToFileURL2(functionPath).href;
1769
- const module = await import(functionUrl);
1770
- return module.default || module;
1771
- },
1843
+ authConfig: userAuthConfig,
1844
+ authKey,
1845
+ authSecret,
1846
+ loadUserFunction,
1772
1847
  buildContext: (req) => buildLocalContext(req, config),
1773
1848
  logger: console.log,
1774
1849
  onRequestStart: ({ req }) => {