@juspay/neurolink 13.8.16 → 13.8.18
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/CHANGELOG.md +2 -2
- package/README.md +1 -1
- package/dist/action/actionExecutor.d.ts +33 -3
- package/dist/action/actionExecutor.js +71 -15
- package/dist/action/actionInputs.js +4 -1
- package/dist/action/cliVersion.d.ts +25 -0
- package/dist/action/cliVersion.js +26 -0
- package/dist/action/index.d.ts +2 -1
- package/dist/action/index.js +2 -1
- package/dist/adapters/video/directorPipeline.js +46 -36
- package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
- package/dist/adapters/video/ffmpegAdapter.js +32 -5
- package/dist/adapters/video/frameExtractor.js +1 -1
- package/dist/adapters/video/videoMerger.js +5 -2
- package/dist/agent/arithmeticEvaluator.d.ts +32 -0
- package/dist/agent/arithmeticEvaluator.js +349 -0
- package/dist/agent/directTools.d.ts +6 -18
- package/dist/agent/directTools.js +17 -56
- package/dist/artifacts/artifactStore.d.ts +22 -1
- package/dist/artifacts/artifactStore.js +66 -6
- package/dist/auth/anthropicOAuth.d.ts +26 -1
- package/dist/auth/anthropicOAuth.js +81 -5
- package/dist/auth/secureFiles.d.ts +37 -0
- package/dist/auth/secureFiles.js +95 -0
- package/dist/auth/tokenStore.d.ts +43 -6
- package/dist/auth/tokenStore.js +260 -79
- package/dist/avatar/providers/DIDAvatar.js +49 -45
- package/dist/avatar/providers/ReplicateAvatar.js +33 -40
- package/dist/browser/neurolink.min.js +485 -558
- package/dist/cli/commands/auth.js +112 -70
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +128 -13
- package/dist/cli/commands/serve.d.ts +10 -1
- package/dist/cli/commands/serve.js +98 -17
- package/dist/cli/commands/server.js +17 -5
- package/dist/cli/index.js +146 -5
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/envManager.js +17 -3
- package/dist/cli/utils/serverUtils.d.ts +39 -3
- package/dist/cli/utils/serverUtils.js +63 -6
- package/dist/core/baseProvider.js +7 -1
- package/dist/core/modules/MessageBuilder.js +5 -0
- package/dist/core/modules/ToolsManager.d.ts +46 -1
- package/dist/core/modules/ToolsManager.js +190 -43
- package/dist/files/fileReferenceRegistry.d.ts +2 -23
- package/dist/files/fileReferenceRegistry.js +55 -5
- package/dist/hitl/hitlManager.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/mcp/auth/tokenStorage.d.ts +7 -0
- package/dist/mcp/auth/tokenStorage.js +23 -4
- package/dist/mcp/externalServerManager.d.ts +13 -3
- package/dist/mcp/externalServerManager.js +154 -136
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/mcpClientFactory.d.ts +14 -0
- package/dist/mcp/mcpClientFactory.js +70 -7
- package/dist/mcp/projectConfigTrust.d.ts +72 -0
- package/dist/mcp/projectConfigTrust.js +237 -0
- package/dist/mcp/transportUrlPolicy.d.ts +39 -0
- package/dist/mcp/transportUrlPolicy.js +172 -0
- package/dist/music/providers/BeatovenMusic.js +35 -46
- package/dist/music/providers/ReplicateMusic.js +33 -40
- package/dist/neurolink.d.ts +16 -1
- package/dist/neurolink.js +64 -11
- package/dist/processors/archive/ArchiveProcessor.js +15 -1
- package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
- package/dist/processors/archive/zipEntryIndex.js +33 -0
- package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
- package/dist/processors/base/BaseFileProcessor.js +40 -111
- package/dist/processors/data/YamlProcessor.d.ts +13 -1
- package/dist/processors/data/YamlProcessor.js +93 -2
- package/dist/processors/document/OpenDocumentProcessor.js +7 -2
- package/dist/processors/document/PptxProcessor.js +12 -7
- package/dist/processors/media/VideoProcessor.js +93 -15
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +13 -1
- package/dist/proxy/updateState.js +4 -4
- package/dist/rag/document/loaders.d.ts +0 -6
- package/dist/rag/document/loaders.js +16 -10
- package/dist/rag/ragIntegration.d.ts +2 -2
- package/dist/rag/ragIntegration.js +35 -10
- package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
- package/dist/rag/retrieval/vectorQueryTool.js +54 -17
- package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
- package/dist/server/abstract/baseServerAdapter.js +119 -6
- package/dist/server/adapters/expressAdapter.d.ts +3 -0
- package/dist/server/adapters/expressAdapter.js +83 -8
- package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
- package/dist/server/adapters/fastifyAdapter.js +165 -70
- package/dist/server/adapters/honoAdapter.d.ts +9 -1
- package/dist/server/adapters/honoAdapter.js +99 -21
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +153 -74
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/middleware/auth.js +6 -2
- package/dist/server/middleware/cache.js +6 -1
- package/dist/server/middleware/rateLimit.js +29 -4
- package/dist/server/routes/agentRoutes.js +24 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +273 -84
- package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
- package/dist/server/routes/codexProxyRoutes.js +125 -40
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +35 -4
- package/dist/server/routes/mcpRoutes.d.ts +6 -2
- package/dist/server/routes/mcpRoutes.js +21 -7
- package/dist/server/routes/memoryRoutes.js +7 -6
- package/dist/server/routes/toolRoutes.d.ts +12 -2
- package/dist/server/routes/toolRoutes.js +33 -3
- package/dist/server/routes/transcribeRoutes.js +60 -13
- package/dist/server/utils/clientAddress.d.ts +23 -0
- package/dist/server/utils/clientAddress.js +65 -0
- package/dist/server/utils/clientError.d.ts +35 -0
- package/dist/server/utils/clientError.js +129 -0
- package/dist/server/utils/expressBody.d.ts +33 -0
- package/dist/server/utils/expressBody.js +353 -0
- package/dist/server/utils/pathMatch.d.ts +21 -0
- package/dist/server/utils/pathMatch.js +41 -0
- package/dist/server/utils/validation.d.ts +9 -0
- package/dist/server/utils/validation.js +9 -0
- package/dist/server/websocket/WebSocketHandler.js +22 -3
- package/dist/server/websocket/frameQueue.d.ts +19 -0
- package/dist/server/websocket/frameQueue.js +41 -3
- package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
- package/dist/server/websocket/transcribeWebSocket.js +223 -34
- package/dist/skills/skillStores.js +5 -0
- package/dist/tasks/store/fileTaskStore.d.ts +2 -0
- package/dist/tasks/store/fileTaskStore.js +11 -4
- package/dist/types/attachments.d.ts +117 -0
- package/dist/types/attachments.js +15 -0
- package/dist/types/auth.d.ts +6 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/file.d.ts +8 -0
- package/dist/types/hitl.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/mcp.d.ts +98 -0
- package/dist/types/multimodal.js +0 -41
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +62 -3
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +255 -10
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/transcribe.d.ts +6 -0
- package/dist/utils/attachmentPolicy.d.ts +89 -0
- package/dist/utils/attachmentPolicy.js +327 -0
- package/dist/utils/dotenvBootstrap.d.ts +48 -19
- package/dist/utils/dotenvBootstrap.js +172 -31
- package/dist/utils/fileDetector.js +115 -65
- package/dist/utils/json/coerce.js +11 -1
- package/dist/utils/logSanitize.d.ts +5 -0
- package/dist/utils/logSanitize.js +8 -0
- package/dist/utils/logger.js +73 -1
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +118 -105
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +2 -0
- package/dist/utils/redis.d.ts +12 -0
- package/dist/utils/redis.js +72 -0
- package/dist/utils/safeFetch.d.ts +22 -1
- package/dist/utils/safeFetch.js +132 -38
- package/dist/utils/safeStoreId.d.ts +19 -0
- package/dist/utils/safeStoreId.js +29 -0
- package/dist/utils/schemaConversion.d.ts +30 -3
- package/dist/utils/schemaConversion.js +457 -84
- package/dist/utils/sizeGuard.js +2 -2
- package/dist/utils/ssrfGuard.d.ts +3 -3
- package/dist/utils/ssrfGuard.js +71 -39
- package/dist/voice/eval/sttEvalStore.d.ts +26 -3
- package/dist/voice/eval/sttEvalStore.js +77 -12
- package/dist/voice/livekit/agentDefinition.d.ts +18 -0
- package/dist/voice/livekit/agentDefinition.js +22 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
- package/dist/voice/livekit/realtimeMcpTools.js +8 -7
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
- package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
- package/dist/voice/livekit/voiceAgent.d.ts +2 -2
- package/dist/voice/livekit/voiceAgent.js +2 -1
- package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
- package/dist/voice/livekit/xaiSessionWatch.js +30 -0
- package/dist/voice/providers/GeminiLive.js +21 -6
- package/dist/voice/providers/GoogleSTT.js +8 -5
- package/dist/voice/transcribe.d.ts +2 -2
- package/dist/voice/transcribe.js +34 -18
- package/dist/voice/whistle/audio.js +11 -5
- package/docs-site/static/search-index.json +75 -52
- package/package.json +171 -156
- package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { ReadableStream } from "node:stream/web";
|
|
18
18
|
import { logger } from "../../utils/logger.js";
|
|
19
|
+
import { markAgentDefinition } from "./agentDefinition.js";
|
|
19
20
|
import { createVoiceBrain } from "./brain.js";
|
|
20
21
|
import { resolveBrainDefaults, resolveEouTurnDetection } from "./config.js";
|
|
21
22
|
import { attachEventBridge } from "./eventBridge.js";
|
|
@@ -458,5 +459,5 @@ export function defineVoiceAgent(config) {
|
|
|
458
459
|
});
|
|
459
460
|
}
|
|
460
461
|
}
|
|
461
|
-
return { entry };
|
|
462
|
+
return markAgentDefinition({ entry });
|
|
462
463
|
}
|
|
@@ -18,9 +18,4 @@
|
|
|
18
18
|
* third.
|
|
19
19
|
*/
|
|
20
20
|
import type { XaiObservedSession } from "../../types/index.js";
|
|
21
|
-
/**
|
|
22
|
-
* Log the usage of every response, warn when the service runs another model
|
|
23
|
-
* than the one asked for, and report a refused upgrade as a session error.
|
|
24
|
-
* `requestedModel` is the id the session was configured with.
|
|
25
|
-
*/
|
|
26
21
|
export declare function observeXaiSession(session: XaiObservedSession, requestedModel: string): void;
|
|
@@ -129,8 +129,38 @@ function watchForRefusedUpgrade(session) {
|
|
|
129
129
|
* than the one asked for, and report a refused upgrade as a session error.
|
|
130
130
|
* `requestedModel` is the id the session was configured with.
|
|
131
131
|
*/
|
|
132
|
+
/** The plugin's own wording for a refused upgrade (agents-plugin-xai >= 1.9). */
|
|
133
|
+
const PLUGIN_REFUSAL = /refused the connection: (\d{3})/;
|
|
132
134
|
export function observeXaiSession(session, requestedModel) {
|
|
133
135
|
const stopWatching = watchForRefusedUpgrade(session);
|
|
136
|
+
// Since @livekit/agents-plugin-xai 1.9 a refused upgrade no longer escapes
|
|
137
|
+
// as an uncaught exception: the plugin reports it as its own `error` event,
|
|
138
|
+
// worded for OpenAI and naming no key. This listener is registered before
|
|
139
|
+
// the agent's, so it can end the crash watch and relabel the event in place
|
|
140
|
+
// with the xAI-specific diagnosis (HTTP status + XAI_API_KEY hint).
|
|
141
|
+
session.on("error", (event) => {
|
|
142
|
+
if (!isRecord(event)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const inner = event.error;
|
|
146
|
+
const text = inner instanceof Error
|
|
147
|
+
? inner.message
|
|
148
|
+
: typeof event.message === "string"
|
|
149
|
+
? event.message
|
|
150
|
+
: "";
|
|
151
|
+
const status = Number(PLUGIN_REFUSAL.exec(text)?.[1]);
|
|
152
|
+
if (Number.isNaN(status)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
stopWatching();
|
|
156
|
+
const message = describeRefusal(status);
|
|
157
|
+
logger.error("realtime.xai.connectRefused", { status, error: message });
|
|
158
|
+
event.error = new Error(message);
|
|
159
|
+
event.recoverable = false;
|
|
160
|
+
if (typeof event.label !== "string" || event.label.length === 0) {
|
|
161
|
+
event.label = "xai.RealtimeModel";
|
|
162
|
+
}
|
|
163
|
+
});
|
|
134
164
|
session.on("openai_server_event_received", (event) => {
|
|
135
165
|
if (isRecord(event) && event.type === "session.created") {
|
|
136
166
|
stopWatching();
|
|
@@ -6,8 +6,21 @@
|
|
|
6
6
|
* @module voice/providers/GeminiLive
|
|
7
7
|
*/
|
|
8
8
|
import { logger } from "../../utils/logger.js";
|
|
9
|
+
import { redactUrlsInText } from "../../utils/logSanitize.js";
|
|
9
10
|
import { RealtimeError } from "../errors.js";
|
|
10
11
|
import { BaseRealtimeHandler } from "../RealtimeVoiceAPI.js";
|
|
12
|
+
/**
|
|
13
|
+
* A copy of `err` whose message has every URL — and so the `?key=` the Live
|
|
14
|
+
* endpoint requires — reduced to origin + path. The original is left alone
|
|
15
|
+
* for anything still holding it; `cause` is not carried, since it would hold
|
|
16
|
+
* the unscrubbed text.
|
|
17
|
+
*/
|
|
18
|
+
function scrubUrlFromError(err) {
|
|
19
|
+
const scrubbed = new Error(redactUrlsInText(err.message));
|
|
20
|
+
scrubbed.name = err.name;
|
|
21
|
+
scrubbed.stack = err.stack ? redactUrlsInText(err.stack) : undefined;
|
|
22
|
+
return scrubbed;
|
|
23
|
+
}
|
|
11
24
|
/**
|
|
12
25
|
* Google Gemini Live Voice API Handler
|
|
13
26
|
*
|
|
@@ -52,7 +65,9 @@ export class GeminiLive extends BaseRealtimeHandler {
|
|
|
52
65
|
const { default: WebSocket } = await import("ws");
|
|
53
66
|
// Determine model
|
|
54
67
|
const model = config.model ?? "gemini-2.5-flash-native-audio-preview-09-2025";
|
|
55
|
-
// Connect to Gemini Live API
|
|
68
|
+
// Connect to Gemini Live API. The Live endpoint authenticates with the
|
|
69
|
+
// key as a query parameter; every error below is scrubbed with
|
|
70
|
+
// `redactUrlsInText` so that URL never reaches a log or a caller.
|
|
56
71
|
const wsUrl = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=${this.apiKey}`;
|
|
57
72
|
this.ws = new WebSocket(wsUrl);
|
|
58
73
|
// Issue 9: capture a local reference so the closure below doesn't need
|
|
@@ -70,7 +85,7 @@ export class GeminiLive extends BaseRealtimeHandler {
|
|
|
70
85
|
const errorHandler = (err) => {
|
|
71
86
|
clearTimeout(timeout);
|
|
72
87
|
ws.off("open", openHandler);
|
|
73
|
-
reject(err);
|
|
88
|
+
reject(scrubUrlFromError(err));
|
|
74
89
|
};
|
|
75
90
|
const timeout = setTimeout(() => {
|
|
76
91
|
// C1: close the half-opened socket and detach temp listeners so we
|
|
@@ -92,7 +107,7 @@ export class GeminiLive extends BaseRealtimeHandler {
|
|
|
92
107
|
this.session = null;
|
|
93
108
|
});
|
|
94
109
|
this.ws.on("error", (err) => {
|
|
95
|
-
this.emitError(err);
|
|
110
|
+
this.emitError(scrubUrlFromError(err));
|
|
96
111
|
});
|
|
97
112
|
// Send setup message
|
|
98
113
|
await this.sendSetup(config, model);
|
|
@@ -114,8 +129,8 @@ export class GeminiLive extends BaseRealtimeHandler {
|
|
|
114
129
|
}
|
|
115
130
|
catch (err) {
|
|
116
131
|
this.emitStateChange("error");
|
|
117
|
-
const errorMessage = err instanceof Error ? err.message : String(err || "Unknown error");
|
|
118
|
-
throw RealtimeError.connectionFailed(errorMessage, "gemini-live", err instanceof Error ? err : undefined);
|
|
132
|
+
const errorMessage = redactUrlsInText(err instanceof Error ? err.message : String(err || "Unknown error"));
|
|
133
|
+
throw RealtimeError.connectionFailed(errorMessage, "gemini-live", err instanceof Error ? scrubUrlFromError(err) : undefined);
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
async disconnect() {
|
|
@@ -133,7 +148,7 @@ export class GeminiLive extends BaseRealtimeHandler {
|
|
|
133
148
|
logger.info("[GeminiLiveHandler] Disconnected");
|
|
134
149
|
}
|
|
135
150
|
catch (err) {
|
|
136
|
-
const errorMessage = err instanceof Error ? err.message : String(err || "Unknown error");
|
|
151
|
+
const errorMessage = redactUrlsInText(err instanceof Error ? err.message : String(err || "Unknown error"));
|
|
137
152
|
throw RealtimeError.protocolError(`Disconnect failed: ${errorMessage}`, "gemini-live", err instanceof Error ? err : undefined);
|
|
138
153
|
}
|
|
139
154
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module voice/providers/GoogleSTT
|
|
7
7
|
*/
|
|
8
8
|
import { logger } from "../../utils/logger.js";
|
|
9
|
+
import { redactUrlsInText } from "../../utils/logSanitize.js";
|
|
9
10
|
import { STTError } from "../errors.js";
|
|
10
11
|
/**
|
|
11
12
|
* Google Cloud Speech-to-Text Handler
|
|
@@ -213,10 +214,9 @@ export class GoogleSTT {
|
|
|
213
214
|
content: audioBuffer.toString("base64"),
|
|
214
215
|
},
|
|
215
216
|
};
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
: `${this.baseUrl}/speech:recognize`;
|
|
217
|
+
// The key travels in the `x-goog-api-key` header, never in the URL:
|
|
218
|
+
// a URL is what proxies, access logs and `fetch failed` causes record.
|
|
219
|
+
const url = `${this.baseUrl}/speech:recognize`;
|
|
220
220
|
const controller = new AbortController();
|
|
221
221
|
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
222
222
|
let response;
|
|
@@ -225,6 +225,7 @@ export class GoogleSTT {
|
|
|
225
225
|
method: "POST",
|
|
226
226
|
headers: {
|
|
227
227
|
"Content-Type": "application/json",
|
|
228
|
+
...(this.apiKey ? { "x-goog-api-key": this.apiKey } : {}),
|
|
228
229
|
...(this.credentialsPath && !this.apiKey
|
|
229
230
|
? { Authorization: `Bearer ${await this.getAccessToken()}` }
|
|
230
231
|
: {}),
|
|
@@ -237,7 +238,9 @@ export class GoogleSTT {
|
|
|
237
238
|
if (fetchErr instanceof Error && fetchErr.name === "AbortError") {
|
|
238
239
|
throw STTError.transcriptionFailed("Google STT request timed out after 30 seconds", "google-stt", fetchErr);
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
+
// A transport error embeds the request URL; scrub any URL (and its
|
|
242
|
+
// query) before the message reaches a log or a caller.
|
|
243
|
+
throw STTError.transcriptionFailed(redactUrlsInText(fetchErr instanceof Error ? fetchErr.message : String(fetchErr)), "google-stt", fetchErr instanceof Error ? fetchErr : undefined);
|
|
241
244
|
}
|
|
242
245
|
finally {
|
|
243
246
|
clearTimeout(timeoutId);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module voice/transcribe
|
|
14
14
|
*/
|
|
15
|
-
import type { STTOptions, STTStreamingOptions, TranscribeAudioInput, TranscribeDeps, TranscribeOptions, TranscribeResult, TTSAudioFormat } from "../types/index.js";
|
|
15
|
+
import type { AttachmentPolicy, STTOptions, STTStreamingOptions, TranscribeAudioInput, TranscribeDeps, TranscribeOptions, TranscribeResult, TTSAudioFormat } from "../types/index.js";
|
|
16
16
|
/**
|
|
17
17
|
* The first `seconds` of a PCM WAV as a smaller WAV, so a language probe
|
|
18
18
|
* uploads kilobytes instead of the whole recording. Anything that is not a
|
|
@@ -33,7 +33,7 @@ export declare function audioFormatFromBytes(buffer: Buffer): TTSAudioFormat | u
|
|
|
33
33
|
*
|
|
34
34
|
* @throws STTError when the input is a frame stream, or cannot be read
|
|
35
35
|
*/
|
|
36
|
-
export declare function loadTranscribeAudio(input: TranscribeAudioInput, timeoutMs?: number): Promise<{
|
|
36
|
+
export declare function loadTranscribeAudio(input: TranscribeAudioInput, timeoutMs?: number, attachmentPolicy?: AttachmentPolicy): Promise<{
|
|
37
37
|
buffer: Buffer;
|
|
38
38
|
formatHint?: TTSAudioFormat;
|
|
39
39
|
}>;
|
package/dist/voice/transcribe.js
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module voice/transcribe
|
|
14
14
|
*/
|
|
15
|
-
import { promises as fs } from "fs";
|
|
16
15
|
import { extname } from "path";
|
|
17
16
|
import { fileURLToPath } from "url";
|
|
17
|
+
import { downloadAttachment, readAttachmentFile, } from "../utils/attachmentPolicy.js";
|
|
18
18
|
import { guardEngineResult, promptTermsOf, runTranscribeStream, } from "./transcribeStream.js";
|
|
19
19
|
import { ENGINE_SAMPLE_RATE } from "./streaming/endpointer.js";
|
|
20
20
|
import { alignWordsToText } from "./correction/align.js";
|
|
@@ -74,6 +74,12 @@ function correctionLinesDisabled() {
|
|
|
74
74
|
}
|
|
75
75
|
/** Default timeout for fetching an `http(s)://` audio input. */
|
|
76
76
|
const URL_FETCH_TIMEOUT_MS = 60_000;
|
|
77
|
+
/**
|
|
78
|
+
* Ceiling for a recording named by path or URL: 512 MiB, the same budget the
|
|
79
|
+
* transcription WebSocket allows a file-mode upload. `attachments.maxBytes`
|
|
80
|
+
* lowers it.
|
|
81
|
+
*/
|
|
82
|
+
const TRANSCRIBE_AUDIO_MAX_BYTES = 512 * 1024 * 1024;
|
|
77
83
|
/** Container names an extension or magic number can map to. */
|
|
78
84
|
const KNOWN_FORMATS = [
|
|
79
85
|
"mp3",
|
|
@@ -140,7 +146,7 @@ export function audioFormatFromBytes(buffer) {
|
|
|
140
146
|
*
|
|
141
147
|
* @throws STTError when the input is a frame stream, or cannot be read
|
|
142
148
|
*/
|
|
143
|
-
export async function loadTranscribeAudio(input, timeoutMs) {
|
|
149
|
+
export async function loadTranscribeAudio(input, timeoutMs, attachmentPolicy) {
|
|
144
150
|
if (Buffer.isBuffer(input)) {
|
|
145
151
|
return { buffer: input };
|
|
146
152
|
}
|
|
@@ -160,17 +166,20 @@ export async function loadTranscribeAudio(input, timeoutMs) {
|
|
|
160
166
|
}
|
|
161
167
|
const source = input.trim();
|
|
162
168
|
if (/^https?:\/\//i.test(source)) {
|
|
163
|
-
const controller = new AbortController();
|
|
164
169
|
const limit = timeoutMs ?? URL_FETCH_TIMEOUT_MS;
|
|
165
|
-
const timer = setTimeout(() => controller.abort(), limit);
|
|
166
170
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
171
|
+
// The instance's attachment policy: HTTPS to a public host unless it
|
|
172
|
+
// says otherwise, the connection pinned to the vetted addresses,
|
|
173
|
+
// redirects re-validated per hop, and the body bounded while it
|
|
174
|
+
// streams rather than read whole with `arrayBuffer()`.
|
|
175
|
+
const downloaded = await downloadAttachment(source, {
|
|
176
|
+
policy: attachmentPolicy,
|
|
177
|
+
maxBytes: TRANSCRIBE_AUDIO_MAX_BYTES,
|
|
178
|
+
label: "audio URL",
|
|
179
|
+
timeoutMs: limit,
|
|
180
|
+
});
|
|
172
181
|
return {
|
|
173
|
-
buffer,
|
|
182
|
+
buffer: downloaded.buffer,
|
|
174
183
|
formatHint: audioFormatFromName(new URL(source).pathname),
|
|
175
184
|
};
|
|
176
185
|
}
|
|
@@ -183,9 +192,6 @@ export async function loadTranscribeAudio(input, timeoutMs) {
|
|
|
183
192
|
? `fetching the audio URL timed out after ${limit}ms`
|
|
184
193
|
: `fetching the audio URL failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
|
|
185
194
|
}
|
|
186
|
-
finally {
|
|
187
|
-
clearTimeout(timer);
|
|
188
|
-
}
|
|
189
195
|
}
|
|
190
196
|
let path;
|
|
191
197
|
try {
|
|
@@ -195,16 +201,26 @@ export async function loadTranscribeAudio(input, timeoutMs) {
|
|
|
195
201
|
throw STTError.audioLoadFailed("the file: URL is not a valid local path", error instanceof Error ? error : undefined);
|
|
196
202
|
}
|
|
197
203
|
try {
|
|
204
|
+
// Roots containment when configured, O_NOFOLLOW open of the real path,
|
|
205
|
+
// regular files only, bounded read.
|
|
206
|
+
const read = await readAttachmentFile(path, {
|
|
207
|
+
policy: attachmentPolicy,
|
|
208
|
+
maxBytes: TRANSCRIBE_AUDIO_MAX_BYTES,
|
|
209
|
+
label: "audio file",
|
|
210
|
+
});
|
|
198
211
|
return {
|
|
199
|
-
buffer:
|
|
212
|
+
buffer: read.buffer,
|
|
200
213
|
formatHint: audioFormatFromName(path),
|
|
201
214
|
};
|
|
202
215
|
}
|
|
203
216
|
catch (error) {
|
|
204
217
|
const code = error?.code;
|
|
205
|
-
|
|
206
|
-
?
|
|
207
|
-
|
|
218
|
+
const detail = typeof code === "string" && code.startsWith("ATTACHMENT_")
|
|
219
|
+
? error instanceof Error
|
|
220
|
+
? error.message
|
|
221
|
+
: String(error)
|
|
222
|
+
: `reading the file failed${code ? ` (${String(code)})` : ""}`;
|
|
223
|
+
throw STTError.audioLoadFailed(code === "ENOENT" ? "no file at the given path" : detail, error instanceof Error ? error : undefined);
|
|
208
224
|
}
|
|
209
225
|
}
|
|
210
226
|
// ============================================================================
|
|
@@ -575,7 +591,7 @@ function sumTimings(corrections, key) {
|
|
|
575
591
|
async function transcribeLayers(options, deps, startedAt) {
|
|
576
592
|
const steps = [];
|
|
577
593
|
// 1. Audio, and its container when the caller did not name one.
|
|
578
|
-
const { buffer, formatHint } = await loadTranscribeAudio(options.audio, options.timeoutMs);
|
|
594
|
+
const { buffer, formatHint } = await loadTranscribeAudio(options.audio, options.timeoutMs, deps.attachmentPolicy);
|
|
579
595
|
if (buffer.length === 0) {
|
|
580
596
|
throw STTError.audioEmpty(options.provider);
|
|
581
597
|
}
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* @module voice/whistle/audio
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
-
import {
|
|
16
|
-
import { rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
17
16
|
import { tmpdir } from "node:os";
|
|
18
17
|
import { join } from "node:path";
|
|
19
18
|
import { STT_ERROR_CODES } from "../../types/index.js";
|
|
@@ -209,8 +208,11 @@ function ffmpegUnavailable(format, cause) {
|
|
|
209
208
|
async function decodeWithFfmpeg(audio, format) {
|
|
210
209
|
const binary = await resolveFfmpeg();
|
|
211
210
|
// A file, not a pipe: MP4/M4A often keep their index at the end, which a pipe cannot seek to.
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
// Its own mkdtemp directory (0o700) and an owner-only file: the recording is
|
|
212
|
+
// the caller's, and the OS temp directory is shared.
|
|
213
|
+
const dir = await mkdtemp(join(tmpdir(), "neurolink-whistle-"));
|
|
214
|
+
const input = join(dir, `input.${format ?? "bin"}`);
|
|
215
|
+
await writeFile(input, audio, { mode: 0o600, flag: "wx" });
|
|
214
216
|
try {
|
|
215
217
|
const raw = await new Promise((resolvePromise, reject) => {
|
|
216
218
|
const proc = spawn(binary, [
|
|
@@ -218,6 +220,10 @@ async function decodeWithFfmpeg(audio, format) {
|
|
|
218
220
|
"-hide_banner",
|
|
219
221
|
"-loglevel",
|
|
220
222
|
"error",
|
|
223
|
+
// Pin the demuxer to local files: a container that references
|
|
224
|
+
// another URL or file cannot make ffmpeg open it.
|
|
225
|
+
"-protocol_whitelist",
|
|
226
|
+
"file,crypto,data",
|
|
221
227
|
"-i",
|
|
222
228
|
input,
|
|
223
229
|
"-f",
|
|
@@ -262,6 +268,6 @@ async function decodeWithFfmpeg(audio, format) {
|
|
|
262
268
|
throw STTError.invalidFormat(`${format ?? "unknown"} (ffmpeg could not decode it: ${error instanceof Error ? error.message : String(error)})`, [...WHISTLE_NATIVE_FORMATS, ...WHISTLE_FFMPEG_FORMATS], "whistle");
|
|
263
269
|
}
|
|
264
270
|
finally {
|
|
265
|
-
await rm(
|
|
271
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
266
272
|
}
|
|
267
273
|
}
|