@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.
package/dist/index.d.ts CHANGED
@@ -1292,6 +1292,12 @@ export declare function createLenientJsonParser(options?: JsonParsingOptions): (
1292
1292
  * @internal
1293
1293
  */
1294
1294
  export declare function setupJsonParsing(app: Express, options?: JsonParsingOptions): Express;
1295
+ /**
1296
+ * Declarative auth configuration exported by user functions.
1297
+ * - string[] — protect specific handler names
1298
+ * - '*' — protect all handlers
1299
+ */
1300
+ export type AuthConfig = string[] | "*";
1295
1301
  /**
1296
1302
  * Get the landing page HTML content
1297
1303
  * Exported so production runtime can also use it
@@ -1329,6 +1335,12 @@ export interface RequestHandlerOptions {
1329
1335
  logger?: (...args: unknown[]) => void;
1330
1336
  /** Enable landing page for browser requests at root (default: true) */
1331
1337
  landingPageEnabled?: boolean;
1338
+ /** Declarative auth config from user module (string[] or '*') */
1339
+ authConfig?: AuthConfig;
1340
+ /** API key for Basic Auth validation */
1341
+ authKey?: string;
1342
+ /** API secret for Basic Auth validation */
1343
+ authSecret?: string;
1332
1344
  /** Called when request starts */
1333
1345
  onRequestStart?: (data: {
1334
1346
  functionName: string;
package/dist/index.js CHANGED
@@ -934,6 +934,31 @@ 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
+
937
962
  // ../runtime-shared/dist/host/app.js
938
963
  import { createRequire as createRequire2 } from "module";
939
964
  import { pathToFileURL } from "url";
@@ -1236,7 +1261,7 @@ function setupRequestHandler(app, options = {}) {
1236
1261
  const functionUrl = pathToFileURL(functionPath).href;
1237
1262
  const module = await import(functionUrl);
1238
1263
  return module.default || module;
1239
- }, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, onRequestStart = () => {
1264
+ }, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, authConfig, authKey, authSecret, onRequestStart = () => {
1240
1265
  }, onRequestEnd = () => {
1241
1266
  } } = options;
1242
1267
  app.use("/{*splat}", async (req, res) => {
@@ -1264,6 +1289,17 @@ function setupRequestHandler(app, options = {}) {
1264
1289
  try {
1265
1290
  const functionName = extractFunctionName(req.originalUrl, req.body);
1266
1291
  logger(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${req.method} ${req.path} -> ${functionName}`);
1292
+ if (authConfig && authKey && authSecret) {
1293
+ const needsAuth = authConfig === "*" || Array.isArray(authConfig) && authConfig.includes(functionName);
1294
+ if (needsAuth) {
1295
+ const isValid = validateBasicAuth(req.headers.authorization, authKey, authSecret);
1296
+ if (!isValid) {
1297
+ logger(`[AUTH] Rejected unauthorized request to ${functionName}`);
1298
+ res.status(401).set("WWW-Authenticate", 'Basic realm="sinch-function"').json({ error: "Unauthorized" });
1299
+ return;
1300
+ }
1301
+ }
1302
+ }
1267
1303
  onRequestStart({ functionName, req });
1268
1304
  const context = buildContext(req);
1269
1305
  const userFunction = await Promise.resolve(loadUserFunction());
@@ -2457,7 +2493,7 @@ import * as path4 from "path";
2457
2493
  var LocalStorage = class {
2458
2494
  baseDir;
2459
2495
  constructor(baseDir) {
2460
- this.baseDir = baseDir ?? path4.join(process.cwd(), "storage");
2496
+ this.baseDir = baseDir ?? path4.join(process.cwd(), ".sinch", "storage");
2461
2497
  }
2462
2498
  resolvePath(key) {
2463
2499
  const sanitized = key.replace(/^\/+/, "").replace(/\.\./g, "_");
@@ -2520,6 +2556,13 @@ import axios from "axios";
2520
2556
 
2521
2557
  // src/tunnel/webhook-config.ts
2522
2558
  import { SinchClient as SinchClient2 } from "@sinch/sdk-core";
2559
+ var SINCH_FN_URL_PATTERN = /\.fn(-\w+)?\.sinch\.com/;
2560
+ function isOurWebhook(target) {
2561
+ return !!target && SINCH_FN_URL_PATTERN.test(target);
2562
+ }
2563
+ function isTunnelUrl(target) {
2564
+ return !!target && target.includes("tunnel.fn");
2565
+ }
2523
2566
  async function configureConversationWebhooks(tunnelUrl, config) {
2524
2567
  try {
2525
2568
  const conversationAppId = process.env.CONVERSATION_APP_ID;
@@ -2541,24 +2584,23 @@ async function configureConversationWebhooks(tunnelUrl, config) {
2541
2584
  app_id: conversationAppId
2542
2585
  });
2543
2586
  const existingWebhooks = webhooksResult.webhooks || [];
2544
- const tunnelWebhooks = existingWebhooks.filter((w) => w.target?.includes("/api/ingress/"));
2545
- for (const staleWebhook of tunnelWebhooks) {
2546
- try {
2547
- await sinchClient.conversation.webhooks.delete({ webhook_id: staleWebhook.id });
2548
- console.log(`\u{1F9F9} Cleaned up stale tunnel webhook: ${staleWebhook.id}`);
2549
- } catch (err) {
2550
- }
2587
+ const deployedWebhook = existingWebhooks.find(
2588
+ (w) => isOurWebhook(w.target)
2589
+ );
2590
+ if (!deployedWebhook || !deployedWebhook.id) {
2591
+ console.log("\u26A0\uFE0F No deployed webhook found \u2014 deploy first");
2592
+ return;
2551
2593
  }
2552
- const createResult = await sinchClient.conversation.webhooks.create({
2553
- webhookCreateRequestBody: {
2554
- app_id: conversationAppId,
2555
- target: webhookUrl,
2556
- target_type: "HTTP",
2557
- triggers: ["MESSAGE_INBOUND"]
2558
- }
2594
+ config.conversationWebhookId = deployedWebhook.id;
2595
+ config.originalTarget = deployedWebhook.target;
2596
+ await sinchClient.conversation.webhooks.update({
2597
+ webhook_id: deployedWebhook.id,
2598
+ webhookUpdateRequestBody: {
2599
+ target: webhookUrl
2600
+ },
2601
+ update_mask: ["target"]
2559
2602
  });
2560
- config.conversationWebhookId = createResult.id;
2561
- console.log(`\u2705 Created Conversation webhook: ${webhookUrl}`);
2603
+ console.log(`\u2705 Updated Conversation webhook to tunnel: ${webhookUrl}`);
2562
2604
  console.log("\u{1F4AC} Send a message to your Conversation app to test!");
2563
2605
  } catch (error) {
2564
2606
  console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error.message);
@@ -2577,10 +2619,29 @@ async function cleanupConversationWebhook(config) {
2577
2619
  keyId,
2578
2620
  keySecret
2579
2621
  });
2580
- await sinchClient.conversation.webhooks.delete({ webhook_id: config.conversationWebhookId });
2581
- console.log("\u{1F9F9} Cleaned up tunnel webhook");
2622
+ let restoreTarget = config.originalTarget;
2623
+ if (!restoreTarget || isTunnelUrl(restoreTarget)) {
2624
+ const functionName = process.env.FUNCTION_NAME || process.env.FUNCTION_ID;
2625
+ if (functionName) {
2626
+ restoreTarget = `https://${functionName}.fn-dev.sinch.com/webhook/conversation`;
2627
+ console.log(`\u{1F527} Derived restore target from env: ${restoreTarget}`);
2628
+ } else {
2629
+ console.log("\u26A0\uFE0F Cannot restore webhook \u2014 no FUNCTION_NAME or FUNCTION_ID available");
2630
+ return;
2631
+ }
2632
+ }
2633
+ await sinchClient.conversation.webhooks.update({
2634
+ webhook_id: config.conversationWebhookId,
2635
+ webhookUpdateRequestBody: {
2636
+ target: restoreTarget
2637
+ },
2638
+ update_mask: ["target"]
2639
+ });
2640
+ console.log(`\u{1F504} Restored webhook target to: ${restoreTarget}`);
2582
2641
  config.conversationWebhookId = void 0;
2642
+ config.originalTarget = void 0;
2583
2643
  } catch (error) {
2644
+ console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error.message);
2584
2645
  }
2585
2646
  }
2586
2647
  async function configureElevenLabs() {