@sinch/functions-runtime 0.3.9 → 0.4.0

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
@@ -760,7 +760,7 @@ export declare function createPieBuilder(): PieSvamlBuilder;
760
760
  /**
761
761
  * Cache interface for Sinch Functions
762
762
  *
763
- * Both dev (LocalCache) and prod (DaprCache) implement this interface,
763
+ * Both dev (LocalCache) and prod (ApiBackedCache) implement this interface,
764
764
  * allowing seamless package swap during deployment.
765
765
  */
766
766
  /**
@@ -865,6 +865,46 @@ export interface IFunctionCache {
865
865
  */
866
866
  getMany<T = unknown>(keys: string[]): Promise<Record<string, T | null>>;
867
867
  }
868
+ /**
869
+ * Storage interface for Sinch Functions
870
+ *
871
+ * Both dev (LocalStorage) and prod (S3Storage) implement this interface,
872
+ * allowing seamless package swap during deployment.
873
+ */
874
+ /**
875
+ * Function storage interface
876
+ *
877
+ * Provides file/blob storage for persistent data.
878
+ * In development, uses the local filesystem (./storage/ directory).
879
+ * In production, uses S3 with local disk caching for reads.
880
+ *
881
+ * Access via `context.storage` — do not construct directly.
882
+ *
883
+ * @example
884
+ * ```typescript
885
+ * // Write a file
886
+ * await context.storage.write('reports/daily.json', JSON.stringify(data));
887
+ *
888
+ * // Read it back
889
+ * const buf = await context.storage.read('reports/daily.json');
890
+ * const data = JSON.parse(buf.toString());
891
+ *
892
+ * // List files
893
+ * const files = await context.storage.list('reports/');
894
+ *
895
+ * // Check existence and delete
896
+ * if (await context.storage.exists('reports/old.json')) {
897
+ * await context.storage.delete('reports/old.json');
898
+ * }
899
+ * ```
900
+ */
901
+ export interface IFunctionStorage {
902
+ write(key: string, data: string | Buffer): Promise<void>;
903
+ read(key: string): Promise<Buffer>;
904
+ list(prefix?: string): Promise<string[]>;
905
+ exists(key: string): Promise<boolean>;
906
+ delete(key: string): Promise<void>;
907
+ }
868
908
  /**
869
909
  * Function configuration with environment variables
870
910
  */
@@ -932,6 +972,37 @@ export interface FunctionContext {
932
972
  * Available when `ENABLE_NUMBERS_API=true` is set.
933
973
  */
934
974
  numbers?: NumbersService;
975
+ /**
976
+ * Persistent file/blob storage.
977
+ * Local filesystem during development, S3-backed in production.
978
+ * @see {@link IFunctionStorage} for available methods
979
+ */
980
+ storage: IFunctionStorage;
981
+ /**
982
+ * File path to a SQLite database for persistent structured data.
983
+ * The database file is managed by a Litestream sidecar in production
984
+ * (continuous WAL replication to S3). In development, it's a local file.
985
+ *
986
+ * Use any SQLite library — `sql.js` (pure WASM, no native deps) or
987
+ * `better-sqlite3` (native C++, fastest but requires build tooling).
988
+ *
989
+ * @example
990
+ * ```typescript
991
+ * // sql.js (recommended — works everywhere)
992
+ * import initSqlJs from 'sql.js';
993
+ * const SQL = await initSqlJs();
994
+ * const buf = existsSync(context.database) ? readFileSync(context.database) : undefined;
995
+ * const db = new SQL.Database(buf);
996
+ * db.run('CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)');
997
+ * writeFileSync(context.database, Buffer.from(db.export()));
998
+ *
999
+ * // better-sqlite3 (fastest — needs python3/make/g++)
1000
+ * import Database from 'better-sqlite3';
1001
+ * const db = new Database(context.database);
1002
+ * db.exec('CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)');
1003
+ * ```
1004
+ */
1005
+ database: string;
935
1006
  /** Read a file from the assets/ directory (private, not served over HTTP) */
936
1007
  assets(filename: string): Promise<string>;
937
1008
  }
@@ -2292,6 +2363,18 @@ export declare class LocalCache implements IFunctionCache {
2292
2363
  * @internal Dev-only factory — access cache via `context.cache`
2293
2364
  */
2294
2365
  export declare function createCacheClient(_projectId?: string, _functionName?: string): IFunctionCache;
2366
+ export declare class LocalStorage implements IFunctionStorage {
2367
+ private baseDir;
2368
+ constructor(baseDir?: string);
2369
+ private resolvePath;
2370
+ write(key: string, data: string | Buffer): Promise<void>;
2371
+ read(key: string): Promise<Buffer>;
2372
+ list(prefix?: string): Promise<string[]>;
2373
+ exists(key: string): Promise<boolean>;
2374
+ delete(key: string): Promise<void>;
2375
+ private walkDir;
2376
+ }
2377
+ export declare function createStorageClient(baseDir?: string): LocalStorage;
2295
2378
  /**
2296
2379
  * Tunnel Client for Sinch Functions
2297
2380
  *
@@ -2380,7 +2463,10 @@ export declare class SecretsLoader {
2380
2463
  */
2381
2464
  loadCustomSecrets(secretNames?: string[]): Promise<Record<string, string>>;
2382
2465
  /**
2383
- * Check if keytar is available
2466
+ * Check if the native keychain is available.
2467
+ * Always true — no native npm deps required. The underlying OS tool
2468
+ * (secret-tool, security, CredManager) may still be absent, in which
2469
+ * case getPassword() silently returns null per credential.
2384
2470
  */
2385
2471
  isAvailable(): Promise<boolean>;
2386
2472
  }
package/dist/index.js CHANGED
@@ -1021,11 +1021,11 @@ function isVoiceCallback(functionName) {
1021
1021
  function isNotificationEvent(functionName) {
1022
1022
  return NOTIFICATION_EVENTS.includes(functionName);
1023
1023
  }
1024
- function extractFunctionName(path5, body) {
1024
+ function extractFunctionName(path6, body) {
1025
1025
  if (body?.event && isVoiceCallback(body.event)) {
1026
1026
  return body.event;
1027
1027
  }
1028
- const pathname = path5.split("?")[0];
1028
+ const pathname = path6.split("?")[0];
1029
1029
  const segments = pathname.split("/").filter((s) => s && s !== "*");
1030
1030
  if (segments.length === 1 && isVoiceCallback(segments[0])) {
1031
1031
  return segments[0];
@@ -1042,10 +1042,10 @@ function generateRequestId() {
1042
1042
  return `req_${Date.now()}_${Math.random().toString(36).substring(7)}`;
1043
1043
  }
1044
1044
  function findFunctionPath() {
1045
- const fs4 = requireCjs2("fs");
1045
+ const fs5 = requireCjs2("fs");
1046
1046
  const distPath = nodePath.join(process.cwd(), "dist", "function.js");
1047
1047
  const rootPath = nodePath.join(process.cwd(), "function.js");
1048
- if (fs4.existsSync(distPath)) {
1048
+ if (fs5.existsSync(distPath)) {
1049
1049
  return distPath;
1050
1050
  }
1051
1051
  return rootPath;
@@ -1123,6 +1123,15 @@ var noOpCache = {
1123
1123
  keys: async () => [],
1124
1124
  getMany: async () => ({})
1125
1125
  };
1126
+ var noOpStorage = {
1127
+ write: async () => {
1128
+ },
1129
+ read: async () => Buffer.alloc(0),
1130
+ list: async () => [],
1131
+ exists: async () => false,
1132
+ delete: async () => {
1133
+ }
1134
+ };
1126
1135
  function buildBaseContext(req, config = {}) {
1127
1136
  return {
1128
1137
  requestId: req.headers["x-request-id"] || generateRequestId(),
@@ -1135,6 +1144,8 @@ function buildBaseContext(req, config = {}) {
1135
1144
  variables: config.variables
1136
1145
  },
1137
1146
  cache: noOpCache,
1147
+ storage: noOpStorage,
1148
+ database: "",
1138
1149
  assets: (filename) => {
1139
1150
  const filePath = nodePath.join(process.cwd(), "assets", filename);
1140
1151
  return nodeFs.promises.readFile(filePath, "utf-8");
@@ -2440,6 +2451,69 @@ function createCacheClient(_projectId, _functionName) {
2440
2451
  return new LocalCache();
2441
2452
  }
2442
2453
 
2454
+ // src/storage/local.ts
2455
+ import * as fs3 from "fs/promises";
2456
+ import * as path4 from "path";
2457
+ var LocalStorage = class {
2458
+ baseDir;
2459
+ constructor(baseDir) {
2460
+ this.baseDir = baseDir ?? path4.join(process.cwd(), "storage");
2461
+ }
2462
+ resolvePath(key) {
2463
+ const sanitized = key.replace(/^\/+/, "").replace(/\.\./g, "_");
2464
+ return path4.join(this.baseDir, sanitized);
2465
+ }
2466
+ async write(key, data) {
2467
+ const filePath = this.resolvePath(key);
2468
+ await fs3.mkdir(path4.dirname(filePath), { recursive: true });
2469
+ await fs3.writeFile(filePath, data);
2470
+ }
2471
+ async read(key) {
2472
+ const filePath = this.resolvePath(key);
2473
+ return fs3.readFile(filePath);
2474
+ }
2475
+ async list(prefix) {
2476
+ const results = [];
2477
+ await this.walkDir(this.baseDir, "", results);
2478
+ if (prefix) {
2479
+ return results.filter((f) => f.startsWith(prefix));
2480
+ }
2481
+ return results;
2482
+ }
2483
+ async exists(key) {
2484
+ const filePath = this.resolvePath(key);
2485
+ try {
2486
+ await fs3.access(filePath);
2487
+ return true;
2488
+ } catch {
2489
+ return false;
2490
+ }
2491
+ }
2492
+ async delete(key) {
2493
+ const filePath = this.resolvePath(key);
2494
+ await fs3.rm(filePath, { force: true });
2495
+ }
2496
+ async walkDir(dir, relative, results) {
2497
+ let entries;
2498
+ try {
2499
+ entries = await fs3.readdir(dir, { withFileTypes: true });
2500
+ } catch {
2501
+ return;
2502
+ }
2503
+ for (const entry of entries) {
2504
+ const rel = relative ? `${relative}/${entry.name}` : entry.name;
2505
+ if (entry.isDirectory()) {
2506
+ await this.walkDir(path4.join(dir, entry.name), rel, results);
2507
+ } else {
2508
+ results.push(rel);
2509
+ }
2510
+ }
2511
+ }
2512
+ };
2513
+ function createStorageClient(baseDir) {
2514
+ return new LocalStorage(baseDir);
2515
+ }
2516
+
2443
2517
  // src/tunnel/index.ts
2444
2518
  import WebSocket from "ws";
2445
2519
  import axios from "axios";
@@ -2831,9 +2905,242 @@ function getTunnelClient(localPort = 3e3) {
2831
2905
  }
2832
2906
 
2833
2907
  // src/secrets/index.ts
2834
- import fs3 from "fs";
2835
- import path4 from "path";
2908
+ import fs4 from "fs";
2909
+ import path5 from "path";
2836
2910
  import os from "os";
2911
+
2912
+ // src/secrets/keychain.ts
2913
+ import { execFile, spawn } from "child_process";
2914
+ import { promisify } from "util";
2915
+ var execFileAsync = promisify(execFile);
2916
+ function b64(value) {
2917
+ return Buffer.from(value, "utf8").toString("base64");
2918
+ }
2919
+ function psParam(name, value) {
2920
+ return `$${name} = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${b64(value)}'));`;
2921
+ }
2922
+ var WIN32_CRED_READ_SCRIPT = `
2923
+ Add-Type -TypeDefinition @'
2924
+ using System;
2925
+ using System.Runtime.InteropServices;
2926
+ public class CredManager {
2927
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
2928
+ private struct CREDENTIAL {
2929
+ public int Flags;
2930
+ public int Type;
2931
+ public IntPtr TargetName;
2932
+ public IntPtr Comment;
2933
+ public long LastWritten;
2934
+ public int CredentialBlobSize;
2935
+ public IntPtr CredentialBlob;
2936
+ public int Persist;
2937
+ public int AttributeCount;
2938
+ public IntPtr Attributes;
2939
+ public IntPtr TargetAlias;
2940
+ public IntPtr UserName;
2941
+ }
2942
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
2943
+ private static extern bool CredReadW(string target, int type, int flags, out IntPtr cred);
2944
+ [DllImport("advapi32.dll")]
2945
+ private static extern void CredFree(IntPtr cred);
2946
+ public static string Read(string target) {
2947
+ IntPtr credPtr;
2948
+ if (!CredReadW(target, 1, 0, out credPtr)) return null;
2949
+ try {
2950
+ CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(credPtr, typeof(CREDENTIAL));
2951
+ if (c.CredentialBlobSize > 0 && c.CredentialBlob != IntPtr.Zero)
2952
+ return Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
2953
+ return "";
2954
+ } finally { CredFree(credPtr); }
2955
+ }
2956
+ }
2957
+ '@
2958
+ $r = [CredManager]::Read($target)
2959
+ if ($r -ne $null) { [Console]::Write($r) }
2960
+ else { exit 1 }
2961
+ `;
2962
+ var WIN32_CRED_WRITE_SCRIPT = `
2963
+ Add-Type -TypeDefinition @'
2964
+ using System;
2965
+ using System.Runtime.InteropServices;
2966
+ using System.Text;
2967
+ public class CredWriter {
2968
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
2969
+ private struct CREDENTIAL {
2970
+ public int Flags;
2971
+ public int Type;
2972
+ public string TargetName;
2973
+ public string Comment;
2974
+ public long LastWritten;
2975
+ public int CredentialBlobSize;
2976
+ public IntPtr CredentialBlob;
2977
+ public int Persist;
2978
+ public int AttributeCount;
2979
+ public IntPtr Attributes;
2980
+ public string TargetAlias;
2981
+ public string UserName;
2982
+ }
2983
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
2984
+ private static extern bool CredWriteW(ref CREDENTIAL cred, int flags);
2985
+ public static bool Write(string target, string password) {
2986
+ byte[] blob = Encoding.Unicode.GetBytes(password);
2987
+ CREDENTIAL c = new CREDENTIAL();
2988
+ c.Type = 1;
2989
+ c.TargetName = target;
2990
+ c.CredentialBlobSize = blob.Length;
2991
+ c.CredentialBlob = Marshal.AllocHGlobal(blob.Length);
2992
+ Marshal.Copy(blob, 0, c.CredentialBlob, blob.Length);
2993
+ c.Persist = 2;
2994
+ try { return CredWriteW(ref c, 0); }
2995
+ finally { Marshal.FreeHGlobal(c.CredentialBlob); }
2996
+ }
2997
+ }
2998
+ '@
2999
+ if (-not [CredWriter]::Write($target, $password)) { exit 1 }
3000
+ `;
3001
+ var WIN32_CRED_DELETE_SCRIPT = `
3002
+ Add-Type -TypeDefinition @'
3003
+ using System;
3004
+ using System.Runtime.InteropServices;
3005
+ public class CredDeleter {
3006
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
3007
+ private static extern bool CredDeleteW(string target, int type, int flags);
3008
+ public static bool Delete(string target) { return CredDeleteW(target, 1, 0); }
3009
+ }
3010
+ '@
3011
+ if (-not [CredDeleter]::Delete($target)) { exit 1 }
3012
+ `;
3013
+ function winTarget(service, account) {
3014
+ return `${service}/${account}`;
3015
+ }
3016
+ var windowsKeychain = {
3017
+ async getPassword(service, account) {
3018
+ try {
3019
+ const params = psParam("target", winTarget(service, account));
3020
+ const { stdout } = await execFileAsync(
3021
+ "powershell.exe",
3022
+ ["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_READ_SCRIPT],
3023
+ { timeout: 15e3, windowsHide: true }
3024
+ );
3025
+ return stdout;
3026
+ } catch {
3027
+ return null;
3028
+ }
3029
+ },
3030
+ async setPassword(service, account, password) {
3031
+ const params = psParam("target", winTarget(service, account)) + psParam("password", password);
3032
+ await execFileAsync(
3033
+ "powershell.exe",
3034
+ ["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_WRITE_SCRIPT],
3035
+ { timeout: 15e3, windowsHide: true }
3036
+ );
3037
+ },
3038
+ async deletePassword(service, account) {
3039
+ try {
3040
+ const params = psParam("target", winTarget(service, account));
3041
+ await execFileAsync(
3042
+ "powershell.exe",
3043
+ ["-NoProfile", "-NonInteractive", "-Command", params + WIN32_CRED_DELETE_SCRIPT],
3044
+ { timeout: 15e3, windowsHide: true }
3045
+ );
3046
+ return true;
3047
+ } catch {
3048
+ return false;
3049
+ }
3050
+ }
3051
+ };
3052
+ var macKeychain = {
3053
+ async getPassword(service, account) {
3054
+ try {
3055
+ const { stdout } = await execFileAsync(
3056
+ "security",
3057
+ ["find-generic-password", "-s", service, "-a", account, "-w"],
3058
+ { timeout: 15e3 }
3059
+ );
3060
+ return stdout.trimEnd();
3061
+ } catch {
3062
+ return null;
3063
+ }
3064
+ },
3065
+ async setPassword(service, account, password) {
3066
+ await execFileAsync(
3067
+ "security",
3068
+ ["add-generic-password", "-U", "-s", service, "-a", account, "-w", password],
3069
+ { timeout: 15e3 }
3070
+ );
3071
+ },
3072
+ async deletePassword(service, account) {
3073
+ try {
3074
+ await execFileAsync(
3075
+ "security",
3076
+ ["delete-generic-password", "-s", service, "-a", account],
3077
+ { timeout: 15e3 }
3078
+ );
3079
+ return true;
3080
+ } catch {
3081
+ return false;
3082
+ }
3083
+ }
3084
+ };
3085
+ var linuxKeychain = {
3086
+ async getPassword(service, account) {
3087
+ try {
3088
+ const { stdout } = await execFileAsync(
3089
+ "secret-tool",
3090
+ ["lookup", "service", service, "account", account],
3091
+ { timeout: 15e3 }
3092
+ );
3093
+ return stdout.trimEnd();
3094
+ } catch {
3095
+ return null;
3096
+ }
3097
+ },
3098
+ // secret-tool reads password from stdin (avoids exposing it in process args)
3099
+ async setPassword(service, account, password) {
3100
+ const child = spawn(
3101
+ "secret-tool",
3102
+ ["store", "--label", `${service}/${account}`, "service", service, "account", account],
3103
+ { stdio: ["pipe", "pipe", "pipe"] }
3104
+ );
3105
+ child.stdin.write(password);
3106
+ child.stdin.end();
3107
+ await new Promise((resolve, reject) => {
3108
+ child.on(
3109
+ "close",
3110
+ (code) => code === 0 ? resolve() : reject(new Error("secret-tool store failed"))
3111
+ );
3112
+ child.on("error", reject);
3113
+ });
3114
+ },
3115
+ async deletePassword(service, account) {
3116
+ try {
3117
+ await execFileAsync(
3118
+ "secret-tool",
3119
+ ["clear", "service", service, "account", account],
3120
+ { timeout: 15e3 }
3121
+ );
3122
+ return true;
3123
+ } catch {
3124
+ return false;
3125
+ }
3126
+ }
3127
+ };
3128
+ function getBackend() {
3129
+ switch (process.platform) {
3130
+ case "win32":
3131
+ return windowsKeychain;
3132
+ case "darwin":
3133
+ return macKeychain;
3134
+ default:
3135
+ return linuxKeychain;
3136
+ }
3137
+ }
3138
+ var backend = getBackend();
3139
+ async function getPassword(service, account) {
3140
+ return backend.getPassword(service, account);
3141
+ }
3142
+
3143
+ // src/secrets/index.ts
2837
3144
  var SecretsLoader = class {
2838
3145
  // Same service name as CLI uses
2839
3146
  SERVICE_NAME = "sinch-functions-cli";
@@ -2847,24 +3154,12 @@ var SecretsLoader = class {
2847
3154
  return false;
2848
3155
  }
2849
3156
  try {
2850
- let keytar;
2851
- try {
2852
- keytar = await import("keytar");
2853
- } catch (error) {
2854
- if (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_MODULE_NOT_FOUND") {
2855
- console.debug("[Secrets] Keytar not available - secrets not loaded");
2856
- return false;
2857
- } else {
2858
- console.error("[Secrets] Error loading keytar:", error.message);
2859
- }
2860
- return false;
2861
- }
2862
- const envPath = path4.join(process.cwd(), ".env");
2863
- if (!fs3.existsSync(envPath)) {
3157
+ const envPath = path5.join(process.cwd(), ".env");
3158
+ if (!fs4.existsSync(envPath)) {
2864
3159
  console.debug("[Secrets] No .env file found, skipping keychain load");
2865
3160
  return false;
2866
3161
  }
2867
- const envContent = fs3.readFileSync(envPath, "utf8");
3162
+ const envContent = fs4.readFileSync(envPath, "utf8");
2868
3163
  const envLines = envContent.replace(/\r\n/g, "\n").split("\n");
2869
3164
  const secretsToLoad = [];
2870
3165
  envLines.forEach((line) => {
@@ -2886,7 +3181,7 @@ var SecretsLoader = class {
2886
3181
  }
2887
3182
  let secretsLoaded = 0;
2888
3183
  if (secretsToLoad.includes("PROJECT_ID_API_SECRET")) {
2889
- const apiSecret = await keytar.getPassword(this.SERVICE_NAME, `${this.username}-keySecret`);
3184
+ const apiSecret = await getPassword(this.SERVICE_NAME, `${this.username}-keySecret`);
2890
3185
  if (apiSecret) {
2891
3186
  process.env.PROJECT_ID_API_SECRET = apiSecret;
2892
3187
  console.log("\u2705 Loaded PROJECT_ID_API_SECRET from secure storage");
@@ -2896,7 +3191,7 @@ var SecretsLoader = class {
2896
3191
  if (secretsToLoad.includes("VOICE_APPLICATION_SECRET")) {
2897
3192
  const applicationKey = process.env.VOICE_APPLICATION_KEY || this.getApplicationKeyFromConfig();
2898
3193
  if (applicationKey) {
2899
- const appSecret = await keytar.getPassword(this.SERVICE_NAME, applicationKey);
3194
+ const appSecret = await getPassword(this.SERVICE_NAME, applicationKey);
2900
3195
  if (appSecret) {
2901
3196
  process.env.VOICE_APPLICATION_SECRET = appSecret;
2902
3197
  console.log("\u2705 Loaded VOICE_APPLICATION_SECRET from secure storage");
@@ -2910,7 +3205,7 @@ var SecretsLoader = class {
2910
3205
  continue;
2911
3206
  }
2912
3207
  if (functionName) {
2913
- const value = await keytar.getPassword(
3208
+ const value = await getPassword(
2914
3209
  this.SERVICE_NAME,
2915
3210
  `${functionName}-${secretName}`
2916
3211
  );
@@ -2938,9 +3233,9 @@ var SecretsLoader = class {
2938
3233
  */
2939
3234
  getApplicationKeyFromConfig() {
2940
3235
  try {
2941
- const sinchJsonPath = path4.join(process.cwd(), "sinch.json");
2942
- if (fs3.existsSync(sinchJsonPath)) {
2943
- const sinchConfig = JSON.parse(fs3.readFileSync(sinchJsonPath, "utf8"));
3236
+ const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
3237
+ if (fs4.existsSync(sinchJsonPath)) {
3238
+ const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
2944
3239
  return sinchConfig.voiceAppId || sinchConfig.applicationKey || null;
2945
3240
  }
2946
3241
  } catch (error) {
@@ -2953,9 +3248,9 @@ var SecretsLoader = class {
2953
3248
  */
2954
3249
  getFunctionNameFromConfig() {
2955
3250
  try {
2956
- const sinchJsonPath = path4.join(process.cwd(), "sinch.json");
2957
- if (fs3.existsSync(sinchJsonPath)) {
2958
- const sinchConfig = JSON.parse(fs3.readFileSync(sinchJsonPath, "utf8"));
3251
+ const sinchJsonPath = path5.join(process.cwd(), "sinch.json");
3252
+ if (fs4.existsSync(sinchJsonPath)) {
3253
+ const sinchConfig = JSON.parse(fs4.readFileSync(sinchJsonPath, "utf8"));
2959
3254
  return sinchConfig.name || null;
2960
3255
  }
2961
3256
  } catch (error) {
@@ -2969,14 +3264,13 @@ var SecretsLoader = class {
2969
3264
  async loadCustomSecrets(secretNames = []) {
2970
3265
  const secrets = {};
2971
3266
  try {
2972
- const keytar = await import("keytar");
2973
3267
  const functionName = this.getFunctionNameFromConfig();
2974
3268
  if (!functionName) {
2975
3269
  console.debug("[Secrets] Could not determine function name for custom secrets");
2976
3270
  return secrets;
2977
3271
  }
2978
3272
  for (const secretName of secretNames) {
2979
- const value = await keytar.getPassword(this.SERVICE_NAME, `${functionName}-${secretName}`);
3273
+ const value = await getPassword(this.SERVICE_NAME, `${functionName}-${secretName}`);
2980
3274
  if (value) {
2981
3275
  secrets[secretName] = value;
2982
3276
  process.env[secretName] = value;
@@ -2988,15 +3282,13 @@ var SecretsLoader = class {
2988
3282
  return secrets;
2989
3283
  }
2990
3284
  /**
2991
- * Check if keytar is available
3285
+ * Check if the native keychain is available.
3286
+ * Always true — no native npm deps required. The underlying OS tool
3287
+ * (secret-tool, security, CredManager) may still be absent, in which
3288
+ * case getPassword() silently returns null per credential.
2992
3289
  */
2993
3290
  async isAvailable() {
2994
- try {
2995
- await import("keytar");
2996
- return true;
2997
- } catch {
2998
- return false;
2999
- }
3291
+ return true;
3000
3292
  }
3001
3293
  };
3002
3294
  var secretsLoader = new SecretsLoader();
@@ -3012,6 +3304,7 @@ export {
3012
3304
  ElevenLabsState,
3013
3305
  IceSvamlBuilder,
3014
3306
  LocalCache,
3307
+ LocalStorage,
3015
3308
  MenuBuilder,
3016
3309
  MenuTemplates,
3017
3310
  NOTIFICATION_EVENTS,
@@ -3042,6 +3335,7 @@ export {
3042
3335
  createResponse,
3043
3336
  createSimpleMenu,
3044
3337
  createSms,
3338
+ createStorageClient,
3045
3339
  createUniversalConfig,
3046
3340
  createWhatsApp,
3047
3341
  extractCallerNumber,