@openclaw/inworld-speech 2026.6.11 → 2026.7.1-beta.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/tts.js CHANGED
@@ -1,8 +1,42 @@
1
+ import { MAX_AUDIO_BYTES } from "openclaw/plugin-sdk/media-runtime";
2
+ import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
1
3
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
2
4
  //#region extensions/inworld/tts.ts
3
5
  const DEFAULT_INWORLD_BASE_URL = "https://api.inworld.ai";
4
6
  const DEFAULT_INWORLD_VOICE_ID = "Sarah";
5
7
  const DEFAULT_INWORLD_MODEL_ID = "inworld-tts-1.5-max";
8
+ const INWORLD_TTS_BODY_MAX_BYTES = MAX_AUDIO_BYTES * 2;
9
+ const INWORLD_VOICES_BODY_MAX_BYTES = MAX_AUDIO_BYTES;
10
+ const INWORLD_BODY_READ_IDLE_TIMEOUT_MS = 3e4;
11
+ const INWORLD_ERROR_BODY_MAX_BYTES = 8 * 1024;
12
+ const INWORLD_ERROR_BODY_MAX_CHARS = 400;
13
+ const INWORLD_ERROR_BODY_READ_IDLE_TIMEOUT_MS = 1e4;
14
+ var InworldErrorBodyOverflow = class extends Error {};
15
+ /**
16
+ * Reads a bounded, whitespace-collapsed diagnostic snippet from a non-OK
17
+ * response body. A misbehaving or hostile endpoint can stream an arbitrarily
18
+ * large error body, so this never buffers it whole: it reuses the shared
19
+ * `readResponseWithLimit` reader (which cancels the underlying stream on
20
+ * overflow and enforces an idle timeout) with a small cap. On overflow it
21
+ * returns a fixed marker instead of echoing attacker-controlled bytes into the
22
+ * thrown error. Kept local to this extension so it depends only on the
23
+ * already-exported `response-limit-runtime` entry and adds no shared plugin-SDK
24
+ * surface.
25
+ */
26
+ async function readInworldErrorBodySnippet(response) {
27
+ let buffer;
28
+ try {
29
+ buffer = await readResponseWithLimit(response, INWORLD_ERROR_BODY_MAX_BYTES, {
30
+ chunkTimeoutMs: INWORLD_ERROR_BODY_READ_IDLE_TIMEOUT_MS,
31
+ onOverflow: () => new InworldErrorBodyOverflow()
32
+ });
33
+ } catch (error) {
34
+ return error instanceof InworldErrorBodyOverflow ? "(error body exceeded diagnostic limit; truncated)" : "";
35
+ }
36
+ const collapsed = buffer.toString("utf8").replace(/\s+/g, " ").trim();
37
+ if (collapsed.length > INWORLD_ERROR_BODY_MAX_CHARS) return `${collapsed.slice(0, INWORLD_ERROR_BODY_MAX_CHARS)}…`;
38
+ return collapsed;
39
+ }
6
40
  const INWORLD_TTS_MODELS = [
7
41
  "inworld-tts-1.5-max",
8
42
  "inworld-tts-1.5-mini",
@@ -55,11 +89,16 @@ async function inworldTTS(params) {
55
89
  });
56
90
  try {
57
91
  if (!response.ok) {
58
- const errorBody = await response.text().catch(() => "");
92
+ const errorBody = await readInworldErrorBodySnippet(response);
59
93
  throw new Error(`Inworld TTS API error (${response.status}): ${errorBody}`);
60
94
  }
61
- const body = await response.text();
95
+ const body = (await readResponseWithLimit(response, INWORLD_TTS_BODY_MAX_BYTES, {
96
+ chunkTimeoutMs: INWORLD_BODY_READ_IDLE_TIMEOUT_MS,
97
+ onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`Inworld TTS audio stream too large: ${size} bytes (limit: ${maxBytes} bytes)`),
98
+ onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`Inworld TTS audio stream stalled: no data received for ${chunkTimeoutMs}ms`)
99
+ })).toString("utf8");
62
100
  const chunks = [];
101
+ let decodedAudioBytes = 0;
63
102
  for (const line of body.split("\n")) {
64
103
  const trimmed = line.trim();
65
104
  if (!trimmed) continue;
@@ -70,7 +109,13 @@ async function inworldTTS(params) {
70
109
  throw new Error(`Inworld TTS stream parse error: unexpected non-JSON line: ${trimmed.slice(0, 80)}`);
71
110
  }
72
111
  if (parsed.error) throw new Error(`Inworld TTS stream error (${parsed.error.code}): ${parsed.error.message}`);
73
- if (parsed.result?.audioContent) chunks.push(Buffer.from(parsed.result.audioContent, "base64"));
112
+ if (parsed.result?.audioContent) {
113
+ const chunk = Buffer.from(parsed.result.audioContent, "base64");
114
+ const nextDecodedAudioBytes = decodedAudioBytes + chunk.length;
115
+ if (nextDecodedAudioBytes > MAX_AUDIO_BYTES) throw new Error(`Inworld TTS decoded audio too large: ${nextDecodedAudioBytes} bytes (limit: ${MAX_AUDIO_BYTES} bytes)`);
116
+ decodedAudioBytes = nextDecodedAudioBytes;
117
+ chunks.push(chunk);
118
+ }
74
119
  }
75
120
  if (chunks.length === 0) throw new Error("Inworld TTS returned no audio data");
76
121
  return Buffer.concat(chunks);
@@ -92,10 +137,20 @@ async function listInworldVoices(params) {
92
137
  });
93
138
  try {
94
139
  if (!response.ok) {
95
- const errorBody = await response.text().catch(() => "");
140
+ const errorBody = await readInworldErrorBodySnippet(response);
96
141
  throw new Error(`Inworld voices API error (${response.status}): ${errorBody}`);
97
142
  }
98
- const json = await response.json();
143
+ const voicesBody = (await readResponseWithLimit(response, INWORLD_VOICES_BODY_MAX_BYTES, {
144
+ chunkTimeoutMs: INWORLD_BODY_READ_IDLE_TIMEOUT_MS,
145
+ onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`Inworld voices response too large: ${size} bytes (limit: ${maxBytes} bytes)`),
146
+ onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`Inworld voices response stalled: no data received for ${chunkTimeoutMs}ms`)
147
+ })).toString("utf8");
148
+ let json;
149
+ try {
150
+ json = JSON.parse(voicesBody);
151
+ } catch {
152
+ throw new Error("Inworld voices API returned malformed JSON");
153
+ }
99
154
  return Array.isArray(json.voices) ? json.voices.map((voice) => ({
100
155
  id: voice.voiceId?.trim() ?? "",
101
156
  name: voice.displayName?.trim() || void 0,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/inworld-speech",
3
- "version": "2026.6.11",
3
+ "version": "2026.7.1-beta.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/inworld-speech",
9
- "version": "2026.6.11"
9
+ "version": "2026.7.1-beta.1"
10
10
  }
11
11
  }
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/inworld-speech",
3
- "version": "2026.6.11",
3
+ "version": "2026.7.1-beta.1",
4
4
  "description": "OpenClaw Inworld speech plugin.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,10 +18,10 @@
18
18
  "minHostVersion": ">=2026.6.8"
19
19
  },
20
20
  "compat": {
21
- "pluginApi": ">=2026.6.11"
21
+ "pluginApi": ">=2026.7.1-beta.1"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.6.11",
24
+ "openclawVersion": "2026.7.1-beta.1",
25
25
  "bundledDist": false
26
26
  },
27
27
  "release": {
@@ -39,7 +39,7 @@
39
39
  "README.md"
40
40
  ],
41
41
  "peerDependencies": {
42
- "openclaw": ">=2026.6.11"
42
+ "openclaw": ">=2026.7.1-beta.1"
43
43
  },
44
44
  "peerDependenciesMeta": {
45
45
  "openclaw": {