@sinch/functions-runtime 0.3.9 → 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 +446 -73
- package/dist/bin/sinch-runtime.js.map +1 -1
- package/dist/index.d.ts +100 -2
- package/dist/index.js +413 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -4
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin/sinch-runtime.ts
|
|
4
|
-
import
|
|
4
|
+
import path6 from "path";
|
|
5
5
|
import { createRequire as createRequire3 } from "module";
|
|
6
6
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
7
|
-
import
|
|
7
|
+
import fs5 from "fs";
|
|
8
8
|
|
|
9
9
|
// ../runtime-shared/dist/ai/connect-agent.js
|
|
10
10
|
var AgentProvider;
|
|
@@ -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";
|
|
@@ -247,11 +272,11 @@ function isVoiceCallback(functionName) {
|
|
|
247
272
|
function isNotificationEvent(functionName) {
|
|
248
273
|
return NOTIFICATION_EVENTS.includes(functionName);
|
|
249
274
|
}
|
|
250
|
-
function extractFunctionName(
|
|
275
|
+
function extractFunctionName(path7, body) {
|
|
251
276
|
if (body?.event && isVoiceCallback(body.event)) {
|
|
252
277
|
return body.event;
|
|
253
278
|
}
|
|
254
|
-
const pathname =
|
|
279
|
+
const pathname = path7.split("?")[0];
|
|
255
280
|
const segments = pathname.split("/").filter((s) => s && s !== "*");
|
|
256
281
|
if (segments.length === 1 && isVoiceCallback(segments[0])) {
|
|
257
282
|
return segments[0];
|
|
@@ -268,10 +293,10 @@ function generateRequestId() {
|
|
|
268
293
|
return `req_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
269
294
|
}
|
|
270
295
|
function findFunctionPath() {
|
|
271
|
-
const
|
|
296
|
+
const fs6 = requireCjs2("fs");
|
|
272
297
|
const distPath = nodePath.join(process.cwd(), "dist", "function.js");
|
|
273
298
|
const rootPath = nodePath.join(process.cwd(), "function.js");
|
|
274
|
-
if (
|
|
299
|
+
if (fs6.existsSync(distPath)) {
|
|
275
300
|
return distPath;
|
|
276
301
|
}
|
|
277
302
|
return rootPath;
|
|
@@ -349,6 +374,15 @@ var noOpCache = {
|
|
|
349
374
|
keys: async () => [],
|
|
350
375
|
getMany: async () => ({})
|
|
351
376
|
};
|
|
377
|
+
var noOpStorage = {
|
|
378
|
+
write: async () => {
|
|
379
|
+
},
|
|
380
|
+
read: async () => Buffer.alloc(0),
|
|
381
|
+
list: async () => [],
|
|
382
|
+
exists: async () => false,
|
|
383
|
+
delete: async () => {
|
|
384
|
+
}
|
|
385
|
+
};
|
|
352
386
|
function buildBaseContext(req, config = {}) {
|
|
353
387
|
return {
|
|
354
388
|
requestId: req.headers["x-request-id"] || generateRequestId(),
|
|
@@ -361,6 +395,8 @@ function buildBaseContext(req, config = {}) {
|
|
|
361
395
|
variables: config.variables
|
|
362
396
|
},
|
|
363
397
|
cache: noOpCache,
|
|
398
|
+
storage: noOpStorage,
|
|
399
|
+
database: "",
|
|
364
400
|
assets: (filename) => {
|
|
365
401
|
const filePath = nodePath.join(process.cwd(), "assets", filename);
|
|
366
402
|
return nodeFs.promises.readFile(filePath, "utf-8");
|
|
@@ -451,7 +487,7 @@ function setupRequestHandler(app, options = {}) {
|
|
|
451
487
|
const functionUrl = pathToFileURL(functionPath).href;
|
|
452
488
|
const module = await import(functionUrl);
|
|
453
489
|
return module.default || module;
|
|
454
|
-
}, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, onRequestStart = () => {
|
|
490
|
+
}, buildContext = buildBaseContext, logger = console.log, landingPageEnabled = true, authConfig, authKey, authSecret, onRequestStart = () => {
|
|
455
491
|
}, onRequestEnd = () => {
|
|
456
492
|
} } = options;
|
|
457
493
|
app.use("/{*splat}", async (req, res) => {
|
|
@@ -479,6 +515,17 @@ function setupRequestHandler(app, options = {}) {
|
|
|
479
515
|
try {
|
|
480
516
|
const functionName = extractFunctionName(req.originalUrl, req.body);
|
|
481
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
|
+
}
|
|
482
529
|
onRequestStart({ functionName, req });
|
|
483
530
|
const context = buildContext(req);
|
|
484
531
|
const userFunction = await Promise.resolve(loadUserFunction());
|
|
@@ -763,10 +810,306 @@ function createCacheClient(_projectId, _functionName) {
|
|
|
763
810
|
return new LocalCache();
|
|
764
811
|
}
|
|
765
812
|
|
|
813
|
+
// src/storage/local.ts
|
|
814
|
+
import * as fs3 from "fs/promises";
|
|
815
|
+
import * as path4 from "path";
|
|
816
|
+
var LocalStorage = class {
|
|
817
|
+
baseDir;
|
|
818
|
+
constructor(baseDir) {
|
|
819
|
+
this.baseDir = baseDir ?? path4.join(process.cwd(), ".sinch", "storage");
|
|
820
|
+
}
|
|
821
|
+
resolvePath(key) {
|
|
822
|
+
const sanitized = key.replace(/^\/+/, "").replace(/\.\./g, "_");
|
|
823
|
+
return path4.join(this.baseDir, sanitized);
|
|
824
|
+
}
|
|
825
|
+
async write(key, data) {
|
|
826
|
+
const filePath = this.resolvePath(key);
|
|
827
|
+
await fs3.mkdir(path4.dirname(filePath), { recursive: true });
|
|
828
|
+
await fs3.writeFile(filePath, data);
|
|
829
|
+
}
|
|
830
|
+
async read(key) {
|
|
831
|
+
const filePath = this.resolvePath(key);
|
|
832
|
+
return fs3.readFile(filePath);
|
|
833
|
+
}
|
|
834
|
+
async list(prefix) {
|
|
835
|
+
const results = [];
|
|
836
|
+
await this.walkDir(this.baseDir, "", results);
|
|
837
|
+
if (prefix) {
|
|
838
|
+
return results.filter((f) => f.startsWith(prefix));
|
|
839
|
+
}
|
|
840
|
+
return results;
|
|
841
|
+
}
|
|
842
|
+
async exists(key) {
|
|
843
|
+
const filePath = this.resolvePath(key);
|
|
844
|
+
try {
|
|
845
|
+
await fs3.access(filePath);
|
|
846
|
+
return true;
|
|
847
|
+
} catch {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async delete(key) {
|
|
852
|
+
const filePath = this.resolvePath(key);
|
|
853
|
+
await fs3.rm(filePath, { force: true });
|
|
854
|
+
}
|
|
855
|
+
async walkDir(dir, relative, results) {
|
|
856
|
+
let entries;
|
|
857
|
+
try {
|
|
858
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
859
|
+
} catch {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
for (const entry of entries) {
|
|
863
|
+
const rel = relative ? `${relative}/${entry.name}` : entry.name;
|
|
864
|
+
if (entry.isDirectory()) {
|
|
865
|
+
await this.walkDir(path4.join(dir, entry.name), rel, results);
|
|
866
|
+
} else {
|
|
867
|
+
results.push(rel);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
function createStorageClient(baseDir) {
|
|
873
|
+
return new LocalStorage(baseDir);
|
|
874
|
+
}
|
|
875
|
+
|
|
766
876
|
// src/secrets/index.ts
|
|
767
|
-
import
|
|
768
|
-
import
|
|
877
|
+
import fs4 from "fs";
|
|
878
|
+
import path5 from "path";
|
|
769
879
|
import os from "os";
|
|
880
|
+
|
|
881
|
+
// src/secrets/keychain.ts
|
|
882
|
+
import { execFile, spawn } from "child_process";
|
|
883
|
+
import { promisify } from "util";
|
|
884
|
+
var execFileAsync = promisify(execFile);
|
|
885
|
+
function b64(value) {
|
|
886
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
887
|
+
}
|
|
888
|
+
function psParam(name, value) {
|
|
889
|
+
return `$${name} = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${b64(value)}'));`;
|
|
890
|
+
}
|
|
891
|
+
var WIN32_CRED_READ_SCRIPT = `
|
|
892
|
+
Add-Type -TypeDefinition @'
|
|
893
|
+
using System;
|
|
894
|
+
using System.Runtime.InteropServices;
|
|
895
|
+
public class CredManager {
|
|
896
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
897
|
+
private struct CREDENTIAL {
|
|
898
|
+
public int Flags;
|
|
899
|
+
public int Type;
|
|
900
|
+
public IntPtr TargetName;
|
|
901
|
+
public IntPtr Comment;
|
|
902
|
+
public long LastWritten;
|
|
903
|
+
public int CredentialBlobSize;
|
|
904
|
+
public IntPtr CredentialBlob;
|
|
905
|
+
public int Persist;
|
|
906
|
+
public int AttributeCount;
|
|
907
|
+
public IntPtr Attributes;
|
|
908
|
+
public IntPtr TargetAlias;
|
|
909
|
+
public IntPtr UserName;
|
|
910
|
+
}
|
|
911
|
+
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
912
|
+
private static extern bool CredReadW(string target, int type, int flags, out IntPtr cred);
|
|
913
|
+
[DllImport("advapi32.dll")]
|
|
914
|
+
private static extern void CredFree(IntPtr cred);
|
|
915
|
+
public static string Read(string target) {
|
|
916
|
+
IntPtr credPtr;
|
|
917
|
+
if (!CredReadW(target, 1, 0, out credPtr)) return null;
|
|
918
|
+
try {
|
|
919
|
+
CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(credPtr, typeof(CREDENTIAL));
|
|
920
|
+
if (c.CredentialBlobSize > 0 && c.CredentialBlob != IntPtr.Zero)
|
|
921
|
+
return Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
|
|
922
|
+
return "";
|
|
923
|
+
} finally { CredFree(credPtr); }
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
'@
|
|
927
|
+
$r = [CredManager]::Read($target)
|
|
928
|
+
if ($r -ne $null) { [Console]::Write($r) }
|
|
929
|
+
else { exit 1 }
|
|
930
|
+
`;
|
|
931
|
+
var WIN32_CRED_WRITE_SCRIPT = `
|
|
932
|
+
Add-Type -TypeDefinition @'
|
|
933
|
+
using System;
|
|
934
|
+
using System.Runtime.InteropServices;
|
|
935
|
+
using System.Text;
|
|
936
|
+
public class CredWriter {
|
|
937
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
938
|
+
private struct CREDENTIAL {
|
|
939
|
+
public int Flags;
|
|
940
|
+
public int Type;
|
|
941
|
+
public string TargetName;
|
|
942
|
+
public string Comment;
|
|
943
|
+
public long LastWritten;
|
|
944
|
+
public int CredentialBlobSize;
|
|
945
|
+
public IntPtr CredentialBlob;
|
|
946
|
+
public int Persist;
|
|
947
|
+
public int AttributeCount;
|
|
948
|
+
public IntPtr Attributes;
|
|
949
|
+
public string TargetAlias;
|
|
950
|
+
public string UserName;
|
|
951
|
+
}
|
|
952
|
+
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
953
|
+
private static extern bool CredWriteW(ref CREDENTIAL cred, int flags);
|
|
954
|
+
public static bool Write(string target, string password) {
|
|
955
|
+
byte[] blob = Encoding.Unicode.GetBytes(password);
|
|
956
|
+
CREDENTIAL c = new CREDENTIAL();
|
|
957
|
+
c.Type = 1;
|
|
958
|
+
c.TargetName = target;
|
|
959
|
+
c.CredentialBlobSize = blob.Length;
|
|
960
|
+
c.CredentialBlob = Marshal.AllocHGlobal(blob.Length);
|
|
961
|
+
Marshal.Copy(blob, 0, c.CredentialBlob, blob.Length);
|
|
962
|
+
c.Persist = 2;
|
|
963
|
+
try { return CredWriteW(ref c, 0); }
|
|
964
|
+
finally { Marshal.FreeHGlobal(c.CredentialBlob); }
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
'@
|
|
968
|
+
if (-not [CredWriter]::Write($target, $password)) { exit 1 }
|
|
969
|
+
`;
|
|
970
|
+
var WIN32_CRED_DELETE_SCRIPT = `
|
|
971
|
+
Add-Type -TypeDefinition @'
|
|
972
|
+
using System;
|
|
973
|
+
using System.Runtime.InteropServices;
|
|
974
|
+
public class CredDeleter {
|
|
975
|
+
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
976
|
+
private static extern bool CredDeleteW(string target, int type, int flags);
|
|
977
|
+
public static bool Delete(string target) { return CredDeleteW(target, 1, 0); }
|
|
978
|
+
}
|
|
979
|
+
'@
|
|
980
|
+
if (-not [CredDeleter]::Delete($target)) { exit 1 }
|
|
981
|
+
`;
|
|
982
|
+
function winTarget(service, account) {
|
|
983
|
+
return `${service}/${account}`;
|
|
984
|
+
}
|
|
985
|
+
var windowsKeychain = {
|
|
986
|
+
async getPassword(service, account) {
|
|
987
|
+
try {
|
|
988
|
+
const params = psParam("target", winTarget(service, account));
|
|
989
|
+
const { stdout } = await execFileAsync(
|
|
990
|
+
"powershell.exe",
|
|
991
|
+
["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_READ_SCRIPT],
|
|
992
|
+
{ timeout: 15e3, windowsHide: true }
|
|
993
|
+
);
|
|
994
|
+
return stdout;
|
|
995
|
+
} catch {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
async setPassword(service, account, password) {
|
|
1000
|
+
const params = psParam("target", winTarget(service, account)) + psParam("password", password);
|
|
1001
|
+
await execFileAsync(
|
|
1002
|
+
"powershell.exe",
|
|
1003
|
+
["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_WRITE_SCRIPT],
|
|
1004
|
+
{ timeout: 15e3, windowsHide: true }
|
|
1005
|
+
);
|
|
1006
|
+
},
|
|
1007
|
+
async deletePassword(service, account) {
|
|
1008
|
+
try {
|
|
1009
|
+
const params = psParam("target", winTarget(service, account));
|
|
1010
|
+
await execFileAsync(
|
|
1011
|
+
"powershell.exe",
|
|
1012
|
+
["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_DELETE_SCRIPT],
|
|
1013
|
+
{ timeout: 15e3, windowsHide: true }
|
|
1014
|
+
);
|
|
1015
|
+
return true;
|
|
1016
|
+
} catch {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
var macKeychain = {
|
|
1022
|
+
async getPassword(service, account) {
|
|
1023
|
+
try {
|
|
1024
|
+
const { stdout } = await execFileAsync(
|
|
1025
|
+
"security",
|
|
1026
|
+
["find-generic-password", "-s", service, "-a", account, "-w"],
|
|
1027
|
+
{ timeout: 15e3 }
|
|
1028
|
+
);
|
|
1029
|
+
return stdout.trimEnd();
|
|
1030
|
+
} catch {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
async setPassword(service, account, password) {
|
|
1035
|
+
await execFileAsync(
|
|
1036
|
+
"security",
|
|
1037
|
+
["add-generic-password", "-U", "-s", service, "-a", account, "-w", password],
|
|
1038
|
+
{ timeout: 15e3 }
|
|
1039
|
+
);
|
|
1040
|
+
},
|
|
1041
|
+
async deletePassword(service, account) {
|
|
1042
|
+
try {
|
|
1043
|
+
await execFileAsync(
|
|
1044
|
+
"security",
|
|
1045
|
+
["delete-generic-password", "-s", service, "-a", account],
|
|
1046
|
+
{ timeout: 15e3 }
|
|
1047
|
+
);
|
|
1048
|
+
return true;
|
|
1049
|
+
} catch {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
var linuxKeychain = {
|
|
1055
|
+
async getPassword(service, account) {
|
|
1056
|
+
try {
|
|
1057
|
+
const { stdout } = await execFileAsync(
|
|
1058
|
+
"secret-tool",
|
|
1059
|
+
["lookup", "service", service, "account", account],
|
|
1060
|
+
{ timeout: 15e3 }
|
|
1061
|
+
);
|
|
1062
|
+
return stdout.trimEnd();
|
|
1063
|
+
} catch {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
// secret-tool reads password from stdin (avoids exposing it in process args)
|
|
1068
|
+
async setPassword(service, account, password) {
|
|
1069
|
+
const child = spawn(
|
|
1070
|
+
"secret-tool",
|
|
1071
|
+
["store", "--label", `${service}/${account}`, "service", service, "account", account],
|
|
1072
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
1073
|
+
);
|
|
1074
|
+
child.stdin.write(password);
|
|
1075
|
+
child.stdin.end();
|
|
1076
|
+
await new Promise((resolve, reject) => {
|
|
1077
|
+
child.on(
|
|
1078
|
+
"close",
|
|
1079
|
+
(code) => code === 0 ? resolve() : reject(new Error("secret-tool store failed"))
|
|
1080
|
+
);
|
|
1081
|
+
child.on("error", reject);
|
|
1082
|
+
});
|
|
1083
|
+
},
|
|
1084
|
+
async deletePassword(service, account) {
|
|
1085
|
+
try {
|
|
1086
|
+
await execFileAsync(
|
|
1087
|
+
"secret-tool",
|
|
1088
|
+
["clear", "service", service, "account", account],
|
|
1089
|
+
{ timeout: 15e3 }
|
|
1090
|
+
);
|
|
1091
|
+
return true;
|
|
1092
|
+
} catch {
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
function getBackend() {
|
|
1098
|
+
switch (process.platform) {
|
|
1099
|
+
case "win32":
|
|
1100
|
+
return windowsKeychain;
|
|
1101
|
+
case "darwin":
|
|
1102
|
+
return macKeychain;
|
|
1103
|
+
default:
|
|
1104
|
+
return linuxKeychain;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
var backend = getBackend();
|
|
1108
|
+
async function getPassword(service, account) {
|
|
1109
|
+
return backend.getPassword(service, account);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/secrets/index.ts
|
|
770
1113
|
var SecretsLoader = class {
|
|
771
1114
|
// Same service name as CLI uses
|
|
772
1115
|
SERVICE_NAME = "sinch-functions-cli";
|
|
@@ -780,24 +1123,12 @@ var SecretsLoader = class {
|
|
|
780
1123
|
return false;
|
|
781
1124
|
}
|
|
782
1125
|
try {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
keytar = await import("keytar");
|
|
786
|
-
} catch (error) {
|
|
787
|
-
if (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_MODULE_NOT_FOUND") {
|
|
788
|
-
console.debug("[Secrets] Keytar not available - secrets not loaded");
|
|
789
|
-
return false;
|
|
790
|
-
} else {
|
|
791
|
-
console.error("[Secrets] Error loading keytar:", error.message);
|
|
792
|
-
}
|
|
793
|
-
return false;
|
|
794
|
-
}
|
|
795
|
-
const envPath = path4.join(process.cwd(), ".env");
|
|
796
|
-
if (!fs3.existsSync(envPath)) {
|
|
1126
|
+
const envPath = path5.join(process.cwd(), ".env");
|
|
1127
|
+
if (!fs4.existsSync(envPath)) {
|
|
797
1128
|
console.debug("[Secrets] No .env file found, skipping keychain load");
|
|
798
1129
|
return false;
|
|
799
1130
|
}
|
|
800
|
-
const envContent =
|
|
1131
|
+
const envContent = fs4.readFileSync(envPath, "utf8");
|
|
801
1132
|
const envLines = envContent.replace(/\r\n/g, "\n").split("\n");
|
|
802
1133
|
const secretsToLoad = [];
|
|
803
1134
|
envLines.forEach((line) => {
|
|
@@ -819,7 +1150,7 @@ var SecretsLoader = class {
|
|
|
819
1150
|
}
|
|
820
1151
|
let secretsLoaded = 0;
|
|
821
1152
|
if (secretsToLoad.includes("PROJECT_ID_API_SECRET")) {
|
|
822
|
-
const apiSecret = await
|
|
1153
|
+
const apiSecret = await getPassword(this.SERVICE_NAME, `${this.username}-keySecret`);
|
|
823
1154
|
if (apiSecret) {
|
|
824
1155
|
process.env.PROJECT_ID_API_SECRET = apiSecret;
|
|
825
1156
|
console.log("\u2705 Loaded PROJECT_ID_API_SECRET from secure storage");
|
|
@@ -829,7 +1160,7 @@ var SecretsLoader = class {
|
|
|
829
1160
|
if (secretsToLoad.includes("VOICE_APPLICATION_SECRET")) {
|
|
830
1161
|
const applicationKey = process.env.VOICE_APPLICATION_KEY || this.getApplicationKeyFromConfig();
|
|
831
1162
|
if (applicationKey) {
|
|
832
|
-
const appSecret = await
|
|
1163
|
+
const appSecret = await getPassword(this.SERVICE_NAME, applicationKey);
|
|
833
1164
|
if (appSecret) {
|
|
834
1165
|
process.env.VOICE_APPLICATION_SECRET = appSecret;
|
|
835
1166
|
console.log("\u2705 Loaded VOICE_APPLICATION_SECRET from secure storage");
|
|
@@ -843,7 +1174,7 @@ var SecretsLoader = class {
|
|
|
843
1174
|
continue;
|
|
844
1175
|
}
|
|
845
1176
|
if (functionName) {
|
|
846
|
-
const value = await
|
|
1177
|
+
const value = await getPassword(
|
|
847
1178
|
this.SERVICE_NAME,
|
|
848
1179
|
`${functionName}-${secretName}`
|
|
849
1180
|
);
|
|
@@ -871,9 +1202,9 @@ var SecretsLoader = class {
|
|
|
871
1202
|
*/
|
|
872
1203
|
getApplicationKeyFromConfig() {
|
|
873
1204
|
try {
|
|
874
|
-
const sinchJsonPath =
|
|
875
|
-
if (
|
|
876
|
-
const sinchConfig = JSON.parse(
|
|
1205
|
+
const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
|
|
1206
|
+
if (fs4.existsSync(sinchJsonPath)) {
|
|
1207
|
+
const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
|
|
877
1208
|
return sinchConfig.voiceAppId || sinchConfig.applicationKey || null;
|
|
878
1209
|
}
|
|
879
1210
|
} catch (error) {
|
|
@@ -886,9 +1217,9 @@ var SecretsLoader = class {
|
|
|
886
1217
|
*/
|
|
887
1218
|
getFunctionNameFromConfig() {
|
|
888
1219
|
try {
|
|
889
|
-
const sinchJsonPath =
|
|
890
|
-
if (
|
|
891
|
-
const sinchConfig = JSON.parse(
|
|
1220
|
+
const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
|
|
1221
|
+
if (fs4.existsSync(sinchJsonPath)) {
|
|
1222
|
+
const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
|
|
892
1223
|
return sinchConfig.name || null;
|
|
893
1224
|
}
|
|
894
1225
|
} catch (error) {
|
|
@@ -902,14 +1233,13 @@ var SecretsLoader = class {
|
|
|
902
1233
|
async loadCustomSecrets(secretNames = []) {
|
|
903
1234
|
const secrets = {};
|
|
904
1235
|
try {
|
|
905
|
-
const keytar = await import("keytar");
|
|
906
1236
|
const functionName = this.getFunctionNameFromConfig();
|
|
907
1237
|
if (!functionName) {
|
|
908
1238
|
console.debug("[Secrets] Could not determine function name for custom secrets");
|
|
909
1239
|
return secrets;
|
|
910
1240
|
}
|
|
911
1241
|
for (const secretName of secretNames) {
|
|
912
|
-
const value = await
|
|
1242
|
+
const value = await getPassword(this.SERVICE_NAME, `${functionName}-${secretName}`);
|
|
913
1243
|
if (value) {
|
|
914
1244
|
secrets[secretName] = value;
|
|
915
1245
|
process.env[secretName] = value;
|
|
@@ -921,15 +1251,13 @@ var SecretsLoader = class {
|
|
|
921
1251
|
return secrets;
|
|
922
1252
|
}
|
|
923
1253
|
/**
|
|
924
|
-
* Check if
|
|
1254
|
+
* Check if the native keychain is available.
|
|
1255
|
+
* Always true — no native npm deps required. The underlying OS tool
|
|
1256
|
+
* (secret-tool, security, CredManager) may still be absent, in which
|
|
1257
|
+
* case getPassword() silently returns null per credential.
|
|
925
1258
|
*/
|
|
926
1259
|
async isAvailable() {
|
|
927
|
-
|
|
928
|
-
await import("keytar");
|
|
929
|
-
return true;
|
|
930
|
-
} catch {
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
1260
|
+
return true;
|
|
933
1261
|
}
|
|
934
1262
|
};
|
|
935
1263
|
var secretsLoader = new SecretsLoader();
|
|
@@ -940,6 +1268,13 @@ import axios from "axios";
|
|
|
940
1268
|
|
|
941
1269
|
// src/tunnel/webhook-config.ts
|
|
942
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
|
+
}
|
|
943
1278
|
async function configureConversationWebhooks(tunnelUrl, config) {
|
|
944
1279
|
try {
|
|
945
1280
|
const conversationAppId = process.env.CONVERSATION_APP_ID;
|
|
@@ -961,24 +1296,23 @@ async function configureConversationWebhooks(tunnelUrl, config) {
|
|
|
961
1296
|
app_id: conversationAppId
|
|
962
1297
|
});
|
|
963
1298
|
const existingWebhooks = webhooksResult.webhooks || [];
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
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;
|
|
971
1305
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
}
|
|
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"]
|
|
979
1314
|
});
|
|
980
|
-
|
|
981
|
-
console.log(`\u2705 Created Conversation webhook: ${webhookUrl}`);
|
|
1315
|
+
console.log(`\u2705 Updated Conversation webhook to tunnel: ${webhookUrl}`);
|
|
982
1316
|
console.log("\u{1F4AC} Send a message to your Conversation app to test!");
|
|
983
1317
|
} catch (error) {
|
|
984
1318
|
console.log("\u26A0\uFE0F Could not configure Conversation webhooks:", error.message);
|
|
@@ -997,10 +1331,29 @@ async function cleanupConversationWebhook(config) {
|
|
|
997
1331
|
keyId,
|
|
998
1332
|
keySecret
|
|
999
1333
|
});
|
|
1000
|
-
|
|
1001
|
-
|
|
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}`);
|
|
1002
1353
|
config.conversationWebhookId = void 0;
|
|
1354
|
+
config.originalTarget = void 0;
|
|
1003
1355
|
} catch (error) {
|
|
1356
|
+
console.log("\u26A0\uFE0F Could not restore Conversation webhook:", error.message);
|
|
1004
1357
|
}
|
|
1005
1358
|
}
|
|
1006
1359
|
async function configureElevenLabs() {
|
|
@@ -1320,9 +1673,9 @@ var TunnelClient = class {
|
|
|
1320
1673
|
// src/bin/sinch-runtime.ts
|
|
1321
1674
|
var requireCjs3 = createRequire3(import.meta.url);
|
|
1322
1675
|
function findFunctionPath3() {
|
|
1323
|
-
const distPath =
|
|
1324
|
-
const rootPath =
|
|
1325
|
-
if (
|
|
1676
|
+
const distPath = path6.join(process.cwd(), "dist", "function.js");
|
|
1677
|
+
const rootPath = path6.join(process.cwd(), "function.js");
|
|
1678
|
+
if (fs5.existsSync(distPath)) {
|
|
1326
1679
|
return distPath;
|
|
1327
1680
|
}
|
|
1328
1681
|
return rootPath;
|
|
@@ -1334,6 +1687,8 @@ function loadRuntimeConfig() {
|
|
|
1334
1687
|
functionId: process.env.FUNCTION_ID || process.env.SINCH_FUNCTION_ID || "local-dev"
|
|
1335
1688
|
};
|
|
1336
1689
|
}
|
|
1690
|
+
var storage = createStorageClient();
|
|
1691
|
+
var databasePath = path6.join(process.cwd(), ".sinch", "data", "app.db");
|
|
1337
1692
|
function buildLocalContext(req, runtimeConfig) {
|
|
1338
1693
|
const baseContext = buildBaseContext(req);
|
|
1339
1694
|
const cache = createCacheClient();
|
|
@@ -1341,6 +1696,8 @@ function buildLocalContext(req, runtimeConfig) {
|
|
|
1341
1696
|
return {
|
|
1342
1697
|
...baseContext,
|
|
1343
1698
|
cache,
|
|
1699
|
+
storage,
|
|
1700
|
+
database: databasePath,
|
|
1344
1701
|
...sinchClients,
|
|
1345
1702
|
env: process.env,
|
|
1346
1703
|
config: {
|
|
@@ -1367,12 +1724,12 @@ function displayStartupInfo(config, verbose, _port) {
|
|
|
1367
1724
|
function displayEnvironmentVariables() {
|
|
1368
1725
|
console.log("\nEnvironment Variables:");
|
|
1369
1726
|
try {
|
|
1370
|
-
const envPath =
|
|
1371
|
-
if (!
|
|
1727
|
+
const envPath = path6.join(process.cwd(), ".env");
|
|
1728
|
+
if (!fs5.existsSync(envPath)) {
|
|
1372
1729
|
console.log(" (no .env file found)");
|
|
1373
1730
|
return;
|
|
1374
1731
|
}
|
|
1375
|
-
const envContent =
|
|
1732
|
+
const envContent = fs5.readFileSync(envPath, "utf8");
|
|
1376
1733
|
const envLines = envContent.split("\n");
|
|
1377
1734
|
const variables = [];
|
|
1378
1735
|
const secrets = [];
|
|
@@ -1432,7 +1789,7 @@ function displayApplicationCredentials() {
|
|
|
1432
1789
|
async function displayDetectedFunctions() {
|
|
1433
1790
|
try {
|
|
1434
1791
|
const functionPath = findFunctionPath3();
|
|
1435
|
-
if (!
|
|
1792
|
+
if (!fs5.existsSync(functionPath)) return;
|
|
1436
1793
|
const functionUrl = pathToFileURL2(functionPath).href;
|
|
1437
1794
|
const module = await import(functionUrl);
|
|
1438
1795
|
const userFunction = module.default || module;
|
|
@@ -1459,18 +1816,34 @@ async function main() {
|
|
|
1459
1816
|
} catch {
|
|
1460
1817
|
}
|
|
1461
1818
|
await secretsLoader.loadFromKeychain();
|
|
1819
|
+
fs5.mkdirSync(path6.join(process.cwd(), ".sinch", "storage"), { recursive: true });
|
|
1820
|
+
fs5.mkdirSync(path6.join(process.cwd(), ".sinch", "data"), { recursive: true });
|
|
1462
1821
|
const config = loadRuntimeConfig();
|
|
1463
1822
|
const staticDir = process.env.STATIC_DIR;
|
|
1464
1823
|
const landingPageEnabled = process.env.LANDING_PAGE_ENABLED !== "false";
|
|
1465
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();
|
|
1466
1841
|
setupRequestHandler(app, {
|
|
1467
1842
|
landingPageEnabled,
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
return module.default || module;
|
|
1473
|
-
},
|
|
1843
|
+
authConfig: userAuthConfig,
|
|
1844
|
+
authKey,
|
|
1845
|
+
authSecret,
|
|
1846
|
+
loadUserFunction,
|
|
1474
1847
|
buildContext: (req) => buildLocalContext(req, config),
|
|
1475
1848
|
logger: console.log,
|
|
1476
1849
|
onRequestStart: ({ req }) => {
|