@juspay/neurolink 12.48.2 → 12.49.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/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +616 -595
- package/dist/cli/commands/serve.d.ts +0 -3
- package/dist/cli/commands/serve.js +85 -3
- package/dist/cli/commands/transcribe.d.ts +24 -0
- package/dist/cli/commands/transcribe.js +718 -0
- package/dist/cli/factories/commandFactory.d.ts +10 -0
- package/dist/cli/factories/commandFactory.js +13 -0
- package/dist/cli/parser.js +3 -0
- package/dist/factories/mediaHandlerCatalog.js +11 -9
- package/dist/factories/sttDescriptors.d.ts +63 -0
- package/dist/factories/sttDescriptors.js +286 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +7 -1
- package/dist/neurolink.d.ts +68 -1
- package/dist/neurolink.js +202 -4
- package/dist/processors/media/AudioProcessor.d.ts +15 -56
- package/dist/processors/media/AudioProcessor.js +105 -276
- package/dist/processors/media/VideoProcessor.d.ts +3 -5
- package/dist/processors/media/VideoProcessor.js +52 -51
- package/dist/server/abstract/baseServerAdapter.d.ts +7 -0
- package/dist/server/abstract/baseServerAdapter.js +13 -1
- package/dist/server/adapters/expressAdapter.d.ts +1 -0
- package/dist/server/adapters/expressAdapter.js +48 -4
- package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
- package/dist/server/adapters/fastifyAdapter.js +38 -1
- package/dist/server/adapters/honoAdapter.d.ts +1 -4
- package/dist/server/adapters/honoAdapter.js +83 -1
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +43 -0
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +3 -2
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.js +3 -0
- package/dist/server/routes/transcribeRoutes.d.ts +23 -0
- package/dist/server/routes/transcribeRoutes.js +506 -0
- package/dist/server/utils/rawBody.d.ts +32 -0
- package/dist/server/utils/rawBody.js +76 -0
- package/dist/server/utils/validation.d.ts +192 -0
- package/dist/server/utils/validation.js +100 -0
- package/dist/server/websocket/transcribeWebSocket.d.ts +42 -0
- package/dist/server/websocket/transcribeWebSocket.js +264 -0
- package/dist/types/cli.d.ts +46 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/providers.d.ts +7 -1
- package/dist/types/server.d.ts +117 -1
- package/dist/types/stt.d.ts +160 -7
- package/dist/types/transcribe.d.ts +506 -0
- package/dist/types/transcribe.js +29 -0
- package/dist/utils/speakerTurns.d.ts +28 -0
- package/dist/utils/speakerTurns.js +93 -0
- package/dist/utils/sttProcessor.d.ts +20 -2
- package/dist/utils/sttProcessor.js +73 -5
- package/dist/voice/correction/dictionary.d.ts +49 -0
- package/dist/voice/correction/dictionary.js +134 -0
- package/dist/voice/correction/guard.d.ts +35 -0
- package/dist/voice/correction/guard.js +161 -0
- package/dist/voice/correction/index.d.ts +47 -0
- package/dist/voice/correction/index.js +211 -0
- package/dist/voice/correction/rewrite.d.ts +92 -0
- package/dist/voice/correction/rewrite.js +322 -0
- package/dist/voice/errors.d.ts +15 -0
- package/dist/voice/errors.js +45 -0
- package/dist/voice/index.d.ts +20 -2
- package/dist/voice/index.js +75 -9
- package/dist/voice/providers/AzureSTT.d.ts +10 -7
- package/dist/voice/providers/AzureSTT.js +23 -75
- package/dist/voice/providers/DeepgramSTT.d.ts +17 -2
- package/dist/voice/providers/DeepgramSTT.js +109 -13
- package/dist/voice/providers/ElevenLabsSTT.d.ts +12 -2
- package/dist/voice/providers/ElevenLabsSTT.js +34 -8
- package/dist/voice/providers/GoogleSTT.d.ts +10 -8
- package/dist/voice/providers/GoogleSTT.js +21 -75
- package/dist/voice/providers/OpenAISTT.d.ts +30 -9
- package/dist/voice/providers/OpenAISTT.js +196 -21
- package/dist/voice/providers/WhistleSTT.d.ts +87 -0
- package/dist/voice/providers/WhistleSTT.js +313 -0
- package/dist/voice/streaming/chunkedStreamAdapter.d.ts +19 -0
- package/dist/voice/streaming/chunkedStreamAdapter.js +519 -0
- package/dist/voice/streaming/endpointer.d.ts +81 -0
- package/dist/voice/streaming/endpointer.js +247 -0
- package/dist/voice/streaming/localAgreement.d.ts +30 -0
- package/dist/voice/streaming/localAgreement.js +44 -0
- package/dist/voice/sttEnvCredentials.d.ts +12 -0
- package/dist/voice/sttEnvCredentials.js +12 -0
- package/dist/voice/transcribe.d.ts +47 -0
- package/dist/voice/transcribe.js +519 -0
- package/dist/voice/transcribeStream.d.ts +4 -0
- package/dist/voice/transcribeStream.js +584 -0
- package/dist/voice/whistle/assets.d.ts +47 -0
- package/dist/voice/whistle/assets.js +270 -0
- package/dist/voice/whistle/audio.d.ts +32 -0
- package/dist/voice/whistle/audio.js +267 -0
- package/dist/voice/whistle/engine.d.ts +32 -0
- package/dist/voice/whistle/engine.js +193 -0
- package/dist/voice/whistle/worker.d.ts +18 -0
- package/dist/voice/whistle/worker.js +294 -0
- package/docs-site/static/search-index.json +66 -8
- package/models/whistle/README.md +18 -0
- package/models/whistle/needle.js +2 -0
- package/models/whistle/needle.wasm +0 -0
- package/models/whistle/whistle.cact +0 -0
- package/package.json +7 -3
|
@@ -51,6 +51,9 @@ import { Readable } from "stream";
|
|
|
51
51
|
import { pipeline } from "stream/promises";
|
|
52
52
|
import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
|
|
53
53
|
import { formatKeyframeTimestamp, formatMediaDuration, } from "../../utils/mediaDuration.js";
|
|
54
|
+
import { getSTTProviderDescriptor, resolveDefaultSTTProvider, STT_PROVIDER_DESCRIPTORS, } from "../../factories/sttDescriptors.js";
|
|
55
|
+
import { STTProcessor } from "../../utils/sttProcessor.js";
|
|
56
|
+
import { freshEnvSTTCredentials } from "../../voice/sttEnvCredentials.js";
|
|
54
57
|
import { SIZE_LIMITS_MB } from "../config/index.js";
|
|
55
58
|
import { extensionsForModality, mimeTypesForModality, } from "../config/fileTypeRegistry.js";
|
|
56
59
|
import { FileErrorCode } from "../errors/index.js";
|
|
@@ -211,10 +214,11 @@ const VIDEO_CONFIG = {
|
|
|
211
214
|
* seeking to a handful of offsets, and a meeting recording is long.
|
|
212
215
|
*/
|
|
213
216
|
AUDIO_EXTRACT_TIMEOUT_MS: 180_000,
|
|
214
|
-
/** Timeout for one
|
|
217
|
+
/** Timeout for one transcription request in milliseconds */
|
|
215
218
|
TRANSCRIPTION_TIMEOUT_MS: 120_000,
|
|
216
219
|
/**
|
|
217
|
-
*
|
|
220
|
+
* Transcription upload ceiling in MB (Whisper's documented limit, and a
|
|
221
|
+
* floor for the other engines). The extracted track is mono 16 kHz MP3,
|
|
218
222
|
* so this is roughly six hours of speech — a clip that breaches it is
|
|
219
223
|
* unusual enough to be worth saying so rather than silently truncating.
|
|
220
224
|
*/
|
|
@@ -853,11 +857,9 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
853
857
|
* working implementation of the same thing, and `--transcribe-audio` would
|
|
854
858
|
* still do nothing.
|
|
855
859
|
*
|
|
856
|
-
* The
|
|
857
|
-
* `
|
|
858
|
-
*
|
|
859
|
-
* merge conflict in the audio pipeline is a worse outcome than fifty
|
|
860
|
-
* duplicated lines. The duplication is worth removing once both land.
|
|
860
|
+
* The call goes through `STTProcessor`, like `AudioProcessor` and
|
|
861
|
+
* `transcribe()`, so the default-provider rule, `OPENAI_STT_*` and every
|
|
862
|
+
* STT handler apply to video too.
|
|
861
863
|
*
|
|
862
864
|
* ## Failure behaviour
|
|
863
865
|
*
|
|
@@ -890,11 +892,35 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
890
892
|
if (!metadata.audioCodec) {
|
|
891
893
|
return skipped("the video has no audio track");
|
|
892
894
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
895
|
+
// The same provider rule as every other transcription: the provider named
|
|
896
|
+
// by NEUROLINK_STT_PROVIDER, else the first configured one, else the
|
|
897
|
+
// built-in local engine when it can run here.
|
|
898
|
+
const provider = resolveDefaultSTTProvider();
|
|
899
|
+
const descriptor = getSTTProviderDescriptor(provider);
|
|
900
|
+
// Same slice AudioProcessor builds, from one shared helper, so audio
|
|
901
|
+
// attachments and video audio tracks can never resolve different endpoints.
|
|
902
|
+
const credentials = descriptor
|
|
903
|
+
? freshEnvSTTCredentials(descriptor.credentialsKey)
|
|
904
|
+
: {};
|
|
905
|
+
let handler;
|
|
906
|
+
try {
|
|
907
|
+
handler = await STTProcessor.resolveHandler(provider, credentials);
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
911
|
+
return skipped(`the transcription backend could not be loaded — ${detail}`);
|
|
896
912
|
}
|
|
897
|
-
|
|
913
|
+
if (!handler?.isConfigured()) {
|
|
914
|
+
return skipped(descriptor?.capabilities.local
|
|
915
|
+
? `no transcription backend is configured — set one of: ${STT_PROVIDER_DESCRIPTORS.flatMap((d) => d.envVars).join(", ")}`
|
|
916
|
+
: `transcription provider "${provider}" is not configured`);
|
|
917
|
+
}
|
|
918
|
+
const label = provider === "whisper" ? "openai-whisper" : provider;
|
|
919
|
+
// MP3 keeps a long recording under the upload ceiling; an engine that
|
|
920
|
+
// does not read MP3 gets 16-bit PCM WAV instead.
|
|
921
|
+
const supported = handler.getSupportedFormats();
|
|
922
|
+
const audioFormat = supported.length === 0 || supported.includes("mp3") ? "mp3" : "wav";
|
|
923
|
+
const audioPath = join(tempDir, `audio.${audioFormat}`);
|
|
898
924
|
try {
|
|
899
925
|
await runFfmpeg([
|
|
900
926
|
"-y",
|
|
@@ -905,16 +931,15 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
905
931
|
// Drop the video stream outright. Without -vn ffmpeg tries to carry
|
|
906
932
|
// it into an MP3 container as cover art and fails on most inputs.
|
|
907
933
|
"-vn",
|
|
908
|
-
// Mono at 16 kHz is what
|
|
909
|
-
// an hour-long recording comfortably under the upload ceiling.
|
|
934
|
+
// Mono at 16 kHz is what speech engines resample to anyway, and it
|
|
935
|
+
// keeps an hour-long recording comfortably under the upload ceiling.
|
|
910
936
|
"-ac",
|
|
911
937
|
"1",
|
|
912
938
|
"-ar",
|
|
913
939
|
"16000",
|
|
914
|
-
"
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
"4",
|
|
940
|
+
...(audioFormat === "mp3"
|
|
941
|
+
? ["-c:a", "libmp3lame", "-q:a", "4"]
|
|
942
|
+
: ["-c:a", "pcm_s16le"]),
|
|
918
943
|
audioPath,
|
|
919
944
|
], { timeoutMs: VIDEO_CONFIG.AUDIO_EXTRACT_TIMEOUT_MS });
|
|
920
945
|
}
|
|
@@ -934,52 +959,28 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
934
959
|
}
|
|
935
960
|
const sizeMB = audioBuffer.length / (1024 * 1024);
|
|
936
961
|
if (sizeMB > VIDEO_CONFIG.WHISPER_MAX_SIZE_MB) {
|
|
937
|
-
return skipped(`the extracted audio is ${sizeMB.toFixed(1)}MB, over
|
|
938
|
-
`${VIDEO_CONFIG.WHISPER_MAX_SIZE_MB}MB limit — split the recording`);
|
|
962
|
+
return skipped(`the extracted audio is ${sizeMB.toFixed(1)}MB, over the ` +
|
|
963
|
+
`${VIDEO_CONFIG.WHISPER_MAX_SIZE_MB}MB transcription limit — split the recording`);
|
|
939
964
|
}
|
|
940
|
-
const baseUrl = (process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
941
|
-
const form = new FormData();
|
|
942
|
-
form.append("file", new Blob([new Uint8Array(audioBuffer)], { type: "audio/mpeg" }), "audio.mp3");
|
|
943
|
-
form.append("model", "whisper-1");
|
|
944
|
-
form.append("response_format", "verbose_json");
|
|
945
|
-
// `withTimeout` only races a promise against a timer — it cannot cancel
|
|
946
|
-
// the request. Without the abort, a timed-out upload keeps its socket and
|
|
947
|
-
// its in-flight body alive after the caller has already moved on.
|
|
948
|
-
const abort = new AbortController();
|
|
949
|
-
const timer = setTimeout(() => abort.abort(), VIDEO_CONFIG.TRANSCRIPTION_TIMEOUT_MS);
|
|
950
965
|
try {
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (!response.ok) {
|
|
958
|
-
const detail = await response.text().catch(() => "");
|
|
959
|
-
return skipped(`the transcription request failed — HTTP ${response.status}` +
|
|
960
|
-
`${detail ? `: ${detail.slice(0, 200)}` : ""}`);
|
|
961
|
-
}
|
|
962
|
-
const payload = await response.json();
|
|
963
|
-
const text = typeof payload === "object" &&
|
|
964
|
-
payload !== null &&
|
|
965
|
-
typeof payload.text === "string"
|
|
966
|
-
? payload.text.trim()
|
|
967
|
-
: "";
|
|
966
|
+
const result = await withTimeout(STTProcessor.transcribe(audioBuffer, provider, {
|
|
967
|
+
format: audioFormat,
|
|
968
|
+
timeoutMs: VIDEO_CONFIG.TRANSCRIPTION_TIMEOUT_MS,
|
|
969
|
+
maxAudioBytes: VIDEO_CONFIG.WHISPER_MAX_SIZE_MB * 1024 * 1024,
|
|
970
|
+
}, credentials), VIDEO_CONFIG.TRANSCRIPTION_TIMEOUT_MS);
|
|
971
|
+
const text = result.text.trim();
|
|
968
972
|
if (text.length === 0) {
|
|
969
973
|
// A successful call returning nothing is a real outcome — silence,
|
|
970
974
|
// music, no speech — and not the same as a failure.
|
|
971
|
-
return skipped("Whisper returned an empty transcript for this audio
|
|
975
|
+
return skipped(`${provider === "whisper" ? "Whisper" : label} returned an empty transcript for this audio`);
|
|
972
976
|
}
|
|
973
|
-
logger.debug(`[VideoProcessor] Transcribed ${filename} via
|
|
977
|
+
logger.debug(`[VideoProcessor] Transcribed ${filename} via ${label} (${text.length} chars)`);
|
|
974
978
|
return { transcript: text };
|
|
975
979
|
}
|
|
976
980
|
catch (error) {
|
|
977
981
|
const detail = error instanceof Error ? error.message : String(error);
|
|
978
982
|
return skipped(`the transcription request failed — ${detail}`);
|
|
979
983
|
}
|
|
980
|
-
finally {
|
|
981
|
-
clearTimeout(timer);
|
|
982
|
-
}
|
|
983
984
|
}
|
|
984
985
|
/**
|
|
985
986
|
* Extract keyframes from a video at calculated intervals.
|
|
@@ -57,6 +57,13 @@ export declare abstract class BaseServerAdapter extends EventEmitter {
|
|
|
57
57
|
* Get the underlying framework instance (for advanced usage)
|
|
58
58
|
*/
|
|
59
59
|
abstract getFrameworkInstance(): unknown;
|
|
60
|
+
/**
|
|
61
|
+
* The listening Node `http.Server`, once `start()` has run and the runtime
|
|
62
|
+
* is Node (Bun and Deno serve without one). A WebSocket upgrade handler
|
|
63
|
+
* such as `attachTranscribeWebSocket` attaches to it; `undefined` means
|
|
64
|
+
* there is nothing to attach to.
|
|
65
|
+
*/
|
|
66
|
+
getNativeServer(): unknown;
|
|
60
67
|
/**
|
|
61
68
|
* Stop accepting new connections
|
|
62
69
|
* Called during graceful shutdown to prevent new requests
|
|
@@ -53,7 +53,10 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
53
53
|
basePath: config.basePath ?? "/api",
|
|
54
54
|
cors: {
|
|
55
55
|
enabled: config.cors?.enabled ?? true,
|
|
56
|
-
|
|
56
|
+
// No origin by default: a browser page may call this server only
|
|
57
|
+
// from an origin the operator listed, or from any origin with an
|
|
58
|
+
// explicit ["*"]. The adapters honour the wildcard, so it is opt-in.
|
|
59
|
+
origins: config.cors?.origins ?? [],
|
|
57
60
|
methods: config.cors?.methods ?? [
|
|
58
61
|
"GET",
|
|
59
62
|
"POST",
|
|
@@ -98,6 +101,15 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
98
101
|
shutdown: this.shutdownConfig,
|
|
99
102
|
};
|
|
100
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* The listening Node `http.Server`, once `start()` has run and the runtime
|
|
106
|
+
* is Node (Bun and Deno serve without one). A WebSocket upgrade handler
|
|
107
|
+
* such as `attachTranscribeWebSocket` attaches to it; `undefined` means
|
|
108
|
+
* there is nothing to attach to.
|
|
109
|
+
*/
|
|
110
|
+
getNativeServer() {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
101
113
|
// ============================================
|
|
102
114
|
// Common Methods (Shared Implementation)
|
|
103
115
|
// ============================================
|
|
@@ -6,6 +6,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
6
6
|
import { AlreadyRunningError, ServerAdapterError, ServerStartError, ServerStopError, wrapError, } from "../errors.js";
|
|
7
7
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
8
8
|
import { isErrorResponse } from "../utils/validation.js";
|
|
9
|
+
import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/rawBody.js";
|
|
9
10
|
/**
|
|
10
11
|
* Express-specific server adapter
|
|
11
12
|
*/
|
|
@@ -42,13 +43,23 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
42
43
|
"10mb";
|
|
43
44
|
this.app.use(express.json({ limit: this.config.bodyParser.jsonLimit || bodyLimit }));
|
|
44
45
|
this.app.use(express.urlencoded({ extended: true, limit: bodyLimit }));
|
|
46
|
+
// Uploads (multipart, octet-stream, audio/*) reach the route as bytes.
|
|
47
|
+
this.app.use(express.raw({
|
|
48
|
+
type: (req) => isRawBodyContentType(req.headers["content-type"]),
|
|
49
|
+
limit: this.config.bodyParser.maxSize || "10mb",
|
|
50
|
+
}));
|
|
45
51
|
}
|
|
46
52
|
// CORS
|
|
47
53
|
if (this.config.cors.enabled) {
|
|
48
54
|
const corsModule = await this.importFrameworkDependency("cors", "Express");
|
|
49
55
|
const cors = corsModule.default;
|
|
50
56
|
this.app.use(cors({
|
|
51
|
-
|
|
57
|
+
// A list is matched literally, so the default `["*"]` would match
|
|
58
|
+
// nothing; the wildcard has to be the string "*" (Koa handles it in
|
|
59
|
+
// its origin callback).
|
|
60
|
+
origin: this.config.cors.origins.includes("*")
|
|
61
|
+
? "*"
|
|
62
|
+
: this.config.cors.origins,
|
|
52
63
|
methods: this.config.cors.methods,
|
|
53
64
|
allowedHeaders: this.config.cors.headers,
|
|
54
65
|
credentials: this.config.cors.credentials,
|
|
@@ -170,6 +181,24 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
170
181
|
// Execute handler
|
|
171
182
|
const result = await route.handler(ctx);
|
|
172
183
|
const duration = Date.now() - startTime;
|
|
184
|
+
// A route that answers in its own wire format returns a web Response.
|
|
185
|
+
if (isWebResponse(result)) {
|
|
186
|
+
const raw = await readWebResponse(result);
|
|
187
|
+
this.emit("response", {
|
|
188
|
+
requestId,
|
|
189
|
+
statusCode: raw.status,
|
|
190
|
+
duration,
|
|
191
|
+
timestamp: new Date(),
|
|
192
|
+
});
|
|
193
|
+
for (const [key, value] of Object.entries({
|
|
194
|
+
...ctx.responseHeaders,
|
|
195
|
+
...raw.headers,
|
|
196
|
+
})) {
|
|
197
|
+
res.setHeader(key, value);
|
|
198
|
+
}
|
|
199
|
+
res.status(raw.status).send(raw.body);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
173
202
|
// Check if result is an error response
|
|
174
203
|
if (isErrorResponse(result)) {
|
|
175
204
|
const statusCode = result.httpStatus ?? 500;
|
|
@@ -247,9 +276,21 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
247
276
|
});
|
|
248
277
|
// Use dynamic status code from ServerAdapterError if available
|
|
249
278
|
const isServerAdapterError = error instanceof ServerAdapterError;
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
279
|
+
// body-parser marks an over-limit body; it is the client's error.
|
|
280
|
+
const tooLarge = !isServerAdapterError &&
|
|
281
|
+
"type" in error &&
|
|
282
|
+
error.type === "entity.too.large";
|
|
283
|
+
const statusCode = isServerAdapterError
|
|
284
|
+
? error.getHttpStatus()
|
|
285
|
+
: tooLarge
|
|
286
|
+
? 413
|
|
287
|
+
: 500;
|
|
288
|
+
const errorCode = isServerAdapterError
|
|
289
|
+
? error.code
|
|
290
|
+
: tooLarge
|
|
291
|
+
? "PAYLOAD_TOO_LARGE"
|
|
292
|
+
: "INTERNAL_ERROR";
|
|
293
|
+
const errorMessage = isServerAdapterError || tooLarge
|
|
253
294
|
? error.message
|
|
254
295
|
: "An internal error occurred";
|
|
255
296
|
res.status(statusCode).json({
|
|
@@ -489,4 +530,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
489
530
|
getFrameworkInstance() {
|
|
490
531
|
return this.app;
|
|
491
532
|
}
|
|
533
|
+
getNativeServer() {
|
|
534
|
+
return this.server;
|
|
535
|
+
}
|
|
492
536
|
}
|
|
@@ -8,6 +8,7 @@ import { AlreadyRunningError, ServerAdapterError, ServerStartError, ServerStopEr
|
|
|
8
8
|
import { withTimeout } from "../../utils/errorHandling.js";
|
|
9
9
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
10
10
|
import { isErrorResponse } from "../utils/validation.js";
|
|
11
|
+
import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/rawBody.js";
|
|
11
12
|
/**
|
|
12
13
|
* Fastify-specific server adapter
|
|
13
14
|
* Provides high-performance HTTP server with schema validation
|
|
@@ -46,11 +47,25 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
46
47
|
genReqId: () => this.generateRequestId(),
|
|
47
48
|
bodyLimit: this.parseBodyLimit(this.config.bodyParser.maxSize),
|
|
48
49
|
});
|
|
50
|
+
// Uploads (multipart, octet-stream, audio/*) reach the route as bytes;
|
|
51
|
+
// without a parser Fastify refuses these content types with a 415.
|
|
52
|
+
if (this.config.bodyParser.enabled) {
|
|
53
|
+
this.app.addContentTypeParser(/^\s*(multipart\/form-data|application\/octet-stream|audio\/)/i, { parseAs: "buffer" }, (request, body, done) => {
|
|
54
|
+
done(null, isRawBodyContentType(request.headers["content-type"])
|
|
55
|
+
? body
|
|
56
|
+
: undefined);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
49
59
|
// Register CORS plugin if enabled
|
|
50
60
|
if (this.config.cors.enabled) {
|
|
51
61
|
const corsModule = await this.importFrameworkDependency("@fastify/cors", "Fastify");
|
|
52
62
|
await this.app.register(corsModule.default, {
|
|
53
|
-
|
|
63
|
+
// A list is matched literally, so the default `["*"]` would match
|
|
64
|
+
// nothing; the wildcard has to be the string "*" (Koa handles it in
|
|
65
|
+
// its origin callback).
|
|
66
|
+
origin: this.config.cors.origins.includes("*")
|
|
67
|
+
? "*"
|
|
68
|
+
: this.config.cors.origins,
|
|
54
69
|
methods: this.config.cors.methods,
|
|
55
70
|
allowedHeaders: this.config.cors.headers,
|
|
56
71
|
credentials: this.config.cors.credentials,
|
|
@@ -235,6 +250,24 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
235
250
|
// Execute handler
|
|
236
251
|
const result = await route.handler(ctx);
|
|
237
252
|
const duration = Date.now() - startTime;
|
|
253
|
+
// A route that answers in its own wire format returns a web Response.
|
|
254
|
+
if (isWebResponse(result)) {
|
|
255
|
+
const raw = await readWebResponse(result);
|
|
256
|
+
this.emit("response", {
|
|
257
|
+
requestId,
|
|
258
|
+
statusCode: raw.status,
|
|
259
|
+
duration,
|
|
260
|
+
timestamp: new Date(),
|
|
261
|
+
});
|
|
262
|
+
for (const [key, value] of Object.entries({
|
|
263
|
+
...ctx.responseHeaders,
|
|
264
|
+
...raw.headers,
|
|
265
|
+
})) {
|
|
266
|
+
reply.header(key, value);
|
|
267
|
+
}
|
|
268
|
+
reply.status(raw.status);
|
|
269
|
+
return reply.send(raw.body);
|
|
270
|
+
}
|
|
238
271
|
// Check if result is an error response
|
|
239
272
|
if (isErrorResponse(result)) {
|
|
240
273
|
const statusCode = result.httpStatus ?? 500;
|
|
@@ -485,4 +518,8 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
485
518
|
getFrameworkInstance() {
|
|
486
519
|
return this.app;
|
|
487
520
|
}
|
|
521
|
+
getNativeServer() {
|
|
522
|
+
// Fastify owns its http.Server and exposes it as `app.server`.
|
|
523
|
+
return this.app?.server;
|
|
524
|
+
}
|
|
488
525
|
}
|
|
@@ -6,10 +6,6 @@
|
|
|
6
6
|
import type { NeuroLink } from "../../neurolink.js";
|
|
7
7
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
8
8
|
import type { MiddlewareDefinition, RouteDefinition, ServerAdapterConfig } from "../../types/index.js";
|
|
9
|
-
/**
|
|
10
|
-
* Hono-specific server adapter
|
|
11
|
-
* Supports multiple runtimes: Bun, Deno, Node.js
|
|
12
|
-
*/
|
|
13
9
|
export declare class HonoServerAdapter extends BaseServerAdapter {
|
|
14
10
|
private app;
|
|
15
11
|
private server?;
|
|
@@ -75,6 +71,7 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
|
|
|
75
71
|
* Get the Hono app instance
|
|
76
72
|
*/
|
|
77
73
|
getFrameworkInstance(): unknown;
|
|
74
|
+
getNativeServer(): unknown;
|
|
78
75
|
private extractHeaders;
|
|
79
76
|
private extractQuery;
|
|
80
77
|
private extractBody;
|
|
@@ -7,10 +7,31 @@ import { logger } from "../../utils/logger.js";
|
|
|
7
7
|
import { AlreadyRunningError, ServerAdapterError, ServerStopError, wrapError, } from "../errors.js";
|
|
8
8
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
9
9
|
import { isErrorResponse } from "../utils/validation.js";
|
|
10
|
+
import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readRawBody, } from "../utils/rawBody.js";
|
|
10
11
|
/**
|
|
11
12
|
* Hono-specific server adapter
|
|
12
13
|
* Supports multiple runtimes: Bun, Deno, Node.js
|
|
13
14
|
*/
|
|
15
|
+
/** Raw bodies already read for a request, so a second extraction never hits a consumed stream. */
|
|
16
|
+
const rawBodies = new WeakMap();
|
|
17
|
+
/** A web `ReadableStream` of bytes as an async iterable of Buffers, releasing the reader on exit. */
|
|
18
|
+
async function* webStreamChunks(stream) {
|
|
19
|
+
const reader = stream.getReader();
|
|
20
|
+
try {
|
|
21
|
+
for (;;) {
|
|
22
|
+
const { value, done } = await reader.read();
|
|
23
|
+
if (done) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (value) {
|
|
27
|
+
yield Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
reader.releaseLock();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
14
35
|
export class HonoServerAdapter extends BaseServerAdapter {
|
|
15
36
|
app;
|
|
16
37
|
server;
|
|
@@ -132,7 +153,12 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
132
153
|
// Add CORS if enabled
|
|
133
154
|
if (this.config.cors.enabled) {
|
|
134
155
|
this.app.use("*", cors({
|
|
135
|
-
|
|
156
|
+
// Hono matches a list of origins literally, so `["*"]` (the default)
|
|
157
|
+
// matches nothing and no CORS header is sent; the string "*" is the
|
|
158
|
+
// wildcard. Pass it as such when the list contains it.
|
|
159
|
+
origin: this.config.cors.origins.includes("*")
|
|
160
|
+
? "*"
|
|
161
|
+
: this.config.cors.origins,
|
|
136
162
|
allowMethods: this.config.cors.methods,
|
|
137
163
|
allowHeaders: this.config.cors.headers,
|
|
138
164
|
credentials: this.config.cors.credentials,
|
|
@@ -242,6 +268,21 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
242
268
|
// Execute handler
|
|
243
269
|
const result = await route.handler(ctx);
|
|
244
270
|
const duration = Date.now() - startTime;
|
|
271
|
+
// A route that answers in its own wire format returns a web Response.
|
|
272
|
+
if (isWebResponse(result)) {
|
|
273
|
+
this.emit("response", {
|
|
274
|
+
requestId,
|
|
275
|
+
statusCode: result.status,
|
|
276
|
+
duration,
|
|
277
|
+
timestamp: new Date(),
|
|
278
|
+
});
|
|
279
|
+
for (const [key, value] of Object.entries(ctx.responseHeaders ?? {})) {
|
|
280
|
+
if (!result.headers.has(key)) {
|
|
281
|
+
result.headers.set(key, value);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
245
286
|
// Check if result is an error response
|
|
246
287
|
if (isErrorResponse(result)) {
|
|
247
288
|
const statusCode = result.httpStatus ?? 500;
|
|
@@ -604,6 +645,10 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
604
645
|
getFrameworkInstance() {
|
|
605
646
|
return this.app;
|
|
606
647
|
}
|
|
648
|
+
getNativeServer() {
|
|
649
|
+
// @hono/node-server's serve() returns the http.Server; Bun/Deno do not.
|
|
650
|
+
return this.server;
|
|
651
|
+
}
|
|
607
652
|
// ============================================
|
|
608
653
|
// Helper Methods
|
|
609
654
|
// ============================================
|
|
@@ -643,6 +688,43 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
643
688
|
return undefined;
|
|
644
689
|
}
|
|
645
690
|
}
|
|
691
|
+
// Uploads (multipart, octet-stream, audio/*) reach the route as bytes.
|
|
692
|
+
// Over the size limit the body is the error itself rather than a throw:
|
|
693
|
+
// a throw from the middleware path would land in onError, which answers
|
|
694
|
+
// 500. The route answers 413 (or a validation error) from it.
|
|
695
|
+
if (isRawBodyContentType(contentType)) {
|
|
696
|
+
const limit = parseByteLimit(this.config.bodyParser.maxSize);
|
|
697
|
+
if (Number(c.req.header("Content-Length") ?? 0) > limit) {
|
|
698
|
+
return new RawBodyTooLargeError(limit);
|
|
699
|
+
}
|
|
700
|
+
// Read the web stream chunk by chunk and stop at the limit, so a
|
|
701
|
+
// chunked upload without a Content-Length cannot be buffered whole
|
|
702
|
+
// (and that before any route logic, including the API-key check, runs).
|
|
703
|
+
// The body is extracted more than once per request (middleware and
|
|
704
|
+
// route); a web stream can only be read once, so the outcome is kept
|
|
705
|
+
// per request the way Hono caches `arrayBuffer()`.
|
|
706
|
+
const cached = rawBodies.get(c.req.raw);
|
|
707
|
+
if (cached !== undefined) {
|
|
708
|
+
return cached;
|
|
709
|
+
}
|
|
710
|
+
const stream = c.req.raw.body;
|
|
711
|
+
if (!stream) {
|
|
712
|
+
return Buffer.alloc(0);
|
|
713
|
+
}
|
|
714
|
+
let outcome;
|
|
715
|
+
try {
|
|
716
|
+
outcome = await readRawBody(webStreamChunks(stream), limit);
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
if (!(error instanceof RawBodyTooLargeError)) {
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
await stream.cancel().catch(() => undefined);
|
|
723
|
+
outcome = error;
|
|
724
|
+
}
|
|
725
|
+
rawBodies.set(c.req.raw, outcome);
|
|
726
|
+
return outcome;
|
|
727
|
+
}
|
|
646
728
|
return undefined;
|
|
647
729
|
}
|
|
648
730
|
}
|
|
@@ -7,6 +7,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
7
7
|
import { AlreadyRunningError, ServerAdapterError, ServerStopError, wrapError, } from "../errors.js";
|
|
8
8
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
9
9
|
import { isErrorResponse } from "../utils/validation.js";
|
|
10
|
+
import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readRawBody, readWebResponse, } from "../utils/rawBody.js";
|
|
10
11
|
/**
|
|
11
12
|
* Koa-specific server adapter
|
|
12
13
|
* Leverages Koa's middleware composition for clean request handling
|
|
@@ -111,6 +112,26 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
111
112
|
jsonLimit: this.config.bodyParser.jsonLimit,
|
|
112
113
|
enableTypes: ["json", "form", "text"],
|
|
113
114
|
}));
|
|
115
|
+
// Uploads (multipart, octet-stream, audio/*) reach the route as bytes.
|
|
116
|
+
const rawLimit = parseByteLimit(this.config.bodyParser.maxSize);
|
|
117
|
+
this.app.use(async (ctx, next) => {
|
|
118
|
+
if (isRawBodyContentType(ctx.get("content-type"))) {
|
|
119
|
+
try {
|
|
120
|
+
ctx.request.body = await readRawBody(ctx.req, rawLimit);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof RawBodyTooLargeError) {
|
|
124
|
+
ctx.status = 413;
|
|
125
|
+
ctx.body = {
|
|
126
|
+
error: { code: "PAYLOAD_TOO_LARGE", message: error.message },
|
|
127
|
+
};
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await next();
|
|
134
|
+
});
|
|
114
135
|
}
|
|
115
136
|
// Rate limiting middleware
|
|
116
137
|
if (this.config.rateLimit.enabled) {
|
|
@@ -254,6 +275,25 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
254
275
|
// Execute handler
|
|
255
276
|
const result = await route.handler(serverCtx);
|
|
256
277
|
const duration = Date.now() - startTime;
|
|
278
|
+
// A route that answers in its own wire format returns a web Response.
|
|
279
|
+
if (isWebResponse(result)) {
|
|
280
|
+
const raw = await readWebResponse(result);
|
|
281
|
+
this.emit("response", {
|
|
282
|
+
requestId,
|
|
283
|
+
statusCode: raw.status,
|
|
284
|
+
duration,
|
|
285
|
+
timestamp: new Date(),
|
|
286
|
+
});
|
|
287
|
+
for (const [key, value] of Object.entries({
|
|
288
|
+
...serverCtx.responseHeaders,
|
|
289
|
+
...raw.headers,
|
|
290
|
+
})) {
|
|
291
|
+
ctx.set(key, value);
|
|
292
|
+
}
|
|
293
|
+
ctx.status = raw.status;
|
|
294
|
+
ctx.body = raw.body;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
257
297
|
// Check if result is an error response
|
|
258
298
|
if (isErrorResponse(result)) {
|
|
259
299
|
const statusCode = result.httpStatus ?? 500;
|
|
@@ -535,4 +575,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
535
575
|
getFrameworkInstance() {
|
|
536
576
|
return this.app;
|
|
537
577
|
}
|
|
578
|
+
getNativeServer() {
|
|
579
|
+
return this.server;
|
|
580
|
+
}
|
|
538
581
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -38,9 +38,10 @@ export { createMCPBodyAttachmentMiddleware, fastifyMCPBodyHook, } from "./middle
|
|
|
38
38
|
export { createRateLimitMiddleware, createSlidingWindowRateLimitMiddleware, InMemoryRateLimitStore, } from "./middleware/rateLimit.js";
|
|
39
39
|
export { CommonSchemas, createFieldValidator, createRequestValidationMiddleware, ValidationError, } from "./middleware/validation.js";
|
|
40
40
|
export { AgentExecuteRequestSchema as OpenAPIAgentExecuteRequestSchema, AgentExecuteResponseSchema, AgentInputSchema, ApiKeySecurityScheme, BasicSecurityScheme, BearerSecurityScheme, CommonParameters, ConversationMessageSchema, createApiInfo, createDeleteOperation, createErrorResponse as createOpenAPIErrorResponse, createGetOperation, createHeaderParameter, createOpenAPIGenerator, createPathParameter, createPostOperation, createQueryParameter, createServer as createOpenAPIServer, createStreamingPostOperation, createStreamingResponse, createSuccessResponse, DefaultServers, ErrorResponseSchema, generateOpenAPIFromConfig, generateOpenAPISpec, HealthResponseSchema, MCPServerStatusSchema, MCPServersListResponseSchema, MCPServerToolSchema, MetricsResponseSchema, NeuroLinkApiInfo, OpenAPIGenerator, OpenAPISchemas, ProviderInfoSchema, ReadyResponseSchema, SessionSchema, SessionsListResponseSchema, StandardErrorResponses, StandardTags, TokenUsageSchema, ToolCallSchema, ToolDefinitionSchema, ToolExecuteRequestSchema as OpenAPIToolExecuteRequestSchema, ToolExecuteResponseSchema, ToolListResponseSchema, ToolParameterSchema, } from "./openapi/index.js";
|
|
41
|
-
export { createAgentRoutes, createAllRoutes, createHealthRoutes, createMCPRoutes, createMemoryRoutes, createOpenApiRoutes, createToolRoutes, registerAllRoutes, createClaudeProxyRoutes, createOpenAIProxyRoutes, createCodexProxyRoutes, createGeminiProxyRoutes, } from "./routes/index.js";
|
|
41
|
+
export { createAgentRoutes, createAllRoutes, createHealthRoutes, createMCPRoutes, createMemoryRoutes, createOpenApiRoutes, createToolRoutes, createTranscribeRoutes, registerAllRoutes, createClaudeProxyRoutes, createOpenAIProxyRoutes, createCodexProxyRoutes, createGeminiProxyRoutes, } from "./routes/index.js";
|
|
42
42
|
export { BaseDataStreamWriter, createDataStreamResponse, createDataStreamWriter, createNDJSONHeaders, createSSEHeaders, DataStreamResponse, formatSSEEvent, pipeAsyncIterableToDataStream, WebStreamWriter, } from "./streaming/index.js";
|
|
43
43
|
export { ErrorCategory, ErrorSeverity, ServerAdapterErrorCode, } from "../types/index.js";
|
|
44
44
|
export { createStreamRedactor, redactStreamChunk } from "./utils/redaction.js";
|
|
45
|
-
export { AgentExecuteRequestSchema, createErrorResponse, ServerNameParamSchema, SessionIdParamSchema, ToolArgumentsSchema, ToolExecuteRequestSchema, ToolNameParamSchema, ToolSearchQuerySchema, validateParams, validateQuery, validateRequest, } from "./utils/validation.js";
|
|
45
|
+
export { AgentExecuteRequestSchema, createErrorResponse, ServerNameParamSchema, SessionIdParamSchema, ToolArgumentsSchema, ToolExecuteRequestSchema, ToolNameParamSchema, ToolSearchQuerySchema, TranscribeConfigSchema, TranscribeRequestSchema, TranscribeStreamConfigSchema, validateParams, validateQuery, validateRequest, } from "./utils/validation.js";
|
|
46
46
|
export { createAgentWebSocketHandler, WebSocketConnectionManager, WebSocketMessageRouter, } from "./websocket/index.js";
|
|
47
|
+
export { attachTranscribeWebSocket } from "./websocket/transcribeWebSocket.js";
|
package/dist/server/index.js
CHANGED
|
@@ -93,7 +93,7 @@ OpenAPIGenerator, OpenAPISchemas, ProviderInfoSchema, ReadyResponseSchema, Sessi
|
|
|
93
93
|
// ============================================
|
|
94
94
|
// Routes
|
|
95
95
|
// ============================================
|
|
96
|
-
export { createAgentRoutes, createAllRoutes, createHealthRoutes, createMCPRoutes, createMemoryRoutes, createOpenApiRoutes, createToolRoutes, registerAllRoutes,
|
|
96
|
+
export { createAgentRoutes, createAllRoutes, createHealthRoutes, createMCPRoutes, createMemoryRoutes, createOpenApiRoutes, createToolRoutes, createTranscribeRoutes, registerAllRoutes,
|
|
97
97
|
// The proxy doors. All were reachable only from the deep path
|
|
98
98
|
// ./routes/index.js, which is not a package export — so a consumer of
|
|
99
99
|
// "@juspay/neurolink/server" could reach them only indirectly through
|
|
@@ -120,8 +120,9 @@ export { createStreamRedactor, redactStreamChunk } from "./utils/redaction.js";
|
|
|
120
120
|
// ============================================
|
|
121
121
|
// Validation Utilities
|
|
122
122
|
// ============================================
|
|
123
|
-
export { AgentExecuteRequestSchema, createErrorResponse, ServerNameParamSchema, SessionIdParamSchema, ToolArgumentsSchema, ToolExecuteRequestSchema, ToolNameParamSchema, ToolSearchQuerySchema, validateParams, validateQuery, validateRequest, } from "./utils/validation.js";
|
|
123
|
+
export { AgentExecuteRequestSchema, createErrorResponse, ServerNameParamSchema, SessionIdParamSchema, ToolArgumentsSchema, ToolExecuteRequestSchema, ToolNameParamSchema, ToolSearchQuerySchema, TranscribeConfigSchema, TranscribeRequestSchema, TranscribeStreamConfigSchema, validateParams, validateQuery, validateRequest, } from "./utils/validation.js";
|
|
124
124
|
// ============================================
|
|
125
125
|
// WebSocket
|
|
126
126
|
// ============================================
|
|
127
127
|
export { createAgentWebSocketHandler, WebSocketConnectionManager, WebSocketMessageRouter, } from "./websocket/index.js";
|
|
128
|
+
export { attachTranscribeWebSocket } from "./websocket/transcribeWebSocket.js";
|
|
@@ -13,6 +13,7 @@ export { createMCPRoutes } from "./mcpRoutes.js";
|
|
|
13
13
|
export { createMemoryRoutes } from "./memoryRoutes.js";
|
|
14
14
|
export { createOpenApiRoutes } from "./openApiRoutes.js";
|
|
15
15
|
export { createToolRoutes } from "./toolRoutes.js";
|
|
16
|
+
export { createTranscribeRoutes } from "./transcribeRoutes.js";
|
|
16
17
|
/**
|
|
17
18
|
* Create all standard routes
|
|
18
19
|
* Convenience method that combines all route groups
|