@juspay/neurolink 12.48.2 → 12.49.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/browser/neurolink.min.js +616 -595
  3. package/dist/cli/commands/serve.d.ts +0 -3
  4. package/dist/cli/commands/serve.js +85 -3
  5. package/dist/cli/commands/transcribe.d.ts +24 -0
  6. package/dist/cli/commands/transcribe.js +718 -0
  7. package/dist/cli/factories/commandFactory.d.ts +10 -0
  8. package/dist/cli/factories/commandFactory.js +13 -0
  9. package/dist/cli/parser.js +3 -0
  10. package/dist/factories/mediaHandlerCatalog.js +11 -9
  11. package/dist/factories/providerDescriptors.js +5 -4
  12. package/dist/factories/sttDescriptors.d.ts +63 -0
  13. package/dist/factories/sttDescriptors.js +286 -0
  14. package/dist/index.d.ts +4 -1
  15. package/dist/index.js +7 -1
  16. package/dist/neurolink.d.ts +68 -1
  17. package/dist/neurolink.js +202 -4
  18. package/dist/processors/media/AudioProcessor.d.ts +15 -56
  19. package/dist/processors/media/AudioProcessor.js +105 -276
  20. package/dist/processors/media/VideoProcessor.d.ts +3 -5
  21. package/dist/processors/media/VideoProcessor.js +52 -51
  22. package/dist/providers/cloudflareClef.js +9 -1
  23. package/dist/server/abstract/baseServerAdapter.d.ts +7 -0
  24. package/dist/server/abstract/baseServerAdapter.js +13 -1
  25. package/dist/server/adapters/expressAdapter.d.ts +1 -0
  26. package/dist/server/adapters/expressAdapter.js +48 -4
  27. package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
  28. package/dist/server/adapters/fastifyAdapter.js +38 -1
  29. package/dist/server/adapters/honoAdapter.d.ts +1 -4
  30. package/dist/server/adapters/honoAdapter.js +83 -1
  31. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  32. package/dist/server/adapters/koaAdapter.js +43 -0
  33. package/dist/server/index.d.ts +3 -2
  34. package/dist/server/index.js +3 -2
  35. package/dist/server/routes/index.d.ts +1 -0
  36. package/dist/server/routes/index.js +3 -0
  37. package/dist/server/routes/transcribeRoutes.d.ts +23 -0
  38. package/dist/server/routes/transcribeRoutes.js +506 -0
  39. package/dist/server/utils/rawBody.d.ts +32 -0
  40. package/dist/server/utils/rawBody.js +76 -0
  41. package/dist/server/utils/validation.d.ts +192 -0
  42. package/dist/server/utils/validation.js +100 -0
  43. package/dist/server/websocket/transcribeWebSocket.d.ts +42 -0
  44. package/dist/server/websocket/transcribeWebSocket.js +264 -0
  45. package/dist/types/cli.d.ts +46 -0
  46. package/dist/types/index.d.ts +1 -0
  47. package/dist/types/index.js +1 -0
  48. package/dist/types/providers.d.ts +7 -1
  49. package/dist/types/server.d.ts +117 -1
  50. package/dist/types/stt.d.ts +160 -7
  51. package/dist/types/transcribe.d.ts +506 -0
  52. package/dist/types/transcribe.js +29 -0
  53. package/dist/utils/speakerTurns.d.ts +28 -0
  54. package/dist/utils/speakerTurns.js +93 -0
  55. package/dist/utils/sttProcessor.d.ts +20 -2
  56. package/dist/utils/sttProcessor.js +73 -5
  57. package/dist/voice/correction/dictionary.d.ts +49 -0
  58. package/dist/voice/correction/dictionary.js +134 -0
  59. package/dist/voice/correction/guard.d.ts +35 -0
  60. package/dist/voice/correction/guard.js +161 -0
  61. package/dist/voice/correction/index.d.ts +47 -0
  62. package/dist/voice/correction/index.js +211 -0
  63. package/dist/voice/correction/rewrite.d.ts +92 -0
  64. package/dist/voice/correction/rewrite.js +322 -0
  65. package/dist/voice/errors.d.ts +15 -0
  66. package/dist/voice/errors.js +45 -0
  67. package/dist/voice/index.d.ts +20 -2
  68. package/dist/voice/index.js +75 -9
  69. package/dist/voice/providers/AzureSTT.d.ts +10 -7
  70. package/dist/voice/providers/AzureSTT.js +23 -75
  71. package/dist/voice/providers/DeepgramSTT.d.ts +17 -2
  72. package/dist/voice/providers/DeepgramSTT.js +109 -13
  73. package/dist/voice/providers/ElevenLabsSTT.d.ts +12 -2
  74. package/dist/voice/providers/ElevenLabsSTT.js +34 -8
  75. package/dist/voice/providers/GoogleSTT.d.ts +10 -8
  76. package/dist/voice/providers/GoogleSTT.js +21 -75
  77. package/dist/voice/providers/OpenAISTT.d.ts +30 -9
  78. package/dist/voice/providers/OpenAISTT.js +196 -21
  79. package/dist/voice/providers/WhistleSTT.d.ts +87 -0
  80. package/dist/voice/providers/WhistleSTT.js +313 -0
  81. package/dist/voice/streaming/chunkedStreamAdapter.d.ts +19 -0
  82. package/dist/voice/streaming/chunkedStreamAdapter.js +519 -0
  83. package/dist/voice/streaming/endpointer.d.ts +81 -0
  84. package/dist/voice/streaming/endpointer.js +247 -0
  85. package/dist/voice/streaming/localAgreement.d.ts +30 -0
  86. package/dist/voice/streaming/localAgreement.js +44 -0
  87. package/dist/voice/sttEnvCredentials.d.ts +12 -0
  88. package/dist/voice/sttEnvCredentials.js +12 -0
  89. package/dist/voice/transcribe.d.ts +47 -0
  90. package/dist/voice/transcribe.js +519 -0
  91. package/dist/voice/transcribeStream.d.ts +4 -0
  92. package/dist/voice/transcribeStream.js +584 -0
  93. package/dist/voice/whistle/assets.d.ts +47 -0
  94. package/dist/voice/whistle/assets.js +270 -0
  95. package/dist/voice/whistle/audio.d.ts +32 -0
  96. package/dist/voice/whistle/audio.js +267 -0
  97. package/dist/voice/whistle/engine.d.ts +32 -0
  98. package/dist/voice/whistle/engine.js +193 -0
  99. package/dist/voice/whistle/worker.d.ts +18 -0
  100. package/dist/voice/whistle/worker.js +294 -0
  101. package/docs-site/static/search-index.json +71 -12
  102. package/models/whistle/README.md +18 -0
  103. package/models/whistle/needle.js +2 -0
  104. package/models/whistle/needle.wasm +0 -0
  105. package/models/whistle/whistle.cact +0 -0
  106. 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 Whisper transcription request in milliseconds */
217
+ /** Timeout for one transcription request in milliseconds */
215
218
  TRANSCRIPTION_TIMEOUT_MS: 120_000,
216
219
  /**
217
- * Whisper's upload ceiling in MB. The extracted track is mono 16 kHz MP3,
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 Whisper call is reproduced here rather than shared with
857
- * `AudioProcessor`: extracting it into a common module is the better
858
- * long-term shape, but that file is being edited concurrently, and a
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
- const apiKey = process.env.OPENAI_API_KEY;
894
- if (!apiKey) {
895
- return skipped("OPENAI_API_KEY is not set, and Whisper is the only transcription backend wired up");
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
- const audioPath = join(tempDir, "audio.mp3");
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 Whisper resamples to anyway, and it keeps
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
- "-c:a",
915
- "libmp3lame",
916
- "-q:a",
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 Whisper's ` +
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 response = await withTimeout(fetch(`${baseUrl}/audio/transcriptions`, {
952
- method: "POST",
953
- headers: { Authorization: `Bearer ${apiKey}` },
954
- body: form,
955
- signal: abort.signal,
956
- }), VIDEO_CONFIG.TRANSCRIPTION_TIMEOUT_MS);
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 openai-whisper (${text.length} chars)`);
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.
@@ -24,6 +24,12 @@ const SUPPORTED_IMAGE_TYPES = new Set([
24
24
  */
25
25
  const TRAILING_REQUEST_ID = /\(([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\)\s*$/i;
26
26
  const MAX_ERROR_MESSAGE_CHARS = 500;
27
+ /**
28
+ * The code on the 429 an account gets once it has used its daily free
29
+ * allocation of 10,000 neurons (seen 2026-10-05). Unlike the capacity 429
30
+ * (3040), nothing changes within a retry's backoff, so it is not retried.
31
+ */
32
+ const DAILY_ALLOCATION_CODE = 4006;
27
33
  /**
28
34
  * An error message is cut to this before any pattern runs on it, so a hostile or
29
35
  * garbled body cannot hold the event loop. Cloudflare's own messages are far
@@ -317,7 +323,9 @@ export class CloudflareClefProvider extends SystemOneDecisionProvider {
317
323
  message,
318
324
  status,
319
325
  requestId: requestId ?? parsed?.requestId,
320
- retryable: kind === "rate_limit" || kind === "overloaded" || kind === "server",
326
+ retryable: (kind === "rate_limit" && parsed?.code !== DAILY_ALLOCATION_CODE) ||
327
+ kind === "overloaded" ||
328
+ kind === "server",
321
329
  };
322
330
  }
323
331
  /**
@@ -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
- origins: config.cors?.origins ?? ["*"],
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
  // ============================================
@@ -67,4 +67,5 @@ export declare class ExpressServerAdapter extends BaseServerAdapter {
67
67
  * Get the Express app instance
68
68
  */
69
69
  getFrameworkInstance(): unknown;
70
+ getNativeServer(): unknown;
70
71
  }
@@ -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
- origin: this.config.cors.origins,
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
- const statusCode = isServerAdapterError ? error.getHttpStatus() : 500;
251
- const errorCode = isServerAdapterError ? error.code : "INTERNAL_ERROR";
252
- const errorMessage = isServerAdapterError
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
  }
@@ -67,4 +67,5 @@ export declare class FastifyServerAdapter extends BaseServerAdapter {
67
67
  * Get the Fastify instance
68
68
  */
69
69
  getFrameworkInstance(): unknown;
70
+ getNativeServer(): unknown;
70
71
  }
@@ -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
- origin: this.config.cors.origins,
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
- origin: this.config.cors.origins,
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
  }
@@ -82,4 +82,5 @@ export declare class KoaServerAdapter extends BaseServerAdapter {
82
82
  * Get the Koa app instance
83
83
  */
84
84
  getFrameworkInstance(): unknown;
85
+ getNativeServer(): unknown;
85
86
  }
@@ -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
  }
@@ -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";