daemora 1.0.10 → 1.0.11

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.
@@ -15,9 +15,9 @@
15
15
 
16
16
  import { writeFileSync, mkdirSync } from "node:fs";
17
17
  import { join } from "node:path";
18
- import { tmpdir } from "node:os";
19
-
20
- const TMP_DIR = join(tmpdir(), "daemora-tts");
18
+ import filesystemGuard from "../safety/FilesystemGuard.js";
19
+ import { getTenantTmpDir } from "./_paths.js";
20
+ import tenantContext from "../tenants/TenantContext.js";
21
21
  const OPENAI_CHAR_LIMIT = 4096;
22
22
  const ELEVENLABS_CHAR_LIMIT = 5000;
23
23
 
@@ -31,7 +31,10 @@ export async function textToSpeech(text, optionsJson) {
31
31
  const provider = opts.provider?.toLowerCase() || "openai";
32
32
 
33
33
  // Prefer ElevenLabs if key is present and provider not forced
34
- if (provider === "elevenlabs" || (provider === "auto" && process.env.ELEVENLABS_API_KEY)) {
34
+ const _store = tenantContext.getStore();
35
+ const _keys = _store?.apiKeys || {};
36
+ const hasElevenLabs = _keys.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY;
37
+ if (provider === "elevenlabs" || (provider === "auto" && hasElevenLabs)) {
35
38
  return await _elevenLabs(text.trim(), opts);
36
39
  }
37
40
 
@@ -44,26 +47,30 @@ export async function textToSpeech(text, optionsJson) {
44
47
  // ── OpenAI TTS ────────────────────────────────────────────────────────────────
45
48
 
46
49
  async function _openAI(text, opts) {
47
- if (!process.env.OPENAI_API_KEY) {
50
+ const store = tenantContext.getStore();
51
+ const apiKeys = store?.apiKeys || {};
52
+ const apiKey = apiKeys.OPENAI_API_KEY || process.env.OPENAI_API_KEY;
53
+
54
+ if (!apiKey) {
48
55
  return "Error: textToSpeech requires OPENAI_API_KEY";
49
56
  }
50
57
 
51
58
  const { default: OpenAI } = await import("openai");
52
- const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
59
+ const client = new OpenAI({ apiKey });
53
60
 
54
61
  const voice = opts.voice || "nova"; // nova = clear, neutral, works great for most use cases
55
62
  const speed = Math.max(0.25, Math.min(4.0, parseFloat(opts.speed || "1.0")));
56
63
  const format = opts.format || "mp3"; // mp3 | opus | aac | flac
57
64
  const model = opts.hd === false ? "tts-1" : "tts-1-hd"; // tts-1-hd = better quality
58
65
 
59
- mkdirSync(TMP_DIR, { recursive: true });
66
+ const ttsDir = getTenantTmpDir("daemora-tts");
60
67
 
61
68
  // Split into chunks if text exceeds API limit
62
69
  const chunks = _splitText(text, OPENAI_CHAR_LIMIT);
63
70
 
64
71
  if (chunks.length === 1) {
65
72
  const response = await client.audio.speech.create({ model, voice, input: chunks[0], speed, response_format: format });
66
- const filePath = join(TMP_DIR, `speech-${Date.now()}.${format}`);
73
+ const filePath = join(ttsDir, `speech-${Date.now()}.${format}`);
67
74
  writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
68
75
  return `Audio saved to: ${filePath}`;
69
76
  }
@@ -72,7 +79,7 @@ async function _openAI(text, opts) {
72
79
  const paths = [];
73
80
  for (let i = 0; i < chunks.length; i++) {
74
81
  const response = await client.audio.speech.create({ model, voice, input: chunks[i], speed, response_format: format });
75
- const filePath = join(TMP_DIR, `speech-${Date.now()}-part${i + 1}.${format}`);
82
+ const filePath = join(ttsDir, `speech-${Date.now()}-part${i + 1}.${format}`);
76
83
  writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
77
84
  paths.push(filePath);
78
85
  }
@@ -83,7 +90,9 @@ async function _openAI(text, opts) {
83
90
  // ── ElevenLabs TTS ────────────────────────────────────────────────────────────
84
91
 
85
92
  async function _elevenLabs(text, opts) {
86
- const apiKey = process.env.ELEVENLABS_API_KEY;
93
+ const store = tenantContext.getStore();
94
+ const tenantKeys = store?.apiKeys || {};
95
+ const apiKey = tenantKeys.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY;
87
96
  if (!apiKey) {
88
97
  return "Error: provider=elevenlabs requires ELEVENLABS_API_KEY";
89
98
  }
@@ -115,8 +124,8 @@ async function _elevenLabs(text, opts) {
115
124
  return `Error: ElevenLabs API returned HTTP ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
116
125
  }
117
126
 
118
- mkdirSync(TMP_DIR, { recursive: true });
119
- const filePath = join(TMP_DIR, `speech-eleven-${Date.now()}.mp3`);
127
+ const ttsDir = getTenantTmpDir("daemora-tts");
128
+ const filePath = join(ttsDir, `speech-eleven-${Date.now()}.mp3`);
120
129
  writeFileSync(filePath, Buffer.from(await res.arrayBuffer()));
121
130
  return `Audio saved to: ${filePath}`;
122
131
  }
@@ -9,8 +9,10 @@
9
9
  */
10
10
  import { createReadStream, writeFileSync, existsSync } from "node:fs";
11
11
  import { join, extname, basename } from "node:path";
12
- import { tmpdir } from "node:os";
13
12
  import OpenAI from "openai";
13
+ import filesystemGuard from "../safety/FilesystemGuard.js";
14
+ import { getTenantTmpDir } from "./_paths.js";
15
+ import tenantContext from "../tenants/TenantContext.js";
14
16
 
15
17
  const SUPPORTED_EXTENSIONS = new Set([
16
18
  ".mp3", ".mp4", ".mpeg", ".mpga", ".m4a", ".wav", ".webm", ".ogg", ".oga", ".flac"
@@ -23,7 +25,10 @@ export async function transcribeAudio(audioPath, prompt) {
23
25
  try {
24
26
  if (!audioPath) return "Error: audioPath is required";
25
27
 
26
- if (!process.env.OPENAI_API_KEY) {
28
+ const _store = tenantContext.getStore();
29
+ const _keys = _store?.apiKeys || {};
30
+ const apiKey = _keys.OPENAI_API_KEY || process.env.OPENAI_API_KEY;
31
+ if (!apiKey) {
27
32
  return "Error: transcribeAudio requires OPENAI_API_KEY (uses OpenAI Whisper API)";
28
33
  }
29
34
 
@@ -32,7 +37,7 @@ export async function transcribeAudio(audioPath, prompt) {
32
37
  // Download if URL
33
38
  if (audioPath.startsWith("https://") || audioPath.startsWith("http://")) {
34
39
  const ext = extname(new URL(audioPath).pathname) || ".ogg";
35
- const tmpPath = join(tmpdir(), `audio-${Date.now()}${ext}`);
40
+ const tmpPath = join(getTenantTmpDir("daemora-audio"), `audio-${Date.now()}${ext}`);
36
41
 
37
42
  const res = await fetch(audioPath, { signal: AbortSignal.timeout(30000) });
38
43
  if (!res.ok) return `Error downloading audio: HTTP ${res.status}`;
@@ -42,6 +47,12 @@ export async function transcribeAudio(audioPath, prompt) {
42
47
  localPath = tmpPath;
43
48
  }
44
49
 
50
+ // Guard read access for local files (not downloaded URLs — those are already in tenant workspace)
51
+ if (!audioPath.startsWith("http")) {
52
+ const rc = filesystemGuard.checkRead(localPath);
53
+ if (!rc.allowed) return `Error: ${rc.reason}`;
54
+ }
55
+
45
56
  if (!existsSync(localPath)) {
46
57
  return `Error: Audio file not found: ${localPath}`;
47
58
  }
@@ -56,7 +67,7 @@ export async function transcribeAudio(audioPath, prompt) {
56
67
  return `Error: Unsupported audio format: ${ext}. Supported: ${[...SUPPORTED_EXTENSIONS].join(", ")}`;
57
68
  }
58
69
 
59
- const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
70
+ const openai = new OpenAI({ apiKey });
60
71
 
61
72
  const transcription = await openai.audio.transcriptions.create({
62
73
  file: createReadStream(localPath),