@juspay/neurolink 13.8.17 → 13.8.19
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 +5 -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/providers/openaiChatCompletionsClient.js +12 -3
- package/dist/proxy/accountLedger.js +6 -8
- package/dist/proxy/geminiFormat.d.ts +3 -11
- package/dist/proxy/geminiFormat.js +11 -2
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/openaiFormat.d.ts +3 -9
- package/dist/proxy/openaiFormat.js +21 -15
- package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
- package/dist/proxy/proxyTranslationEngine.js +25 -14
- package/dist/proxy/routingPolicy.d.ts +3 -1
- package/dist/proxy/routingPolicy.js +7 -8
- 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 +78 -6
- 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/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/openaiCompatible.d.ts +1 -1
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +71 -16
- package/dist/types/proxyClient.d.ts +2 -0
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +206 -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/retryAfter.d.ts +32 -0
- package/dist/utils/retryAfter.js +135 -11
- 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/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
- package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
- package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
- package/dist/workflow/workflows/consensusWorkflow.js +12 -12
- package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
- package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
- package/docs-site/static/search-index.json +74 -52
- package/package.json +173 -156
- package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
|
@@ -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
|
|
@@ -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
|
|
@@ -40,10 +47,11 @@ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
40
47
|
import { join } from "node:path";
|
|
41
48
|
import { logger } from "../../utils/logger.js";
|
|
42
49
|
import { timingSafeEqualString } from "../voice/tokenCompare.js";
|
|
50
|
+
import { canonicalAddress, isLoopbackAddress } from "../utils/clientAddress.js";
|
|
43
51
|
import { TranscribeOutputControlSchema, TranscribeStreamConfigSchema, TranscribeSubscribeSchema, } from "../utils/validation.js";
|
|
44
52
|
import { fileModeStreamOptions } from "../../voice/transcribe.js";
|
|
45
53
|
import { TranscriptionSession } from "../../voice/session/transcriptionSession.js";
|
|
46
|
-
import { FrameQueue } from "./frameQueue.js";
|
|
54
|
+
import { FrameQueue, totalQueuedFrameBytes } from "./frameQueue.js";
|
|
47
55
|
import { resolveRealtimeProfiles, serveRealtimeTranscription, } from "./realtimeTranscription.js";
|
|
48
56
|
const DEFAULT_PATH = "/v1/audio/transcriptions/stream";
|
|
49
57
|
const DEFAULT_REALTIME_PATH = "/v1/realtime";
|
|
@@ -51,6 +59,21 @@ const DEFAULT_REALTIME_PATH = "/v1/realtime";
|
|
|
51
59
|
const MAX_QUEUED_BYTES = 16 * 1024 * 1024;
|
|
52
60
|
/** In file mode the whole recording may be queued at once: 512 MB is about 4.5 hours of 16 kHz PCM. */
|
|
53
61
|
const MAX_QUEUED_BYTES_FILE = 512 * 1024 * 1024;
|
|
62
|
+
/** Queued audio per socket above which the socket is paused until the engine catches up. */
|
|
63
|
+
const DEFAULT_HIGH_WATER_MARK = 8 * 1024 * 1024;
|
|
64
|
+
/** Queued audio across every socket of the process. */
|
|
65
|
+
const DEFAULT_MAX_TOTAL_QUEUED_BYTES = 512 * 1024 * 1024;
|
|
66
|
+
const DEFAULT_MAX_CONNECTIONS = 100;
|
|
67
|
+
const DEFAULT_MAX_CONNECTIONS_PER_ADDRESS = 16;
|
|
68
|
+
/** Subprotocol carrying the server's API key for a browser, next to `neurolink`. */
|
|
69
|
+
const SUBPROTOCOL_KEY_PREFIX = "neurolink-api-key.";
|
|
70
|
+
/** OpenAI's browser SDK sends the key as this subprotocol; honoured on the realtime path. */
|
|
71
|
+
const OPENAI_SUBPROTOCOL_KEY_PREFIX = "openai-insecure-api-key.";
|
|
72
|
+
/** Answer an upgrade before the handshake, with a status the client can read. */
|
|
73
|
+
function refuseUpgrade(socket, status, reason) {
|
|
74
|
+
socket.write(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\n\r\n`);
|
|
75
|
+
socket.destroy();
|
|
76
|
+
}
|
|
54
77
|
/**
|
|
55
78
|
* `true` when the upgrade's `Origin` is on the allow-list, the list allows
|
|
56
79
|
* any origin, or the request carries no `Origin` (not a browser).
|
|
@@ -66,8 +89,26 @@ function isOriginAllowed(req, allowed) {
|
|
|
66
89
|
const wanted = origin.trim().toLowerCase();
|
|
67
90
|
return allowed.some((o) => o.trim().toLowerCase() === wanted);
|
|
68
91
|
}
|
|
69
|
-
|
|
70
|
-
|
|
92
|
+
/** The configured token(s), empty when the stream is open. */
|
|
93
|
+
function configuredTokens(token) {
|
|
94
|
+
return (typeof token === "string" ? [token] : (token ?? [])).filter((t) => t.length > 0);
|
|
95
|
+
}
|
|
96
|
+
/** The subprotocols the client offered, trimmed. */
|
|
97
|
+
function offeredProtocols(req) {
|
|
98
|
+
return String(req.headers["sec-websocket-protocol"] ?? "")
|
|
99
|
+
.split(",")
|
|
100
|
+
.map((p) => p.trim())
|
|
101
|
+
.filter((p) => p.length > 0);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether the upgrade carries one of `tokens`. A browser cannot set headers,
|
|
105
|
+
* so besides `Authorization: Bearer` the key may ride in the subprotocol list
|
|
106
|
+
* as `neurolink-api-key.<key>` (and, on the realtime path, as OpenAI's
|
|
107
|
+
* `openai-insecure-api-key.<key>`). `?token=` in the URL is accepted only when
|
|
108
|
+
* the operator allows it: a URL is written to access logs and `Referer`
|
|
109
|
+
* headers, so a key there leaks.
|
|
110
|
+
*/
|
|
111
|
+
function isAuthorized(req, tokens, options) {
|
|
71
112
|
if (tokens.length === 0) {
|
|
72
113
|
return true;
|
|
73
114
|
}
|
|
@@ -76,26 +117,37 @@ function isAuthorized(req, token, allowSubprotocolKey = false) {
|
|
|
76
117
|
? header.slice(7)
|
|
77
118
|
: undefined;
|
|
78
119
|
let urlToken;
|
|
120
|
+
if (options.allowQueryToken) {
|
|
121
|
+
try {
|
|
122
|
+
urlToken =
|
|
123
|
+
new URL(req.url ?? "/", "http://localhost").searchParams.get("token") ??
|
|
124
|
+
undefined;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
urlToken = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const protocols = offeredProtocols(req);
|
|
131
|
+
const protocolToken = protocols
|
|
132
|
+
.find((p) => p.startsWith(SUBPROTOCOL_KEY_PREFIX))
|
|
133
|
+
?.slice(SUBPROTOCOL_KEY_PREFIX.length) ??
|
|
134
|
+
(options.allowOpenAISubprotocolKey
|
|
135
|
+
? protocols
|
|
136
|
+
.find((p) => p.startsWith(OPENAI_SUBPROTOCOL_KEY_PREFIX))
|
|
137
|
+
?.slice(OPENAI_SUBPROTOCOL_KEY_PREFIX.length)
|
|
138
|
+
: undefined);
|
|
139
|
+
const provided = headerToken ?? protocolToken ?? urlToken;
|
|
140
|
+
return (provided !== undefined &&
|
|
141
|
+
tokens.some((t) => timingSafeEqualString(provided, t)));
|
|
142
|
+
}
|
|
143
|
+
/** `true` when the upgrade's URL carries a `?token=`, whether or not it is honoured. */
|
|
144
|
+
function carriesQueryToken(req) {
|
|
79
145
|
try {
|
|
80
|
-
|
|
81
|
-
new URL(req.url ?? "/", "http://localhost").searchParams.get("token") ??
|
|
82
|
-
undefined;
|
|
146
|
+
return new URL(req.url ?? "/", "http://localhost").searchParams.has("token");
|
|
83
147
|
}
|
|
84
148
|
catch {
|
|
85
|
-
|
|
149
|
+
return false;
|
|
86
150
|
}
|
|
87
|
-
// OpenAI's browser SDK cannot set headers, so it sends the key as the
|
|
88
|
-
// subprotocol "openai-insecure-api-key.<key>"; honoured on the realtime path.
|
|
89
|
-
const protocolToken = allowSubprotocolKey
|
|
90
|
-
? String(req.headers["sec-websocket-protocol"] ?? "")
|
|
91
|
-
.split(",")
|
|
92
|
-
.map((p) => p.trim())
|
|
93
|
-
.find((p) => p.startsWith("openai-insecure-api-key."))
|
|
94
|
-
?.slice("openai-insecure-api-key.".length)
|
|
95
|
-
: undefined;
|
|
96
|
-
const provided = headerToken ?? urlToken ?? protocolToken;
|
|
97
|
-
return (provided !== undefined &&
|
|
98
|
-
tokens.some((t) => timingSafeEqualString(provided, t)));
|
|
99
151
|
}
|
|
100
152
|
function pathOf(req) {
|
|
101
153
|
try {
|
|
@@ -150,20 +202,51 @@ export async function attachTranscribeWebSocket(server, neurolink, options = {})
|
|
|
150
202
|
? undefined
|
|
151
203
|
: (options.realtimePath ?? DEFAULT_REALTIME_PATH);
|
|
152
204
|
const realtimeProfiles = resolveRealtimeProfiles(options.realtimeProfiles);
|
|
205
|
+
const tokens = configuredTokens(options.authToken);
|
|
206
|
+
const allowQueryToken = options.allowQueryToken === true;
|
|
207
|
+
const maxConnections = options.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
|
|
208
|
+
const maxPerAddress = options.maxConnectionsPerAddress ?? DEFAULT_MAX_CONNECTIONS_PER_ADDRESS;
|
|
209
|
+
const highWaterMark = options.highWaterMark ?? DEFAULT_HIGH_WATER_MARK;
|
|
210
|
+
const maxTotalQueued = options.maxTotalQueuedBytes ?? DEFAULT_MAX_TOTAL_QUEUED_BYTES;
|
|
153
211
|
const wss = new WebSocketServer({
|
|
154
212
|
noServer: true,
|
|
155
213
|
maxPayload: options.maxPayload ?? 1_048_576,
|
|
156
|
-
// OpenAI's browser SDK offers "realtime" alongside its key subprotocol
|
|
157
|
-
|
|
214
|
+
// OpenAI's browser SDK offers "realtime" alongside its key subprotocol;
|
|
215
|
+
// a browser on the stream path offers "neurolink" alongside
|
|
216
|
+
// "neurolink-api-key.<key>". The key-bearing protocol is never selected.
|
|
217
|
+
handleProtocols: (protocols) => protocols.has("realtime")
|
|
218
|
+
? "realtime"
|
|
219
|
+
: protocols.has("neurolink")
|
|
220
|
+
? "neurolink"
|
|
221
|
+
: false,
|
|
158
222
|
});
|
|
159
|
-
// Sessions by id, alive while a speaker or a listener holds them.
|
|
223
|
+
// Sessions by id, alive while a speaker or a listener holds them. A session
|
|
224
|
+
// whose first speaker set `?secret=` admits only joins that present it.
|
|
160
225
|
const sessions = new Map();
|
|
161
|
-
|
|
162
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Join `id` with `secret`, or the reason a join is refused: a session with a
|
|
228
|
+
* secret and a join without the same one, or (with `requireSessionSecret`) a
|
|
229
|
+
* new session that sets none.
|
|
230
|
+
*/
|
|
231
|
+
const admit = (id, secret) => {
|
|
232
|
+
const existing = sessions.get(id);
|
|
233
|
+
if (existing) {
|
|
234
|
+
if (existing.secret !== undefined &&
|
|
235
|
+
(secret === undefined ||
|
|
236
|
+
!timingSafeEqualString(secret, existing.secret))) {
|
|
237
|
+
return "secret-mismatch";
|
|
238
|
+
}
|
|
239
|
+
existing.refs++;
|
|
240
|
+
return existing.session;
|
|
241
|
+
}
|
|
242
|
+
if (options.requireSessionSecret && !secret) {
|
|
243
|
+
return "secret-required";
|
|
244
|
+
}
|
|
245
|
+
const entry = {
|
|
163
246
|
session: new TranscriptionSession(neurolink, { id }),
|
|
164
|
-
refs:
|
|
247
|
+
refs: 1,
|
|
248
|
+
secret,
|
|
165
249
|
};
|
|
166
|
-
entry.refs++;
|
|
167
250
|
sessions.set(id, entry);
|
|
168
251
|
return entry.session;
|
|
169
252
|
};
|
|
@@ -178,6 +261,20 @@ export async function attachTranscribeWebSocket(server, neurolink, options = {})
|
|
|
178
261
|
sessions.delete(id);
|
|
179
262
|
}
|
|
180
263
|
};
|
|
264
|
+
const sessionRefusal = (verdict) => verdict === "secret-mismatch"
|
|
265
|
+
? "This session requires its secret (?secret=)"
|
|
266
|
+
: "A new session must set ?secret=<value> on this server";
|
|
267
|
+
// Open sockets, in all and per remote address, counted from the upgrade so
|
|
268
|
+
// a flood of handshakes is refused before any engine is started. The raw
|
|
269
|
+
// socket's close releases the count whether or not the handshake completed.
|
|
270
|
+
let openConnections = 0;
|
|
271
|
+
const perAddress = new Map();
|
|
272
|
+
const serverIsLoopback = () => {
|
|
273
|
+
const address = server.address();
|
|
274
|
+
return (typeof address === "object" &&
|
|
275
|
+
address !== null &&
|
|
276
|
+
isLoopbackAddress(address.address));
|
|
277
|
+
};
|
|
181
278
|
const onUpgrade = (req, socket, head) => {
|
|
182
279
|
const requested = pathOf(req);
|
|
183
280
|
if (requested !== path &&
|
|
@@ -186,15 +283,46 @@ export async function attachTranscribeWebSocket(server, neurolink, options = {})
|
|
|
186
283
|
return; // Someone else's upgrade.
|
|
187
284
|
}
|
|
188
285
|
if (!isOriginAllowed(req, options.allowedOrigins)) {
|
|
189
|
-
socket
|
|
190
|
-
socket.destroy();
|
|
286
|
+
refuseUpgrade(socket, 403, "Forbidden");
|
|
191
287
|
return;
|
|
192
288
|
}
|
|
193
|
-
if (!isAuthorized(req,
|
|
194
|
-
|
|
195
|
-
|
|
289
|
+
if (!isAuthorized(req, tokens, {
|
|
290
|
+
allowQueryToken,
|
|
291
|
+
allowOpenAISubprotocolKey: requested === realtimePath,
|
|
292
|
+
})) {
|
|
293
|
+
if (tokens.length > 0 && !allowQueryToken && carriesQueryToken(req)) {
|
|
294
|
+
logger.warn("[TranscribeWebSocket] refused an upgrade that carried the key in ?token= (allowQueryToken is off)");
|
|
295
|
+
}
|
|
296
|
+
refuseUpgrade(socket, 401, "Unauthorized");
|
|
196
297
|
return;
|
|
197
298
|
}
|
|
299
|
+
const peer = canonicalAddress(req.socket.remoteAddress ?? "") ?? "unknown";
|
|
300
|
+
const fromPeer = perAddress.get(peer) ?? 0;
|
|
301
|
+
if (openConnections >= maxConnections || fromPeer >= maxPerAddress) {
|
|
302
|
+
logger.warn("[TranscribeWebSocket] refused an upgrade: connection cap", {
|
|
303
|
+
open: openConnections,
|
|
304
|
+
fromPeer,
|
|
305
|
+
});
|
|
306
|
+
refuseUpgrade(socket, 503, "Service Unavailable");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
openConnections++;
|
|
310
|
+
perAddress.set(peer, fromPeer + 1);
|
|
311
|
+
let released = false;
|
|
312
|
+
socket.once("close", () => {
|
|
313
|
+
if (released) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
released = true;
|
|
317
|
+
openConnections--;
|
|
318
|
+
const left = (perAddress.get(peer) ?? 1) - 1;
|
|
319
|
+
if (left <= 0) {
|
|
320
|
+
perAddress.delete(peer);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
perAddress.set(peer, left);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
198
326
|
wss.handleUpgrade(req, socket, head, (client) => {
|
|
199
327
|
wss.emit("connection", client, req);
|
|
200
328
|
});
|
|
@@ -212,7 +340,17 @@ export async function attachTranscribeWebSocket(server, neurolink, options = {})
|
|
|
212
340
|
client.close(1008, "session required");
|
|
213
341
|
return;
|
|
214
342
|
}
|
|
215
|
-
const
|
|
343
|
+
const admitted = admit(id, queryParam(req, "secret"));
|
|
344
|
+
if (typeof admitted === "string") {
|
|
345
|
+
client.send(JSON.stringify({
|
|
346
|
+
type: "error",
|
|
347
|
+
message: sessionRefusal(admitted),
|
|
348
|
+
recoverable: false,
|
|
349
|
+
}));
|
|
350
|
+
client.close(1008, "session refused");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const session = admitted;
|
|
216
354
|
let subscription;
|
|
217
355
|
const deliver = (event) => {
|
|
218
356
|
if (client.readyState === WebSocket.OPEN) {
|
|
@@ -269,6 +407,20 @@ export async function attachTranscribeWebSocket(server, neurolink, options = {})
|
|
|
269
407
|
const queue = new FrameQueue();
|
|
270
408
|
let started = false;
|
|
271
409
|
let fileMode = false;
|
|
410
|
+
// Backpressure: above the high-water mark the socket stops reading until
|
|
411
|
+
// the engine has taken enough of the backlog (half the mark), so a fast
|
|
412
|
+
// sender is slowed by TCP instead of buffered without bound.
|
|
413
|
+
let paused = false;
|
|
414
|
+
queue.setLevelListener((level) => {
|
|
415
|
+
if (!paused && level > highWaterMark) {
|
|
416
|
+
paused = true;
|
|
417
|
+
client.pause();
|
|
418
|
+
}
|
|
419
|
+
else if (paused && level <= highWaterMark / 2) {
|
|
420
|
+
paused = false;
|
|
421
|
+
client.resume();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
272
424
|
// Debugging: `NEUROLINK_STT_RECORD_DIR` keeps every session's config,
|
|
273
425
|
// audio (PCM16LE) and events on disk, so a live session can be replayed
|
|
274
426
|
// and compared against another engine. Off unless set.
|
|
@@ -291,6 +443,7 @@ export async function attachTranscribeWebSocket(server, neurolink, options = {})
|
|
|
291
443
|
// Output languages, changeable mid-stream by control frames.
|
|
292
444
|
let liveLanguages = [];
|
|
293
445
|
const sessionId = subscribePath ? queryParam(req, "session") : undefined;
|
|
446
|
+
const sessionSecret = queryParam(req, "secret");
|
|
294
447
|
// No name: the session assigns a unique one (speaker-1, speaker-2, …).
|
|
295
448
|
const speaker = queryParam(req, "speaker") ?? "";
|
|
296
449
|
const send = (event) => {
|
|
@@ -315,7 +468,16 @@ export async function attachTranscribeWebSocket(server, neurolink, options = {})
|
|
|
315
468
|
}
|
|
316
469
|
// A session speaker: events go to the session's listeners and back to
|
|
317
470
|
// this socket. The session is held until this speaker's audio ends.
|
|
318
|
-
const
|
|
471
|
+
const admitted = admit(sessionId, sessionSecret);
|
|
472
|
+
if (typeof admitted === "string") {
|
|
473
|
+
const refusal = new Error(sessionRefusal(admitted));
|
|
474
|
+
return {
|
|
475
|
+
[Symbol.asyncIterator]: () => ({
|
|
476
|
+
next: () => Promise.reject(refusal),
|
|
477
|
+
}),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const session = admitted;
|
|
319
481
|
const buffered = [];
|
|
320
482
|
let wake;
|
|
321
483
|
let finished = false;
|
|
@@ -415,12 +577,39 @@ export async function attachTranscribeWebSocket(server, neurolink, options = {})
|
|
|
415
577
|
started = true;
|
|
416
578
|
const { mode, ...rest } = config.data;
|
|
417
579
|
fileMode = mode === "file";
|
|
580
|
+
// File mode buffers a whole recording (up to 512 MiB) for a
|
|
581
|
+
// caller; on a listener the network can reach, only a caller the
|
|
582
|
+
// server authenticated gets that, unless the operator opts out.
|
|
583
|
+
if (fileMode &&
|
|
584
|
+
tokens.length === 0 &&
|
|
585
|
+
options.allowUnauthenticatedFileMode !== true &&
|
|
586
|
+
!serverIsLoopback()) {
|
|
587
|
+
send({
|
|
588
|
+
type: "error",
|
|
589
|
+
message: 'mode "file" needs authentication on this server; stream live audio instead',
|
|
590
|
+
recoverable: false,
|
|
591
|
+
});
|
|
592
|
+
client.close(1008, "file mode requires authentication");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
418
595
|
liveLanguages = rest.output?.languages ?? [];
|
|
419
596
|
record("config.json", JSON.stringify(config.data, null, 1), false);
|
|
420
597
|
void run(fileMode ? fileModeStreamOptions(rest) : rest);
|
|
421
598
|
return;
|
|
422
599
|
}
|
|
423
600
|
if (isBinary) {
|
|
601
|
+
// The process-wide budget is checked before the frame is taken:
|
|
602
|
+
// one socket's burst must not take the others down with it.
|
|
603
|
+
if (totalQueuedFrameBytes() + frame.length > maxTotalQueued) {
|
|
604
|
+
send({
|
|
605
|
+
type: "error",
|
|
606
|
+
message: "The server is holding too much audio right now; try again later",
|
|
607
|
+
recoverable: false,
|
|
608
|
+
});
|
|
609
|
+
queue.end();
|
|
610
|
+
client.close(1013, "server busy");
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
424
613
|
record("audio.pcm", frame);
|
|
425
614
|
queue.push(frame);
|
|
426
615
|
if (queue.queuedBytes >
|
|
@@ -11,6 +11,7 @@ import * as fs from "node:fs";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import yaml from "js-yaml";
|
|
13
13
|
import { logger } from "../utils/logger.js";
|
|
14
|
+
import { assertSafeStoreId } from "../utils/safeStoreId.js";
|
|
14
15
|
import { toSkillIndexItem } from "./skillMatcher.js";
|
|
15
16
|
import { S3SkillStore } from "./skillStoreS3.js";
|
|
16
17
|
import { RedisSkillStore } from "./skillStoreRedis.js";
|
|
@@ -220,12 +221,16 @@ export class FileSystemSkillStore {
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
async put(skill) {
|
|
224
|
+
// The id becomes a file name under `baseDir`; refuse anything that could
|
|
225
|
+
// leave it (a separator, a traversal segment) before the join.
|
|
226
|
+
assertSafeStoreId(skill.id, "skill id");
|
|
223
227
|
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
224
228
|
const filePath = path.join(this.baseDir, `${skill.id}.json`);
|
|
225
229
|
fs.writeFileSync(filePath, JSON.stringify(normalizeSkill(skill), null, 2), "utf-8");
|
|
226
230
|
this.invalidate();
|
|
227
231
|
}
|
|
228
232
|
async delete(id) {
|
|
233
|
+
assertSafeStoreId(id, "skill id");
|
|
229
234
|
const filePath = path.join(this.baseDir, `${id}.json`);
|
|
230
235
|
try {
|
|
231
236
|
fs.unlinkSync(filePath);
|
|
@@ -28,6 +28,8 @@ export declare class FileTaskStore implements TaskStore {
|
|
|
28
28
|
}): Promise<Task[]>;
|
|
29
29
|
update(taskId: string, updates: Partial<Task>): Promise<Task>;
|
|
30
30
|
delete(taskId: string): Promise<void>;
|
|
31
|
+
/** `{logsPath}/{taskId}.jsonl`, after refusing an id that is not a file name. */
|
|
32
|
+
private runLogPath;
|
|
31
33
|
appendRun(taskId: string, run: TaskRunResult): Promise<void>;
|
|
32
34
|
getRuns(taskId: string, options?: {
|
|
33
35
|
limit?: number;
|
|
@@ -11,6 +11,7 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
11
11
|
import { appendFile, readFile, rename, unlink, writeFile, } from "node:fs/promises";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { logger } from "../../utils/logger.js";
|
|
14
|
+
import { assertSafeStoreId } from "../../utils/safeStoreId.js";
|
|
14
15
|
import { TaskError } from "../errors.js";
|
|
15
16
|
import { TASK_DEFAULTS, } from "../../types/index.js";
|
|
16
17
|
export class FileTaskStore {
|
|
@@ -91,8 +92,9 @@ export class FileTaskStore {
|
|
|
91
92
|
async delete(taskId) {
|
|
92
93
|
this.tasks.delete(taskId);
|
|
93
94
|
this.history.delete(taskId);
|
|
94
|
-
// Delete run log file
|
|
95
|
-
|
|
95
|
+
// Delete run log file. The id becomes a file name under `logsPath`;
|
|
96
|
+
// nanoid ids always pass, anything that could leave the directory does not.
|
|
97
|
+
const logPath = this.runLogPath(taskId);
|
|
96
98
|
try {
|
|
97
99
|
await unlink(logPath);
|
|
98
100
|
}
|
|
@@ -102,14 +104,19 @@ export class FileTaskStore {
|
|
|
102
104
|
await this.flush();
|
|
103
105
|
}
|
|
104
106
|
// ── Run Logs ──────────────────────────────────────────
|
|
107
|
+
/** `{logsPath}/{taskId}.jsonl`, after refusing an id that is not a file name. */
|
|
108
|
+
runLogPath(taskId) {
|
|
109
|
+
assertSafeStoreId(taskId, "task id");
|
|
110
|
+
return join(this.logsPath, `${taskId}.jsonl`);
|
|
111
|
+
}
|
|
105
112
|
async appendRun(taskId, run) {
|
|
106
|
-
const logPath =
|
|
113
|
+
const logPath = this.runLogPath(taskId);
|
|
107
114
|
mkdirSync(dirname(logPath), { recursive: true });
|
|
108
115
|
await appendFile(logPath, JSON.stringify(run) + "\n", "utf-8");
|
|
109
116
|
await this.pruneRunLog(logPath);
|
|
110
117
|
}
|
|
111
118
|
async getRuns(taskId, options) {
|
|
112
|
-
const logPath =
|
|
119
|
+
const logPath = this.runLogPath(taskId);
|
|
113
120
|
if (!existsSync(logPath)) {
|
|
114
121
|
return [];
|
|
115
122
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment ingestion policy — the SDK-level counterpart of the server's
|
|
3
|
+
* default-deny path roots and SSRF guard.
|
|
4
|
+
*
|
|
5
|
+
* Every loader that turns a caller-supplied string into bytes (image URLs and
|
|
6
|
+
* paths in `input.images`, entries of `input.files`, `rag.files`,
|
|
7
|
+
* `transcribe({ audio })`, Director Mode segment images, `WebLoader`) reads
|
|
8
|
+
* its limits from one {@link AttachmentPolicy}. The policy is resolved once
|
|
9
|
+
* per `NeuroLink` instance from {@link AttachmentsConfig} and the
|
|
10
|
+
* `NEUROLINK_ATTACHMENT_*` environment variables, then stamped onto each
|
|
11
|
+
* call's options so the loaders several hops down see the same answer.
|
|
12
|
+
*
|
|
13
|
+
* Runtime helpers live in `src/lib/utils/attachmentPolicy.ts`.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* `new NeuroLink({ attachments })` — what an instance may read or fetch on a
|
|
17
|
+
* caller's behalf.
|
|
18
|
+
*
|
|
19
|
+
* Every field also has an environment variable for deployments that cannot
|
|
20
|
+
* change constructor arguments (the CLI, containers). An explicit field wins
|
|
21
|
+
* over its variable.
|
|
22
|
+
*
|
|
23
|
+
* | Field | Environment variable | Default |
|
|
24
|
+
* | --- | --- | --- |
|
|
25
|
+
* | `allowedRoots` | `NEUROLINK_ATTACHMENT_ROOTS` (path-list, `:`/`;` separated) | unrestricted (warns once) |
|
|
26
|
+
* | `allowInsecureUrls` | `NEUROLINK_ATTACHMENT_ALLOW_INSECURE_URLS=1` | `false` |
|
|
27
|
+
* | `allowPrivateNetwork` | `NEUROLINK_ATTACHMENT_ALLOW_PRIVATE_NETWORK=1` | `false` |
|
|
28
|
+
* | `followRedirects` | `NEUROLINK_ATTACHMENT_FOLLOW_REDIRECTS=0` | `true` |
|
|
29
|
+
* | `maxBytes` | `NEUROLINK_ATTACHMENT_MAX_BYTES` | each loader's own cap |
|
|
30
|
+
*/
|
|
31
|
+
export type AttachmentsConfig = {
|
|
32
|
+
/**
|
|
33
|
+
* Directories a local file path may resolve into. Containment is checked on
|
|
34
|
+
* the real (symlink-resolved) path of both the root and the target, and the
|
|
35
|
+
* file is opened `O_NOFOLLOW` on that real path, so a symlink planted under
|
|
36
|
+
* a root cannot reach outside it. Unset means any readable file — the
|
|
37
|
+
* pre-hardening behaviour — and the SDK warns once per process when it
|
|
38
|
+
* reads a path without roots. A host that forwards strings from untrusted
|
|
39
|
+
* callers must set this.
|
|
40
|
+
*/
|
|
41
|
+
allowedRoots?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Accept `http://` attachment URLs. Default `false`: plain HTTP carries the
|
|
44
|
+
* bytes — and any credentials on the request — in the clear, and is the
|
|
45
|
+
* transport every loopback and link-local target answers on.
|
|
46
|
+
*/
|
|
47
|
+
allowInsecureUrls?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Fetch URLs whose host is, or resolves to, a loopback, RFC 1918,
|
|
50
|
+
* link-local, CGNAT, multicast or cloud-metadata address. Default `false`.
|
|
51
|
+
* The connection is still pinned to the resolved addresses.
|
|
52
|
+
*/
|
|
53
|
+
allowPrivateNetwork?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Follow HTTP redirects (up to five hops). Default `true`. Every hop is
|
|
56
|
+
* re-validated against the same URL policy, and caller-supplied headers
|
|
57
|
+
* (`authHeaders`) are never sent to a redirect target.
|
|
58
|
+
*/
|
|
59
|
+
followRedirects?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Upper bound, in bytes, for any single attachment read or download. A
|
|
62
|
+
* loader's own cap still applies when it is lower.
|
|
63
|
+
*/
|
|
64
|
+
maxBytes?: number;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The resolved, immutable form of {@link AttachmentsConfig} that the loaders
|
|
68
|
+
* consume. `allowedRoots` holds absolute paths; `undefined` means unrestricted.
|
|
69
|
+
*/
|
|
70
|
+
export type AttachmentPolicy = {
|
|
71
|
+
readonly kind: "neurolink-attachment-policy";
|
|
72
|
+
readonly allowedRoots: readonly string[] | undefined;
|
|
73
|
+
readonly allowInsecureUrls: boolean;
|
|
74
|
+
readonly allowPrivateNetwork: boolean;
|
|
75
|
+
readonly followRedirects: boolean;
|
|
76
|
+
readonly maxBytes: number | undefined;
|
|
77
|
+
};
|
|
78
|
+
/** Options for `downloadAttachment()`. */
|
|
79
|
+
export type AttachmentDownloadOptions = {
|
|
80
|
+
/** Policy to apply; the environment-derived default when omitted. */
|
|
81
|
+
policy?: AttachmentPolicy;
|
|
82
|
+
/** Hard cap on the response body in bytes (the policy's `maxBytes` lowers it). */
|
|
83
|
+
maxBytes: number;
|
|
84
|
+
/** Short label for error messages, e.g. `"image URL"`. */
|
|
85
|
+
label: string;
|
|
86
|
+
/**
|
|
87
|
+
* Headers for the first request only. They are dropped on every redirect
|
|
88
|
+
* hop so a credential meant for one host never reaches another.
|
|
89
|
+
*/
|
|
90
|
+
headers?: Record<string, string>;
|
|
91
|
+
/** Transfer timeout in milliseconds. */
|
|
92
|
+
timeoutMs?: number;
|
|
93
|
+
/** Caller cancellation. */
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
/** `HEAD` fetches headers only and returns an empty body. */
|
|
96
|
+
method?: "GET" | "HEAD";
|
|
97
|
+
};
|
|
98
|
+
/** Options for `readAttachmentFile()`. */
|
|
99
|
+
export type AttachmentReadOptions = {
|
|
100
|
+
/** Policy to apply; the environment-derived default when omitted. */
|
|
101
|
+
policy?: AttachmentPolicy;
|
|
102
|
+
/** Hard cap on the file size in bytes (the policy's `maxBytes` lowers it). */
|
|
103
|
+
maxBytes: number;
|
|
104
|
+
/** Short label for error messages, e.g. `"image file"`. */
|
|
105
|
+
label: string;
|
|
106
|
+
};
|
|
107
|
+
/** What `readAttachmentFile()` returns. */
|
|
108
|
+
export type AttachmentReadResult = {
|
|
109
|
+
buffer: Buffer;
|
|
110
|
+
/** The symlink-resolved path that was actually opened. */
|
|
111
|
+
realPath: string;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Machine-readable reasons an attachment was refused. Carried as `code` on the
|
|
115
|
+
* thrown error so callers can map them without parsing messages.
|
|
116
|
+
*/
|
|
117
|
+
export type AttachmentRefusalCode = "ATTACHMENT_OUTSIDE_ROOTS" | "ATTACHMENT_NOT_A_FILE" | "ATTACHMENT_TOO_LARGE" | "ATTACHMENT_URL_REFUSED";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment ingestion policy — the SDK-level counterpart of the server's
|
|
3
|
+
* default-deny path roots and SSRF guard.
|
|
4
|
+
*
|
|
5
|
+
* Every loader that turns a caller-supplied string into bytes (image URLs and
|
|
6
|
+
* paths in `input.images`, entries of `input.files`, `rag.files`,
|
|
7
|
+
* `transcribe({ audio })`, Director Mode segment images, `WebLoader`) reads
|
|
8
|
+
* its limits from one {@link AttachmentPolicy}. The policy is resolved once
|
|
9
|
+
* per `NeuroLink` instance from {@link AttachmentsConfig} and the
|
|
10
|
+
* `NEUROLINK_ATTACHMENT_*` environment variables, then stamped onto each
|
|
11
|
+
* call's options so the loaders several hops down see the same answer.
|
|
12
|
+
*
|
|
13
|
+
* Runtime helpers live in `src/lib/utils/attachmentPolicy.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
package/dist/types/auth.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export type TokenStorageData = {
|
|
|
55
55
|
/** Tokens indexed by provider name */
|
|
56
56
|
providers: Record<string, StoredProviderTokens>;
|
|
57
57
|
};
|
|
58
|
+
/** Owner record written into the token store's cross-process lock file. */
|
|
59
|
+
export type TokenStoreLockOwner = {
|
|
60
|
+
pid: number;
|
|
61
|
+
token: string;
|
|
62
|
+
acquiredAt: number;
|
|
63
|
+
};
|
|
58
64
|
/**
|
|
59
65
|
* Per-provider token storage structure
|
|
60
66
|
*/
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -805,6 +805,10 @@ export type ProxyStartArgs = {
|
|
|
805
805
|
envFile?: string;
|
|
806
806
|
passthrough?: boolean;
|
|
807
807
|
dev?: boolean;
|
|
808
|
+
/** Bearer every inbound caller must present; also `NEUROLINK_PROXY_INBOUND_TOKEN`. */
|
|
809
|
+
inboundToken?: string;
|
|
810
|
+
/** Operator's explicit choice to bind off-loopback with no inbound auth. */
|
|
811
|
+
allowUnauthenticated?: boolean;
|
|
808
812
|
};
|
|
809
813
|
/** Arguments accepted by `neurolink proxy status` */
|
|
810
814
|
export type ProxyStatusArgs = {
|