@juspay/neurolink 13.8.16 → 13.8.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/README.md +1 -1
- package/dist/action/actionExecutor.d.ts +33 -3
- package/dist/action/actionExecutor.js +71 -15
- package/dist/action/actionInputs.js +4 -1
- package/dist/action/cliVersion.d.ts +25 -0
- package/dist/action/cliVersion.js +26 -0
- package/dist/action/index.d.ts +2 -1
- package/dist/action/index.js +2 -1
- package/dist/adapters/video/directorPipeline.js +46 -36
- package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
- package/dist/adapters/video/ffmpegAdapter.js +32 -5
- package/dist/adapters/video/frameExtractor.js +1 -1
- package/dist/adapters/video/videoMerger.js +5 -2
- package/dist/agent/arithmeticEvaluator.d.ts +32 -0
- package/dist/agent/arithmeticEvaluator.js +349 -0
- package/dist/agent/directTools.d.ts +6 -18
- package/dist/agent/directTools.js +17 -56
- package/dist/artifacts/artifactStore.d.ts +22 -1
- package/dist/artifacts/artifactStore.js +66 -6
- package/dist/auth/anthropicOAuth.d.ts +26 -1
- package/dist/auth/anthropicOAuth.js +81 -5
- package/dist/auth/secureFiles.d.ts +37 -0
- package/dist/auth/secureFiles.js +95 -0
- package/dist/auth/tokenStore.d.ts +43 -6
- package/dist/auth/tokenStore.js +260 -79
- package/dist/avatar/providers/DIDAvatar.js +49 -45
- package/dist/avatar/providers/ReplicateAvatar.js +33 -40
- package/dist/browser/neurolink.min.js +485 -558
- package/dist/cli/commands/auth.js +112 -70
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +128 -13
- package/dist/cli/commands/serve.d.ts +10 -1
- package/dist/cli/commands/serve.js +98 -17
- package/dist/cli/commands/server.js +17 -5
- package/dist/cli/index.js +146 -5
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/envManager.js +17 -3
- package/dist/cli/utils/serverUtils.d.ts +39 -3
- package/dist/cli/utils/serverUtils.js +63 -6
- package/dist/core/baseProvider.js +7 -1
- package/dist/core/modules/MessageBuilder.js +5 -0
- package/dist/core/modules/ToolsManager.d.ts +46 -1
- package/dist/core/modules/ToolsManager.js +190 -43
- package/dist/files/fileReferenceRegistry.d.ts +2 -23
- package/dist/files/fileReferenceRegistry.js +55 -5
- package/dist/hitl/hitlManager.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/mcp/auth/tokenStorage.d.ts +7 -0
- package/dist/mcp/auth/tokenStorage.js +23 -4
- package/dist/mcp/externalServerManager.d.ts +13 -3
- package/dist/mcp/externalServerManager.js +154 -136
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/mcpClientFactory.d.ts +14 -0
- package/dist/mcp/mcpClientFactory.js +70 -7
- package/dist/mcp/projectConfigTrust.d.ts +72 -0
- package/dist/mcp/projectConfigTrust.js +237 -0
- package/dist/mcp/transportUrlPolicy.d.ts +39 -0
- package/dist/mcp/transportUrlPolicy.js +172 -0
- package/dist/music/providers/BeatovenMusic.js +35 -46
- package/dist/music/providers/ReplicateMusic.js +33 -40
- package/dist/neurolink.d.ts +16 -1
- package/dist/neurolink.js +64 -11
- package/dist/processors/archive/ArchiveProcessor.js +15 -1
- package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
- package/dist/processors/archive/zipEntryIndex.js +33 -0
- package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
- package/dist/processors/base/BaseFileProcessor.js +40 -111
- package/dist/processors/data/YamlProcessor.d.ts +13 -1
- package/dist/processors/data/YamlProcessor.js +93 -2
- package/dist/processors/document/OpenDocumentProcessor.js +7 -2
- package/dist/processors/document/PptxProcessor.js +12 -7
- package/dist/processors/media/VideoProcessor.js +93 -15
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +13 -1
- package/dist/proxy/updateState.js +4 -4
- package/dist/rag/document/loaders.d.ts +0 -6
- package/dist/rag/document/loaders.js +16 -10
- package/dist/rag/ragIntegration.d.ts +2 -2
- package/dist/rag/ragIntegration.js +35 -10
- package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
- package/dist/rag/retrieval/vectorQueryTool.js +54 -17
- package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
- package/dist/server/abstract/baseServerAdapter.js +119 -6
- package/dist/server/adapters/expressAdapter.d.ts +3 -0
- package/dist/server/adapters/expressAdapter.js +83 -8
- package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
- package/dist/server/adapters/fastifyAdapter.js +165 -70
- package/dist/server/adapters/honoAdapter.d.ts +9 -1
- package/dist/server/adapters/honoAdapter.js +99 -21
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +153 -74
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/middleware/auth.js +6 -2
- package/dist/server/middleware/cache.js +6 -1
- package/dist/server/middleware/rateLimit.js +29 -4
- package/dist/server/routes/agentRoutes.js +24 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +273 -84
- package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
- package/dist/server/routes/codexProxyRoutes.js +125 -40
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +35 -4
- package/dist/server/routes/mcpRoutes.d.ts +6 -2
- package/dist/server/routes/mcpRoutes.js +21 -7
- package/dist/server/routes/memoryRoutes.js +7 -6
- package/dist/server/routes/toolRoutes.d.ts +12 -2
- package/dist/server/routes/toolRoutes.js +33 -3
- package/dist/server/routes/transcribeRoutes.js +60 -13
- package/dist/server/utils/clientAddress.d.ts +23 -0
- package/dist/server/utils/clientAddress.js +65 -0
- package/dist/server/utils/clientError.d.ts +35 -0
- package/dist/server/utils/clientError.js +129 -0
- package/dist/server/utils/expressBody.d.ts +33 -0
- package/dist/server/utils/expressBody.js +353 -0
- package/dist/server/utils/pathMatch.d.ts +21 -0
- package/dist/server/utils/pathMatch.js +41 -0
- package/dist/server/utils/validation.d.ts +9 -0
- package/dist/server/utils/validation.js +9 -0
- package/dist/server/websocket/WebSocketHandler.js +22 -3
- package/dist/server/websocket/frameQueue.d.ts +19 -0
- package/dist/server/websocket/frameQueue.js +41 -3
- package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
- package/dist/server/websocket/transcribeWebSocket.js +223 -34
- package/dist/skills/skillStores.js +5 -0
- package/dist/tasks/store/fileTaskStore.d.ts +2 -0
- package/dist/tasks/store/fileTaskStore.js +11 -4
- package/dist/types/attachments.d.ts +117 -0
- package/dist/types/attachments.js +15 -0
- package/dist/types/auth.d.ts +6 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/file.d.ts +8 -0
- package/dist/types/hitl.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/mcp.d.ts +98 -0
- package/dist/types/multimodal.js +0 -41
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +62 -3
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +255 -10
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/transcribe.d.ts +6 -0
- package/dist/utils/attachmentPolicy.d.ts +89 -0
- package/dist/utils/attachmentPolicy.js +327 -0
- package/dist/utils/dotenvBootstrap.d.ts +48 -19
- package/dist/utils/dotenvBootstrap.js +172 -31
- package/dist/utils/fileDetector.js +115 -65
- package/dist/utils/json/coerce.js +11 -1
- package/dist/utils/logSanitize.d.ts +5 -0
- package/dist/utils/logSanitize.js +8 -0
- package/dist/utils/logger.js +73 -1
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +118 -105
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +2 -0
- package/dist/utils/redis.d.ts +12 -0
- package/dist/utils/redis.js +72 -0
- package/dist/utils/safeFetch.d.ts +22 -1
- package/dist/utils/safeFetch.js +132 -38
- package/dist/utils/safeStoreId.d.ts +19 -0
- package/dist/utils/safeStoreId.js +29 -0
- package/dist/utils/schemaConversion.d.ts +30 -3
- package/dist/utils/schemaConversion.js +457 -84
- package/dist/utils/sizeGuard.js +2 -2
- package/dist/utils/ssrfGuard.d.ts +3 -3
- package/dist/utils/ssrfGuard.js +71 -39
- package/dist/voice/eval/sttEvalStore.d.ts +26 -3
- package/dist/voice/eval/sttEvalStore.js +77 -12
- package/dist/voice/livekit/agentDefinition.d.ts +18 -0
- package/dist/voice/livekit/agentDefinition.js +22 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
- package/dist/voice/livekit/realtimeMcpTools.js +8 -7
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
- package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
- package/dist/voice/livekit/voiceAgent.d.ts +2 -2
- package/dist/voice/livekit/voiceAgent.js +2 -1
- package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
- package/dist/voice/livekit/xaiSessionWatch.js +30 -0
- package/dist/voice/providers/GeminiLive.js +21 -6
- package/dist/voice/providers/GoogleSTT.js +8 -5
- package/dist/voice/transcribe.d.ts +2 -2
- package/dist/voice/transcribe.js +34 -18
- package/dist/voice/whistle/audio.js +11 -5
- package/docs-site/static/search-index.json +75 -52
- package/package.json +171 -156
- package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|