@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/bin/sinch-runtime.js +104 -29
- package/dist/bin/sinch-runtime.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +81 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
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
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2581
|
-
|
|
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() {
|