@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.
Files changed (194) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +1 -1
  3. package/dist/action/actionExecutor.d.ts +33 -3
  4. package/dist/action/actionExecutor.js +71 -15
  5. package/dist/action/actionInputs.js +4 -1
  6. package/dist/action/cliVersion.d.ts +25 -0
  7. package/dist/action/cliVersion.js +26 -0
  8. package/dist/action/index.d.ts +2 -1
  9. package/dist/action/index.js +2 -1
  10. package/dist/adapters/video/directorPipeline.js +46 -36
  11. package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
  12. package/dist/adapters/video/ffmpegAdapter.js +32 -5
  13. package/dist/adapters/video/frameExtractor.js +1 -1
  14. package/dist/adapters/video/videoMerger.js +5 -2
  15. package/dist/agent/arithmeticEvaluator.d.ts +32 -0
  16. package/dist/agent/arithmeticEvaluator.js +349 -0
  17. package/dist/agent/directTools.d.ts +6 -18
  18. package/dist/agent/directTools.js +17 -56
  19. package/dist/artifacts/artifactStore.d.ts +22 -1
  20. package/dist/artifacts/artifactStore.js +66 -6
  21. package/dist/auth/anthropicOAuth.d.ts +26 -1
  22. package/dist/auth/anthropicOAuth.js +81 -5
  23. package/dist/auth/secureFiles.d.ts +37 -0
  24. package/dist/auth/secureFiles.js +95 -0
  25. package/dist/auth/tokenStore.d.ts +43 -6
  26. package/dist/auth/tokenStore.js +260 -79
  27. package/dist/avatar/providers/DIDAvatar.js +49 -45
  28. package/dist/avatar/providers/ReplicateAvatar.js +33 -40
  29. package/dist/browser/neurolink.min.js +485 -558
  30. package/dist/cli/commands/auth.js +112 -70
  31. package/dist/cli/commands/proxy.d.ts +4 -0
  32. package/dist/cli/commands/proxy.js +128 -13
  33. package/dist/cli/commands/serve.d.ts +10 -1
  34. package/dist/cli/commands/serve.js +98 -17
  35. package/dist/cli/commands/server.js +17 -5
  36. package/dist/cli/index.js +146 -5
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/utils/envManager.js +17 -3
  39. package/dist/cli/utils/serverUtils.d.ts +39 -3
  40. package/dist/cli/utils/serverUtils.js +63 -6
  41. package/dist/core/baseProvider.js +7 -1
  42. package/dist/core/modules/MessageBuilder.js +5 -0
  43. package/dist/core/modules/ToolsManager.d.ts +46 -1
  44. package/dist/core/modules/ToolsManager.js +190 -43
  45. package/dist/files/fileReferenceRegistry.d.ts +2 -23
  46. package/dist/files/fileReferenceRegistry.js +55 -5
  47. package/dist/hitl/hitlManager.js +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +6 -1
  50. package/dist/mcp/auth/tokenStorage.d.ts +7 -0
  51. package/dist/mcp/auth/tokenStorage.js +23 -4
  52. package/dist/mcp/externalServerManager.d.ts +13 -3
  53. package/dist/mcp/externalServerManager.js +154 -136
  54. package/dist/mcp/index.d.ts +2 -0
  55. package/dist/mcp/index.js +3 -0
  56. package/dist/mcp/mcpClientFactory.d.ts +14 -0
  57. package/dist/mcp/mcpClientFactory.js +70 -7
  58. package/dist/mcp/projectConfigTrust.d.ts +72 -0
  59. package/dist/mcp/projectConfigTrust.js +237 -0
  60. package/dist/mcp/transportUrlPolicy.d.ts +39 -0
  61. package/dist/mcp/transportUrlPolicy.js +172 -0
  62. package/dist/music/providers/BeatovenMusic.js +35 -46
  63. package/dist/music/providers/ReplicateMusic.js +33 -40
  64. package/dist/neurolink.d.ts +16 -1
  65. package/dist/neurolink.js +64 -11
  66. package/dist/processors/archive/ArchiveProcessor.js +15 -1
  67. package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
  68. package/dist/processors/archive/zipEntryIndex.js +33 -0
  69. package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
  70. package/dist/processors/base/BaseFileProcessor.js +40 -111
  71. package/dist/processors/data/YamlProcessor.d.ts +13 -1
  72. package/dist/processors/data/YamlProcessor.js +93 -2
  73. package/dist/processors/document/OpenDocumentProcessor.js +7 -2
  74. package/dist/processors/document/PptxProcessor.js +12 -7
  75. package/dist/processors/media/VideoProcessor.js +93 -15
  76. package/dist/proxy/inboundAuth.d.ts +89 -0
  77. package/dist/proxy/inboundAuth.js +219 -0
  78. package/dist/proxy/tokenRefresh.d.ts +6 -0
  79. package/dist/proxy/tokenRefresh.js +13 -1
  80. package/dist/proxy/updateState.js +4 -4
  81. package/dist/rag/document/loaders.d.ts +0 -6
  82. package/dist/rag/document/loaders.js +16 -10
  83. package/dist/rag/ragIntegration.d.ts +2 -2
  84. package/dist/rag/ragIntegration.js +35 -10
  85. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  86. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  87. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  88. package/dist/server/abstract/baseServerAdapter.js +119 -6
  89. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  90. package/dist/server/adapters/expressAdapter.js +83 -8
  91. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  92. package/dist/server/adapters/fastifyAdapter.js +165 -70
  93. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  94. package/dist/server/adapters/honoAdapter.js +99 -21
  95. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  96. package/dist/server/adapters/koaAdapter.js +153 -74
  97. package/dist/server/index.d.ts +2 -0
  98. package/dist/server/index.js +4 -0
  99. package/dist/server/middleware/auth.js +6 -2
  100. package/dist/server/middleware/cache.js +6 -1
  101. package/dist/server/middleware/rateLimit.js +29 -4
  102. package/dist/server/routes/agentRoutes.js +24 -11
  103. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  104. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  105. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  106. package/dist/server/routes/codexProxyRoutes.js +125 -40
  107. package/dist/server/routes/index.d.ts +4 -0
  108. package/dist/server/routes/index.js +35 -4
  109. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  110. package/dist/server/routes/mcpRoutes.js +21 -7
  111. package/dist/server/routes/memoryRoutes.js +7 -6
  112. package/dist/server/routes/toolRoutes.d.ts +12 -2
  113. package/dist/server/routes/toolRoutes.js +33 -3
  114. package/dist/server/routes/transcribeRoutes.js +60 -13
  115. package/dist/server/utils/clientAddress.d.ts +23 -0
  116. package/dist/server/utils/clientAddress.js +65 -0
  117. package/dist/server/utils/clientError.d.ts +35 -0
  118. package/dist/server/utils/clientError.js +129 -0
  119. package/dist/server/utils/expressBody.d.ts +33 -0
  120. package/dist/server/utils/expressBody.js +353 -0
  121. package/dist/server/utils/pathMatch.d.ts +21 -0
  122. package/dist/server/utils/pathMatch.js +41 -0
  123. package/dist/server/utils/validation.d.ts +9 -0
  124. package/dist/server/utils/validation.js +9 -0
  125. package/dist/server/websocket/WebSocketHandler.js +22 -3
  126. package/dist/server/websocket/frameQueue.d.ts +19 -0
  127. package/dist/server/websocket/frameQueue.js +41 -3
  128. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  129. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  130. package/dist/skills/skillStores.js +5 -0
  131. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  132. package/dist/tasks/store/fileTaskStore.js +11 -4
  133. package/dist/types/attachments.d.ts +117 -0
  134. package/dist/types/attachments.js +15 -0
  135. package/dist/types/auth.d.ts +6 -0
  136. package/dist/types/cli.d.ts +4 -0
  137. package/dist/types/config.d.ts +56 -0
  138. package/dist/types/file.d.ts +8 -0
  139. package/dist/types/hitl.d.ts +8 -0
  140. package/dist/types/index.d.ts +1 -0
  141. package/dist/types/index.js +1 -0
  142. package/dist/types/mcp.d.ts +98 -0
  143. package/dist/types/multimodal.js +0 -41
  144. package/dist/types/processor.d.ts +14 -1
  145. package/dist/types/proxy.d.ts +62 -3
  146. package/dist/types/rag.d.ts +9 -0
  147. package/dist/types/safeFetch.d.ts +40 -0
  148. package/dist/types/server.d.ts +255 -10
  149. package/dist/types/tools.d.ts +57 -0
  150. package/dist/types/transcribe.d.ts +6 -0
  151. package/dist/utils/attachmentPolicy.d.ts +89 -0
  152. package/dist/utils/attachmentPolicy.js +327 -0
  153. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  154. package/dist/utils/dotenvBootstrap.js +172 -31
  155. package/dist/utils/fileDetector.js +115 -65
  156. package/dist/utils/json/coerce.js +11 -1
  157. package/dist/utils/logSanitize.d.ts +5 -0
  158. package/dist/utils/logSanitize.js +8 -0
  159. package/dist/utils/logger.js +73 -1
  160. package/dist/utils/messageBuilder.d.ts +2 -2
  161. package/dist/utils/messageBuilder.js +118 -105
  162. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  163. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  164. package/dist/utils/redis.d.ts +12 -0
  165. package/dist/utils/redis.js +72 -0
  166. package/dist/utils/safeFetch.d.ts +22 -1
  167. package/dist/utils/safeFetch.js +132 -38
  168. package/dist/utils/safeStoreId.d.ts +19 -0
  169. package/dist/utils/safeStoreId.js +29 -0
  170. package/dist/utils/schemaConversion.d.ts +30 -3
  171. package/dist/utils/schemaConversion.js +457 -84
  172. package/dist/utils/sizeGuard.js +2 -2
  173. package/dist/utils/ssrfGuard.d.ts +3 -3
  174. package/dist/utils/ssrfGuard.js +71 -39
  175. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  176. package/dist/voice/eval/sttEvalStore.js +77 -12
  177. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  178. package/dist/voice/livekit/agentDefinition.js +22 -0
  179. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  180. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  181. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  182. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  183. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  184. package/dist/voice/livekit/voiceAgent.js +2 -1
  185. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  186. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  187. package/dist/voice/providers/GeminiLive.js +21 -6
  188. package/dist/voice/providers/GoogleSTT.js +8 -5
  189. package/dist/voice/transcribe.d.ts +2 -2
  190. package/dist/voice/transcribe.js +34 -18
  191. package/dist/voice/whistle/audio.js +11 -5
  192. package/docs-site/static/search-index.json +75 -52
  193. package/package.json +171 -156
  194. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Bounded JSON and form body reads for the Express adapter.
3
+ *
4
+ * `express.json()` and `express.urlencoded()` (body-parser) refuse a body over
5
+ * their limit with a 413, but only after the whole request has been taken off
6
+ * the socket: a declared Content-Length over the limit is seen at once and the
7
+ * body is still drained, and a chunked body is read to its end. Nothing has
8
+ * authenticated the request at that point, so a client could make the server
9
+ * receive any amount of data for the price of one 413.
10
+ *
11
+ * `createBoundedBodyParser` wraps a body-parser middleware so the bytes are
12
+ * read here, up to the limit, and handed to that same middleware to parse. Which
13
+ * requests are parsed, charset handling, strict JSON, the qs options and the
14
+ * error each failure carries all stay body-parser's, because body-parser still
15
+ * makes those decisions: it is asked first, from the headers alone, whether it
16
+ * would read the request, and then parses the bytes it is given. What changes is
17
+ * who reads the socket and what happens at the limit. A body over the limit is
18
+ * refused without reading it (declared Content-Length) or at the limit (chunked,
19
+ * or a compressed body that inflates past it), and the connection is closed so
20
+ * the client stops sending.
21
+ *
22
+ * `express.raw()` (uploads) is not wrapped.
23
+ *
24
+ * @module server/utils/expressBody
25
+ */
26
+ import { Readable } from "node:stream";
27
+ import * as zlib from "node:zlib";
28
+ import { tryParseByteLimit } from "./rawBody.js";
29
+ const noop = () => undefined;
30
+ /** The error body-parser raises for an over-limit body, which the adapter's error handler answers 413. */
31
+ function bodyParserError(status, message, type, details = {}) {
32
+ return Object.assign(new Error(message), {
33
+ status,
34
+ statusCode: status,
35
+ expose: status < 500,
36
+ type,
37
+ ...details,
38
+ });
39
+ }
40
+ const entityTooLarge = (limit, received) => bodyParserError(413, "request entity too large", "entity.too.large", {
41
+ limit,
42
+ ...(received === undefined ? {} : { received }),
43
+ });
44
+ /** A Content-Encoding body-parser inflates, and nothing else. */
45
+ function decompressorFor(encoding) {
46
+ switch (encoding) {
47
+ case "gzip":
48
+ return zlib.createGunzip;
49
+ case "deflate":
50
+ return zlib.createInflate;
51
+ case "br":
52
+ return zlib.createBrotliDecompress;
53
+ case "zstd":
54
+ // Node 22.15 and later.
55
+ return typeof zlib.createZstdDecompress === "function"
56
+ ? zlib.createZstdDecompress
57
+ : undefined;
58
+ default:
59
+ return undefined;
60
+ }
61
+ }
62
+ /** body-parser's own "already handled" test: the request has been read off, or cannot be. */
63
+ const isRequestFinished = (req) => ("upgrade" in req && req.upgrade === true) ||
64
+ !req.socket?.readable ||
65
+ (req.complete && !req.readable);
66
+ /** A request that is already in memory, to hand a body-parser middleware in place of the socket. */
67
+ function bufferedRequest(headers, bytes, body) {
68
+ let pushed = false;
69
+ const stream = new Readable({
70
+ read() {
71
+ if (!pushed) {
72
+ pushed = true;
73
+ if (bytes.length > 0) {
74
+ this.push(bytes);
75
+ }
76
+ this.push(null);
77
+ }
78
+ },
79
+ });
80
+ return Object.assign(stream, { headers, body });
81
+ }
82
+ /**
83
+ * Ask a body-parser middleware what it would do with this request, without
84
+ * giving it a body. It is run on an empty one: what it decides from the headers
85
+ * (not a body, not its type, a charset it refuses) comes back as it would
86
+ * have, and so does what it only finds out when it starts to read (a charset
87
+ * the decoder lacks). The Content-Encoding is left off because inflating is
88
+ * done here, and the Content-Length is zeroed so the declared size, which is
89
+ * checked here, does not stand in the way.
90
+ */
91
+ function askParser(parser, req, res) {
92
+ const headers = { ...req.headers };
93
+ delete headers["content-encoding"];
94
+ delete headers["transfer-encoding"];
95
+ if (req.headers["transfer-encoding"] !== undefined ||
96
+ req.headers["content-length"] !== undefined) {
97
+ headers["content-length"] = "0";
98
+ }
99
+ const before = "body" in req ? req.body : undefined;
100
+ const probe = bufferedRequest(headers, Buffer.alloc(0), before);
101
+ return new Promise((resolve) => {
102
+ let synchronous = true;
103
+ parser(probe, res, (error) => {
104
+ if (error !== undefined) {
105
+ resolve({ kind: "reject", error, beforeRead: synchronous });
106
+ }
107
+ else if (probe.body !== before) {
108
+ resolve({ kind: "parse" });
109
+ }
110
+ else {
111
+ resolve({ kind: "skip", body: probe.body });
112
+ }
113
+ });
114
+ synchronous = false;
115
+ });
116
+ }
117
+ /**
118
+ * How far past the limit the raw bytes of a compressed body may run. The limit
119
+ * is on what a body inflates to, but the bytes on the wire are capped too:
120
+ * empty gzip members inflate to nothing, and so do the zeros Node lets follow a
121
+ * complete gzip stream, so without a cap a stream of either is read until the
122
+ * client stops. The slack covers the framing a real encoder adds (deflate's
123
+ * stored blocks carry 5 bytes per 64 KiB).
124
+ */
125
+ const COMPRESSED_WIRE_SLACK = 64 * 1024;
126
+ /**
127
+ * Read a request body, refusing more than `maxBytes` (of what is inflated, when
128
+ * the request is compressed, with the raw bytes capped at `maxBytes` plus
129
+ * {@link COMPRESSED_WIRE_SLACK}). Reading stops at the limit and the request is
130
+ * left paused, not drained. Events rather than `for await`: leaving an async
131
+ * iterator early destroys the request, and its socket with it, before an answer
132
+ * can be sent.
133
+ */
134
+ function readBounded(req, maxBytes, decompressor) {
135
+ return new Promise((resolve, reject) => {
136
+ const source = decompressor ?? req;
137
+ const chunks = [];
138
+ const wireCap = maxBytes + COMPRESSED_WIRE_SLACK;
139
+ let received = 0;
140
+ let wire = 0;
141
+ // A compressed body is complete; the rest of the request is being read off.
142
+ let draining = false;
143
+ let settled = false;
144
+ const release = () => {
145
+ source.off("data", onData);
146
+ source.off("end", onEnd);
147
+ source.off("error", onFail);
148
+ req.off("data", onWire);
149
+ req.off("end", onRequestEnd);
150
+ req.off("error", onFail);
151
+ req.off("close", onClose);
152
+ };
153
+ const detachDecompressor = () => {
154
+ if (decompressor) {
155
+ req.unpipe(decompressor);
156
+ decompressor.on("error", noop);
157
+ decompressor.destroy();
158
+ }
159
+ };
160
+ const stop = () => {
161
+ detachDecompressor();
162
+ req.pause();
163
+ };
164
+ const settle = (outcome) => {
165
+ if (!settled) {
166
+ settled = true;
167
+ release();
168
+ outcome();
169
+ }
170
+ };
171
+ const finish = (closeConnection) => {
172
+ settle(() => resolve({ bytes: Buffer.concat(chunks), closeConnection }));
173
+ };
174
+ const aborted = () => bodyParserError(400, "request aborted", "request.aborted", {
175
+ code: "ECONNABORTED",
176
+ received,
177
+ });
178
+ function onData(chunk) {
179
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
180
+ received += buffer.length;
181
+ if (received > maxBytes) {
182
+ settle(() => {
183
+ stop();
184
+ reject(entityTooLarge(maxBytes, received));
185
+ });
186
+ return;
187
+ }
188
+ chunks.push(buffer);
189
+ }
190
+ function onWire(chunk) {
191
+ wire += chunk.length;
192
+ if (wire <= wireCap) {
193
+ return;
194
+ }
195
+ if (draining) {
196
+ // The body is whole; what keeps arriving is not part of it.
197
+ req.pause();
198
+ finish(true);
199
+ return;
200
+ }
201
+ settle(() => {
202
+ stop();
203
+ reject(entityTooLarge(maxBytes, wire));
204
+ });
205
+ }
206
+ function onEnd() {
207
+ if (!decompressor) {
208
+ finish(false);
209
+ return;
210
+ }
211
+ // The rest of the request, if any, is not part of the body: read it off
212
+ // up to the cap, so the connection can be kept.
213
+ detachDecompressor();
214
+ if (req.complete) {
215
+ req.resume();
216
+ finish(false);
217
+ return;
218
+ }
219
+ draining = true;
220
+ source.off("data", onData);
221
+ source.off("end", onEnd);
222
+ source.off("error", onFail);
223
+ req.on("end", onRequestEnd);
224
+ req.resume();
225
+ }
226
+ function onRequestEnd() {
227
+ finish(false);
228
+ }
229
+ function onFail(error) {
230
+ if (draining) {
231
+ finish(true);
232
+ return;
233
+ }
234
+ settle(() => {
235
+ stop();
236
+ reject(error);
237
+ });
238
+ }
239
+ function onClose() {
240
+ // A request that arrived whole closes once it has been read, which for a
241
+ // compressed one can be before the inflating has finished.
242
+ if (req.complete) {
243
+ return;
244
+ }
245
+ if (draining) {
246
+ finish(true);
247
+ return;
248
+ }
249
+ settle(() => {
250
+ stop();
251
+ reject(aborted());
252
+ });
253
+ }
254
+ if (req.destroyed && !req.complete) {
255
+ settle(() => reject(aborted()));
256
+ return;
257
+ }
258
+ source.on("data", onData);
259
+ source.on("end", onEnd);
260
+ source.on("error", onFail);
261
+ req.on("close", onClose);
262
+ if (decompressor) {
263
+ req.on("error", onFail);
264
+ req.on("data", onWire);
265
+ req.pipe(decompressor);
266
+ }
267
+ });
268
+ }
269
+ /** Answer the request with `error` and close the connection, so a client still sending stops. */
270
+ function refuse(res, next, error) {
271
+ if (!res.headersSent) {
272
+ res.setHeader("Connection", "close");
273
+ }
274
+ next(error);
275
+ }
276
+ async function parseBounded(parser, maxBytes, req, res, next) {
277
+ try {
278
+ if (isRequestFinished(req)) {
279
+ next();
280
+ return;
281
+ }
282
+ const verdict = await askParser(parser, req, res);
283
+ if (verdict.kind === "skip") {
284
+ req.body = verdict.body;
285
+ next();
286
+ return;
287
+ }
288
+ if (verdict.kind === "reject" && verdict.beforeRead) {
289
+ refuse(res, next, verdict.error);
290
+ return;
291
+ }
292
+ const encoding = (req.headers["content-encoding"] || "identity").toLowerCase();
293
+ const inflate = encoding === "identity" ? undefined : decompressorFor(encoding);
294
+ if (encoding !== "identity" && !inflate) {
295
+ refuse(res, next, bodyParserError(415, `unsupported content encoding "${encoding}"`, "encoding.unsupported", { encoding }));
296
+ return;
297
+ }
298
+ // Compressed, the declared length is the compressed size, which says
299
+ // nothing about the body.
300
+ const declared = Number.parseInt(String(req.headers["content-length"]), 10);
301
+ if (!inflate && declared > maxBytes) {
302
+ refuse(res, next, entityTooLarge(maxBytes));
303
+ return;
304
+ }
305
+ if (verdict.kind === "reject") {
306
+ refuse(res, next, verdict.error);
307
+ return;
308
+ }
309
+ const before = "body" in req ? req.body : undefined;
310
+ let read;
311
+ try {
312
+ read = await readBounded(req, maxBytes, inflate?.());
313
+ }
314
+ catch (error) {
315
+ refuse(res, next, error);
316
+ return;
317
+ }
318
+ if (read.closeConnection && !res.headersSent) {
319
+ res.setHeader("Connection", "close");
320
+ }
321
+ const { bytes } = read;
322
+ const headers = { ...req.headers, "content-length": String(bytes.length) };
323
+ delete headers["content-encoding"];
324
+ delete headers["transfer-encoding"];
325
+ const replay = bufferedRequest(headers, bytes, before);
326
+ parser(replay, res, (error) => {
327
+ if (error !== undefined) {
328
+ next(error);
329
+ return;
330
+ }
331
+ req.body = replay.body;
332
+ next();
333
+ });
334
+ }
335
+ catch (error) {
336
+ next(error);
337
+ }
338
+ }
339
+ /**
340
+ * Wrap a body-parser middleware (`express.json()`, `express.urlencoded()`) so a
341
+ * body is read only as far as `limit`. `limit` is the one the middleware was
342
+ * built with. A limit that cannot be read as a byte count here is left to the
343
+ * middleware alone.
344
+ */
345
+ export function createBoundedBodyParser(parser, limit) {
346
+ const maxBytes = tryParseByteLimit(limit);
347
+ if (maxBytes === undefined) {
348
+ return parser;
349
+ }
350
+ return (req, res, next) => {
351
+ void parseBounded(parser, maxBytes, req, res, next);
352
+ };
353
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Path matching shared by the adapters' middleware `paths` / `excludePaths`
3
+ * and the rate limiters' `skipPaths`.
4
+ *
5
+ * Every match is at a segment boundary: a pattern names the path itself and
6
+ * what lies under it as a directory would, so `/api/health` covers
7
+ * `/api/health` and `/api/health/live` but not `/api/healthz`. The plain
8
+ * prefix test the adapters used to run let the second be exempted by the
9
+ * first.
10
+ *
11
+ * @module server/utils/pathMatch
12
+ */
13
+ /** The request path without its query string or fragment. */
14
+ export declare function stripQuery(path: string): string;
15
+ /**
16
+ * `true` when `path` is `prefix`, or lies under `prefix` at a segment
17
+ * boundary. `"*"`, `""` and `"/"` match every path.
18
+ */
19
+ export declare function pathStartsWithSegment(path: string, prefix: string): boolean;
20
+ /** `true` when any of `prefixes` matches `path` (see {@link pathStartsWithSegment}). */
21
+ export declare function pathMatchesAny(path: string, prefixes: readonly string[] | undefined): boolean;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Path matching shared by the adapters' middleware `paths` / `excludePaths`
3
+ * and the rate limiters' `skipPaths`.
4
+ *
5
+ * Every match is at a segment boundary: a pattern names the path itself and
6
+ * what lies under it as a directory would, so `/api/health` covers
7
+ * `/api/health` and `/api/health/live` but not `/api/healthz`. The plain
8
+ * prefix test the adapters used to run let the second be exempted by the
9
+ * first.
10
+ *
11
+ * @module server/utils/pathMatch
12
+ */
13
+ /** The request path without its query string or fragment. */
14
+ export function stripQuery(path) {
15
+ const cut = path.search(/[?#]/);
16
+ return cut === -1 ? path : path.slice(0, cut);
17
+ }
18
+ /** Collapse repeated slashes and drop a trailing one, keeping the root `/`. */
19
+ function normalize(path) {
20
+ const collapsed = path.replace(/\/{2,}/g, "/");
21
+ return collapsed.length > 1 ? collapsed.replace(/\/+$/, "") : collapsed;
22
+ }
23
+ /**
24
+ * `true` when `path` is `prefix`, or lies under `prefix` at a segment
25
+ * boundary. `"*"`, `""` and `"/"` match every path.
26
+ */
27
+ export function pathStartsWithSegment(path, prefix) {
28
+ if (prefix === "*" || prefix === "" || prefix === "/") {
29
+ return true;
30
+ }
31
+ const target = normalize(stripQuery(path));
32
+ const base = normalize(prefix);
33
+ return target === base || target.startsWith(`${base}/`);
34
+ }
35
+ /** `true` when any of `prefixes` matches `path` (see {@link pathStartsWithSegment}). */
36
+ export function pathMatchesAny(path, prefixes) {
37
+ if (!prefixes || prefixes.length === 0) {
38
+ return false;
39
+ }
40
+ return prefixes.some((prefix) => pathStartsWithSegment(path, prefix));
41
+ }
@@ -57,6 +57,15 @@ export declare const WebSocketAgentRequestSchema: z.ZodObject<{
57
57
  maxSteps: z.ZodOptional<z.ZodNumber>;
58
58
  }, z.core.$strip>>;
59
59
  }, z.core.$strip>;
60
+ /**
61
+ * WebSocket `tool_call` message payload schema: a tool name the registry can
62
+ * look up and a JSON object of arguments. The payload used to reach
63
+ * `executeTool` unchecked.
64
+ */
65
+ export declare const WebSocketToolCallSchema: z.ZodObject<{
66
+ toolName: z.ZodString;
67
+ args: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
68
+ }, z.core.$strip>;
60
69
  /**
61
70
  * Tool execute request schema
62
71
  */
@@ -55,6 +55,15 @@ export const WebSocketAgentRequestSchema = z.object({
55
55
  prompt: z.string().min(1, "Prompt is required"),
56
56
  options: WebSocketAgentOptionsSchema.optional(),
57
57
  });
58
+ /**
59
+ * WebSocket `tool_call` message payload schema: a tool name the registry can
60
+ * look up and a JSON object of arguments. The payload used to reach
61
+ * `executeTool` unchecked.
62
+ */
63
+ export const WebSocketToolCallSchema = z.object({
64
+ toolName: z.string().min(1, "toolName is required").max(256),
65
+ args: z.record(z.string(), z.unknown()).optional().default({}),
66
+ });
58
67
  /**
59
68
  * Tool execute request schema
60
69
  */
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { WebSocketError, WebSocketConnectionError } from "../errors.js";
8
8
  import { logger } from "../../utils/logger.js";
9
- import { WebSocketAgentRequestSchema } from "../utils/validation.js";
9
+ import { WebSocketAgentRequestSchema, WebSocketToolCallSchema, } from "../utils/validation.js";
10
10
  /**
11
11
  * Default WebSocket configuration
12
12
  */
@@ -36,7 +36,20 @@ export class WebSocketConnectionManager {
36
36
  pingIntervals = new Map();
37
37
  handlers = new Map();
38
38
  constructor(config = {}) {
39
- this.config = { ...DEFAULT_CONFIG, ...config };
39
+ // A strategy other than "none" means the deployer wants callers
40
+ // identified, so `required` defaults to true with it: a config that named
41
+ // a strategy and forgot `required` used to get an open socket.
42
+ const strategy = config.auth?.strategy ?? DEFAULT_CONFIG.auth.strategy;
43
+ this.config = {
44
+ ...DEFAULT_CONFIG,
45
+ ...config,
46
+ auth: {
47
+ ...DEFAULT_CONFIG.auth,
48
+ ...config.auth,
49
+ strategy,
50
+ required: config.auth?.required ?? strategy !== "none",
51
+ },
52
+ };
40
53
  }
41
54
  /**
42
55
  * Register a handler for a path
@@ -498,7 +511,13 @@ export function createAgentWebSocketHandler(neurolink) {
498
511
  return clientPresent ? { type: "stream_complete" } : undefined;
499
512
  });
500
513
  router.route("tool_call", async (connection, payload) => {
501
- const { toolName, args } = payload;
514
+ const parsed = WebSocketToolCallSchema.safeParse(payload);
515
+ if (!parsed.success) {
516
+ throw new WebSocketError(`Invalid tool_call payload: ${parsed.error.issues
517
+ .map((issue) => `${issue.path.join(".") || "payload"} ${issue.message}`)
518
+ .join("; ")}`, undefined, connection.id);
519
+ }
520
+ const { toolName, args } = parsed.data;
502
521
  const result = await neurolink.executeTool(toolName, args);
503
522
  return { type: "tool_result", toolName, result };
504
523
  });
@@ -2,15 +2,34 @@
2
2
  * Push-driven async iterable shared by the transcription sockets: socket
3
3
  * frames are pushed in, `transcribeStream()` pulls them out.
4
4
  *
5
+ * The queue reports its level to whoever feeds it, so a socket can be paused
6
+ * when audio arrives faster than the engine takes it and resumed once the
7
+ * backlog drains (TCP backpressure, instead of buffering the whole burst).
8
+ * Every queue also charges its bytes to one process-wide budget, so a burst
9
+ * of uploads across many sockets cannot exhaust the process.
10
+ *
5
11
  * @module server/websocket/frameQueue
6
12
  */
13
+ /** What every queue in the process holds right now, for caps and tests. */
14
+ export declare function totalQueuedFrameBytes(): number;
7
15
  /** Push-driven async iterable: socket frames in, `transcribeStream()` pulls. */
8
16
  export declare class FrameQueue implements AsyncIterable<Buffer> {
9
17
  private readonly frames;
10
18
  private waiting;
11
19
  private ended;
20
+ private onLevel;
12
21
  queuedBytes: number;
22
+ /** Called after every change of `queuedBytes`, with the new level. */
23
+ setLevelListener(listener: ((queuedBytes: number) => void) | undefined): void;
13
24
  push(frame: Buffer): void;
25
+ /**
26
+ * No more frames will arrive; the ones already queued are still handed
27
+ * out, so a recording whose `end` frame followed its audio is transcribed
28
+ * whole. The consumer's `return()` (see below) is what discards.
29
+ */
14
30
  end(): void;
31
+ /** The consumer is gone: whatever it did not pull leaves the process budget. */
32
+ private discard;
33
+ private charge;
15
34
  [Symbol.asyncIterator](): AsyncIterator<Buffer>;
16
35
  }
@@ -2,14 +2,31 @@
2
2
  * Push-driven async iterable shared by the transcription sockets: socket
3
3
  * frames are pushed in, `transcribeStream()` pulls them out.
4
4
  *
5
+ * The queue reports its level to whoever feeds it, so a socket can be paused
6
+ * when audio arrives faster than the engine takes it and resumed once the
7
+ * backlog drains (TCP backpressure, instead of buffering the whole burst).
8
+ * Every queue also charges its bytes to one process-wide budget, so a burst
9
+ * of uploads across many sockets cannot exhaust the process.
10
+ *
5
11
  * @module server/websocket/frameQueue
6
12
  */
13
+ /** Queued bytes across every `FrameQueue` of this process. */
14
+ let processQueuedBytes = 0;
15
+ /** What every queue in the process holds right now, for caps and tests. */
16
+ export function totalQueuedFrameBytes() {
17
+ return processQueuedBytes;
18
+ }
7
19
  /** Push-driven async iterable: socket frames in, `transcribeStream()` pulls. */
8
20
  export class FrameQueue {
9
21
  frames = [];
10
22
  waiting;
11
23
  ended = false;
24
+ onLevel;
12
25
  queuedBytes = 0;
26
+ /** Called after every change of `queuedBytes`, with the new level. */
27
+ setLevelListener(listener) {
28
+ this.onLevel = listener;
29
+ }
13
30
  push(frame) {
14
31
  if (this.ended) {
15
32
  return;
@@ -21,9 +38,17 @@ export class FrameQueue {
21
38
  return;
22
39
  }
23
40
  this.frames.push(frame);
24
- this.queuedBytes += frame.length;
41
+ this.charge(frame.length);
25
42
  }
43
+ /**
44
+ * No more frames will arrive; the ones already queued are still handed
45
+ * out, so a recording whose `end` frame followed its audio is transcribed
46
+ * whole. The consumer's `return()` (see below) is what discards.
47
+ */
26
48
  end() {
49
+ if (this.ended) {
50
+ return;
51
+ }
27
52
  this.ended = true;
28
53
  if (this.waiting) {
29
54
  const resolve = this.waiting;
@@ -31,12 +56,25 @@ export class FrameQueue {
31
56
  resolve({ value: undefined, done: true });
32
57
  }
33
58
  }
59
+ /** The consumer is gone: whatever it did not pull leaves the process budget. */
60
+ discard() {
61
+ this.end();
62
+ if (this.queuedBytes > 0) {
63
+ this.charge(-this.queuedBytes);
64
+ }
65
+ this.frames.length = 0;
66
+ }
67
+ charge(delta) {
68
+ this.queuedBytes += delta;
69
+ processQueuedBytes = Math.max(0, processQueuedBytes + delta);
70
+ this.onLevel?.(this.queuedBytes);
71
+ }
34
72
  [Symbol.asyncIterator]() {
35
73
  return {
36
74
  next: () => {
37
75
  const frame = this.frames.shift();
38
76
  if (frame) {
39
- this.queuedBytes -= frame.length;
77
+ this.charge(-frame.length);
40
78
  return Promise.resolve({ value: frame, done: false });
41
79
  }
42
80
  if (this.ended) {
@@ -47,7 +85,7 @@ export class FrameQueue {
47
85
  });
48
86
  },
49
87
  return: () => {
50
- this.end();
88
+ this.discard();
51
89
  return Promise.resolve({ value: undefined, done: true });
52
90
  },
53
91
  };
@@ -24,7 +24,14 @@
24
24
  * to change them. Listeners receive every event of the speakers they follow,
25
25
  * tagged with `speaker`, and only the `output` events of their languages; a
26
26
  * session renders each utterance once into the union of its listeners'
27
- * languages. A session lives while a speaker or a listener is connected.
27
+ * languages. A session lives while a speaker or a listener is connected. The
28
+ * speaker that opens a session may add `&secret=<value>`; every later speaker
29
+ * and listener must then present the same secret, so a session id alone does
30
+ * not admit a stranger (`requireSessionSecret` makes the secret mandatory).
31
+ *
32
+ * Limits: sockets are capped in all and per remote address (503 before the
33
+ * handshake), a socket that sends faster than the engine takes is paused at a
34
+ * high-water mark, and queued audio is budgeted across the process.
28
35
  *
29
36
  * OpenAI Realtime transcription (default path `/v1/realtime`,
30
37
  * `?intent=transcription`): the same pipeline behind OpenAI's protocol, for