@juspay/neurolink 13.0.1 → 13.0.3

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.
@@ -14,8 +14,7 @@
14
14
  *
15
15
  * @module @neurolink/react
16
16
  */
17
- import type { ReactNode } from "react";
18
- import type { NeuroLinkProviderProps, UseChatOptions, UseChatReturn, UseAgentOptions, UseAgentReturn, UseWorkflowOptions, UseWorkflowReturn, UseVoiceOptions, UseVoiceReturn, UseStreamOptions, UseStreamReturn, UseToolsOptions, UseToolsReturn, SpeechRecognitionInternal } from "../types/index.js";
17
+ import type { ClientReactElement, NeuroLinkProviderProps, UseChatOptions, UseChatReturn, UseAgentOptions, UseAgentReturn, UseWorkflowOptions, UseWorkflowReturn, UseVoiceOptions, UseVoiceReturn, UseStreamOptions, UseStreamReturn, UseToolsOptions, UseToolsReturn, SpeechRecognitionInternal } from "../types/index.js";
19
18
  import { NeuroLinkClient } from "./httpClient.js";
20
19
  /**
21
20
  * Provider component for NeuroLink client
@@ -40,9 +39,7 @@ import { NeuroLinkClient } from "./httpClient.js";
40
39
  * }
41
40
  * ```
42
41
  */
43
- export declare function NeuroLinkProvider({ config, children, }: NeuroLinkProviderProps & {
44
- children: ReactNode;
45
- }): ReactNode;
42
+ export declare function NeuroLinkProvider({ config, children, }: NeuroLinkProviderProps): ClientReactElement;
46
43
  /**
47
44
  * Hook to access the NeuroLink client
48
45
  *
@@ -48,12 +48,15 @@ const DEFAULT_EMBEDDING_MIN_TOOLS = 20;
48
48
  const DEFAULT_EMBEDDING_TOP_K = 20;
49
49
  /**
50
50
  * Upper bound on the user-query text interpolated into the router prompt.
51
- * The query is untrusted and can attempt prompt injection — the blast radius
52
- * is already bounded (the worst a successful injection achieves is making the
53
- * router keep MORE already-registered tools; out-of-catalog ids are filtered),
54
- * but without a cap an arbitrarily large query is sent to the router LLM every
55
- * turn. 10K chars is far more than enough to classify routing intent while
56
- * leaving room for a window of recent conversation turns.
51
+ * The query is untrusted and can attempt prompt injection. The reach is
52
+ * bounded, not zero: out-of-catalog ids are filtered, so an injected query
53
+ * cannot name a tool that was not already registered, and an empty pick fails
54
+ * open. But the router returns the servers to KEEP and every unselected
55
+ * routable server is excluded for the turn with no confidence bar, so a
56
+ * steered pick can withhold tools. Without a cap an arbitrarily large query is
57
+ * sent to the router LLM every turn. 10K chars is far more than enough to
58
+ * classify routing intent while leaving room for a window of recent
59
+ * conversation turns.
57
60
  */
58
61
  const MAX_ROUTER_QUERY_CHARS = 10000;
59
62
  /**
@@ -33,9 +33,11 @@ const SERVER_NAMESPACE = "server";
33
33
  const DEFAULT_MIN_DROP_CONFIDENCE = 0.6;
34
34
  /**
35
35
  * Upper bound on the query text sent as state. The query is untrusted and may
36
- * attempt injection; the blast radius is bounded (the worst outcome is keeping
37
- * MORE already-registered tools, since ids are never read back off the wire —
38
- * answers are matched by position), but an unbounded state would still be
36
+ * attempt injection. Ids are never read back off the wire — answers are matched
37
+ * by position — so an injected query cannot name a tool that was not already
38
+ * registered, and the result is only a denylist appended to `excludeTools`. It
39
+ * can still steer a verdict: a confident `false` drops that server's tools for
40
+ * the turn. The reach is bounded, not zero. An unbounded state would also be
39
41
  * billed and could push the request past the model's input ceiling.
40
42
  */
41
43
  const MAX_STATE_CHARS = 10000;
@@ -117,12 +117,12 @@ export const AgentInputSchema = {
117
117
  images: {
118
118
  type: "array",
119
119
  items: { type: "string" },
120
- description: "Array of image URLs or base64 data",
120
+ description: "Array of images, each a data: URI, an https URL the server downloads (public hosts only, no redirects) or a path under a directory the server allows (allowedInputRoots; with none set every local path is refused with 403). Bare base64 is not accepted, and http:// or any other scheme is refused with 400. A request is limited in attachment count and total bytes (413).",
121
121
  },
122
122
  files: {
123
123
  type: "array",
124
124
  items: { type: "string" },
125
- description: "Array of file paths or URLs",
125
+ description: "Array of files, each a data: URI, an https URL the server downloads (public hosts only, no redirects) or a path under a directory the server allows (allowedInputRoots; with none set every local path is refused with 403). http:// or any other scheme is refused with 400. A request is limited in attachment count and total bytes (413), images and files together.",
126
126
  },
127
127
  },
128
128
  },
@@ -2,8 +2,15 @@
2
2
  * Agent Routes
3
3
  * Endpoints for agent execution and streaming
4
4
  */
5
- import type { RouteGroup } from "../../types/index.js";
5
+ import type { RouteGroup, ServerAgentRouteOptions } from "../../types/index.js";
6
6
  /**
7
7
  * Create agent routes
8
+ *
9
+ * @param options - Limits for `input.images` / `input.files`:
10
+ * `allowedInputRoots` (default: none, every local-path attachment is
11
+ * refused), `maxInputBytes` per attachment (default 50 MiB),
12
+ * `maxInputAttachments` per request (default 20, images and files together)
13
+ * and `maxInputTotalBytes` per request (default 100 MiB, one budget shared by
14
+ * images and files).
8
15
  */
9
- export declare function createAgentRoutes(basePath?: string): RouteGroup;
16
+ export declare function createAgentRoutes(basePath?: string, options?: ServerAgentRouteOptions): RouteGroup;
@@ -27,6 +27,7 @@ async function resolveRequestToolFilter(ctx, requestedTools) {
27
27
  logger.warn("[agentRoutes] Ignoring request.tools — no entries match registered tool names (legacy fail-open)", { requested: requestedTools });
28
28
  return undefined;
29
29
  }
30
+ import { AgentInputError, resolveAgentInput } from "../utils/agentInput.js";
30
31
  import { createStreamRedactor } from "../utils/redaction.js";
31
32
  import { AgentExecuteRequestSchema, createErrorResponse as createError, EmbedManyRequestSchema, EmbedRequestSchema, SkillCreateRequestSchema, SkillUpdateRequestSchema, validateRequest, } from "../utils/validation.js";
32
33
  /**
@@ -56,10 +57,35 @@ function resolveMutableSkillsManager(ctx) {
56
57
  }
57
58
  return manager;
58
59
  }
60
+ /**
61
+ * The request's `input` with its attachments vetted (see `agentInput.ts`), or
62
+ * the error response for the first one the server refuses.
63
+ */
64
+ async function resolveRequestInput(ctx, requestInput, options) {
65
+ if (typeof requestInput === "string") {
66
+ return { text: requestInput };
67
+ }
68
+ try {
69
+ return await resolveAgentInput(requestInput, options);
70
+ }
71
+ catch (error) {
72
+ if (error instanceof AgentInputError) {
73
+ return createError(error.code, error.message, undefined, ctx.requestId, error.status);
74
+ }
75
+ throw error;
76
+ }
77
+ }
59
78
  /**
60
79
  * Create agent routes
80
+ *
81
+ * @param options - Limits for `input.images` / `input.files`:
82
+ * `allowedInputRoots` (default: none, every local-path attachment is
83
+ * refused), `maxInputBytes` per attachment (default 50 MiB),
84
+ * `maxInputAttachments` per request (default 20, images and files together)
85
+ * and `maxInputTotalBytes` per request (default 100 MiB, one budget shared by
86
+ * images and files).
61
87
  */
62
- export function createAgentRoutes(basePath = "/api") {
88
+ export function createAgentRoutes(basePath = "/api", options) {
63
89
  return {
64
90
  prefix: `${basePath}/agent`,
65
91
  routes: [
@@ -82,10 +108,10 @@ export function createAgentRoutes(basePath = "/api") {
82
108
  "ai.model": request.model || "default",
83
109
  },
84
110
  }, async () => {
85
- // Normalize input
86
- const input = typeof request.input === "string"
87
- ? { text: request.input }
88
- : request.input;
111
+ const input = await resolveRequestInput(ctx, request.input, options);
112
+ if ("error" in input) {
113
+ return input;
114
+ }
89
115
  const requestToolFilter = await resolveRequestToolFilter(ctx, request.tools);
90
116
  const result = await ctx.neurolink.generate({
91
117
  input,
@@ -135,10 +161,10 @@ export function createAgentRoutes(basePath = "/api") {
135
161
  return validation.error;
136
162
  }
137
163
  const request = validation.data;
138
- // Normalize input
139
- const input = typeof request.input === "string"
140
- ? { text: request.input }
141
- : request.input;
164
+ const input = await resolveRequestInput(ctx, request.input, options);
165
+ if ("error" in input) {
166
+ return input;
167
+ }
142
168
  const streamToolFilter = await resolveRequestToolFilter(ctx, request.tools);
143
169
  const result = await ctx.neurolink.stream({
144
170
  input,
@@ -41,7 +41,7 @@ export { createTranscribeRoutes } from "./transcribeRoutes.js";
41
41
  */
42
42
  export function createAllRoutes(basePath = "/api", options) {
43
43
  const routes = [
44
- createAgentRoutes(basePath),
44
+ createAgentRoutes(basePath, options?.agent),
45
45
  createToolRoutes(basePath),
46
46
  createMCPRoutes(basePath),
47
47
  createMemoryRoutes(basePath),
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Agent request attachments, vetted at the HTTP boundary.
3
+ *
4
+ * `input.images` and `input.files` reach the SDK as strings, and the SDK
5
+ * resolves a string itself: an `http(s)` URL is fetched with redirects followed
6
+ * and no address check, and anything else is read from the local disk. Both
7
+ * are right for a program that names its own files. Neither is right when the
8
+ * string came from a caller of a network server, who could name
9
+ * `https://169.254.169.254/...`, a redirect to it, or `/etc/passwd`.
10
+ *
11
+ * So the route resolves every attachment itself and hands the SDK bytes:
12
+ * - `data:` URIs pass through, they carry their own content;
13
+ * - `https` URLs are downloaded with `safeDownload`: redirects are refused and
14
+ * the size is capped. The address is checked and the connection pinned to
15
+ * the checked addresses only for a direct download. Behind `HTTPS_PROXY` /
16
+ * `ALL_PROXY` the proxy resolves the host, so only the URL text is checked
17
+ * (IP literals, single-label and internal host names) and what a
18
+ * public-looking name resolves to is the proxy's egress policy;
19
+ * - local paths are read only from the operator's `allowedInputRoots`, after
20
+ * symlinks are resolved;
21
+ * - everything else is refused rather than guessed at, so this classifier
22
+ * can never disagree with the SDK's about what a string means.
23
+ *
24
+ * One request is also bounded as a whole: at most `maxInputAttachments`
25
+ * entries, each at most `maxInputBytes`, all together at most
26
+ * `maxInputTotalBytes`. The count and every `data:` URI are judged before
27
+ * anything is read; the rest are resolved one at a time, each allowed only what
28
+ * is left of the shared budget, so a transfer that would overflow it stops
29
+ * mid-stream.
30
+ *
31
+ * The SDK itself is untouched: a program calling `generate()` directly keeps
32
+ * reading the paths and URLs it names.
33
+ *
34
+ * @module server/utils/agentInput
35
+ */
36
+ import type { ServerAgentResolvedInput, ServerAgentRouteOptions } from "../../types/index.js";
37
+ /** An attachment the server refuses, with the HTTP status that says why. */
38
+ export declare class AgentInputError extends Error {
39
+ readonly status: number;
40
+ readonly code: string;
41
+ constructor(message: string, status: number, code: string);
42
+ }
43
+ /**
44
+ * Replace every attachment of an agent request's `input` with vetted content.
45
+ *
46
+ * @throws {AgentInputError} for the first attachment that is refused, or for a
47
+ * request that carries too many attachments or too many bytes.
48
+ */
49
+ export declare function resolveAgentInput(input: {
50
+ text: string;
51
+ images?: string[];
52
+ files?: string[];
53
+ }, options?: ServerAgentRouteOptions): Promise<ServerAgentResolvedInput>;
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Agent request attachments, vetted at the HTTP boundary.
3
+ *
4
+ * `input.images` and `input.files` reach the SDK as strings, and the SDK
5
+ * resolves a string itself: an `http(s)` URL is fetched with redirects followed
6
+ * and no address check, and anything else is read from the local disk. Both
7
+ * are right for a program that names its own files. Neither is right when the
8
+ * string came from a caller of a network server, who could name
9
+ * `https://169.254.169.254/...`, a redirect to it, or `/etc/passwd`.
10
+ *
11
+ * So the route resolves every attachment itself and hands the SDK bytes:
12
+ * - `data:` URIs pass through, they carry their own content;
13
+ * - `https` URLs are downloaded with `safeDownload`: redirects are refused and
14
+ * the size is capped. The address is checked and the connection pinned to
15
+ * the checked addresses only for a direct download. Behind `HTTPS_PROXY` /
16
+ * `ALL_PROXY` the proxy resolves the host, so only the URL text is checked
17
+ * (IP literals, single-label and internal host names) and what a
18
+ * public-looking name resolves to is the proxy's egress policy;
19
+ * - local paths are read only from the operator's `allowedInputRoots`, after
20
+ * symlinks are resolved;
21
+ * - everything else is refused rather than guessed at, so this classifier
22
+ * can never disagree with the SDK's about what a string means.
23
+ *
24
+ * One request is also bounded as a whole: at most `maxInputAttachments`
25
+ * entries, each at most `maxInputBytes`, all together at most
26
+ * `maxInputTotalBytes`. The count and every `data:` URI are judged before
27
+ * anything is read; the rest are resolved one at a time, each allowed only what
28
+ * is left of the shared budget, so a transfer that would overflow it stops
29
+ * mid-stream.
30
+ *
31
+ * The SDK itself is untouched: a program calling `generate()` directly keeps
32
+ * reading the paths and URLs it names.
33
+ *
34
+ * @module server/utils/agentInput
35
+ */
36
+ import fs from "node:fs";
37
+ import path from "node:path";
38
+ import { logger } from "../../utils/logger.js";
39
+ const DEFAULT_MAX_INPUT_BYTES = 50 * 1024 * 1024;
40
+ const DEFAULT_MAX_INPUT_TOTAL_BYTES = 100 * 1024 * 1024;
41
+ const DEFAULT_MAX_INPUT_ATTACHMENTS = 20;
42
+ const READ_CHUNK_BYTES = 64 * 1024;
43
+ /** Two or more scheme characters, so a Windows drive letter (`C:\`) is not one. */
44
+ const URL_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]+:/;
45
+ /** An attachment the server refuses, with the HTTP status that says why. */
46
+ export class AgentInputError extends Error {
47
+ status;
48
+ code;
49
+ constructor(message, status, code) {
50
+ super(message);
51
+ this.status = status;
52
+ this.code = code;
53
+ this.name = "AgentInputError";
54
+ }
55
+ }
56
+ const isUnder = (child, root) => child === root ||
57
+ child.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
58
+ const forbidden = () => new AgentInputError("attachment path is outside the directories this server allows (or local paths are disabled)", 403, "FORBIDDEN");
59
+ const notFound = () => new AgentInputError("attachment path does not exist", 404, "NOT_FOUND");
60
+ const notReadable = () => new AgentInputError("attachment path is not readable", 403, "FORBIDDEN");
61
+ const notAFile = () => new AgentInputError("attachment path is not a file", 400, "VALIDATION_ERROR");
62
+ const tooLarge = (limit) => new AgentInputError(`attachment exceeds the ${limit}-byte limit`, 413, "PAYLOAD_TOO_LARGE");
63
+ const budgetExceeded = (total) => new AgentInputError(`attachments together exceed the ${total}-byte limit`, 413, "PAYLOAD_TOO_LARGE");
64
+ const tooMany = (max) => new AgentInputError(`a request may carry at most ${max} attachments`, 413, "PAYLOAD_TOO_LARGE");
65
+ /** A configured limit, or the default when it is missing or not a number >= 0. */
66
+ const limitOr = (value, fallback) => typeof value === "number" && value >= 0 ? value : fallback;
67
+ const isDataUri = (entry) => entry.startsWith("data:");
68
+ const fsErrorCode = (error) => typeof error === "object" &&
69
+ error !== null &&
70
+ "code" in error &&
71
+ typeof error.code === "string"
72
+ ? error.code
73
+ : undefined;
74
+ /**
75
+ * Turn whatever the file system threw into the refusal the caller gets. Nothing
76
+ * raw gets through: a Node error message carries the server's path, and an
77
+ * unmapped code is logged for the operator (the code only) and answered like an
78
+ * unreadable file instead of surfacing as a 500.
79
+ */
80
+ function asAgentInputError(error) {
81
+ if (error instanceof AgentInputError) {
82
+ return error;
83
+ }
84
+ const code = fsErrorCode(error);
85
+ switch (code) {
86
+ case "ENOENT":
87
+ case "ENOTDIR":
88
+ return notFound();
89
+ case "EACCES":
90
+ case "EPERM":
91
+ case "ELOOP":
92
+ return notReadable();
93
+ case "ENXIO":
94
+ case "EOPNOTSUPP":
95
+ case "ENOTSUP":
96
+ case "EISDIR":
97
+ return notAFile();
98
+ case "ENAMETOOLONG":
99
+ case "EINVAL":
100
+ case "ERR_INVALID_ARG_VALUE":
101
+ return new AgentInputError("attachment path is not a usable path", 400, "VALIDATION_ERROR");
102
+ default:
103
+ logger.warn("[agentInput] unmapped file-system error on an attachment", {
104
+ code,
105
+ name: error instanceof Error ? error.name : typeof error,
106
+ });
107
+ return notReadable();
108
+ }
109
+ }
110
+ /**
111
+ * A file under one of the allowed roots, after symlinks are resolved.
112
+ *
113
+ * A path outside every root is refused before the file system is touched, so
114
+ * the answer for `/etc/passwd` and for `/etc/nothing-here` is the same 403 and
115
+ * the route is not an oracle for which files exist.
116
+ */
117
+ async function readAllowedPath(requested, roots, limit) {
118
+ if (!roots || roots.length === 0) {
119
+ throw forbidden();
120
+ }
121
+ const lexical = path.resolve(requested);
122
+ const realRoots = (await Promise.all(roots.map(async (root) => {
123
+ try {
124
+ return [path.resolve(root), await fs.promises.realpath(root)];
125
+ }
126
+ catch {
127
+ return [];
128
+ }
129
+ }))).flat();
130
+ if (!realRoots.some((root) => isUnder(lexical, root))) {
131
+ throw forbidden();
132
+ }
133
+ let real;
134
+ try {
135
+ real = await fs.promises.realpath(lexical);
136
+ }
137
+ catch (error) {
138
+ throw asAgentInputError(error);
139
+ }
140
+ if (!realRoots.some((root) => isUnder(real, root))) {
141
+ throw forbidden();
142
+ }
143
+ // Open once and judge the descriptor, not the path: a path can be swapped for
144
+ // a symlink between the checks above and a later read. O_NOFOLLOW refuses a
145
+ // symlink in the last component; a directory swapped higher up is not caught,
146
+ // which matters only if something untrusted can write under an allowed root.
147
+ let handle;
148
+ try {
149
+ // O_NONBLOCK: opening a FIFO for reading otherwise waits for a writer, and
150
+ // each such request would hold a libuv worker until one appears. With it
151
+ // the open returns at once and the stat below refuses the FIFO as not a
152
+ // file. Regular files read the same either way. Windows has no constant.
153
+ handle = await fs.promises.open(real, fs.constants.O_RDONLY |
154
+ fs.constants.O_NOFOLLOW |
155
+ (fs.constants.O_NONBLOCK ?? 0));
156
+ }
157
+ catch (error) {
158
+ throw asAgentInputError(error);
159
+ }
160
+ let content;
161
+ try {
162
+ const stat = await handle.stat();
163
+ if (!stat.isFile()) {
164
+ throw notAFile();
165
+ }
166
+ if (stat.size > limit) {
167
+ throw tooLarge(limit);
168
+ }
169
+ content = await readWithinLimit(handle, limit);
170
+ }
171
+ catch (error) {
172
+ await handle.close().catch(() => undefined);
173
+ throw asAgentInputError(error);
174
+ }
175
+ try {
176
+ await handle.close();
177
+ }
178
+ catch (error) {
179
+ throw asAgentInputError(error);
180
+ }
181
+ return content;
182
+ }
183
+ /**
184
+ * Read the whole file, refusing one that outgrew `limit` since it was measured:
185
+ * reads one byte past the limit, so growth is seen rather than buffered.
186
+ */
187
+ async function readWithinLimit(handle, limit) {
188
+ const chunks = [];
189
+ let total = 0;
190
+ while (total <= limit) {
191
+ const chunk = Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, limit + 1 - total));
192
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, total);
193
+ if (bytesRead === 0) {
194
+ break;
195
+ }
196
+ chunks.push(chunk.subarray(0, bytesRead));
197
+ total += bytesRead;
198
+ }
199
+ if (total > limit) {
200
+ throw tooLarge(limit);
201
+ }
202
+ return Buffer.concat(chunks, total);
203
+ }
204
+ async function downloadAttachment(url, limit) {
205
+ // Dynamic import keeps undici off the static import graph; see
206
+ // test/continuous-test-suite-import-cost.ts.
207
+ const { safeDownload } = await import("../../utils/safeFetch.js");
208
+ try {
209
+ return await safeDownload(url, {
210
+ maxBytes: limit,
211
+ label: "agent attachment",
212
+ });
213
+ }
214
+ catch (error) {
215
+ // The reason can name the address a host resolved to; the caller gets none
216
+ // of it, the operator gets it in the log.
217
+ const reason = error instanceof Error ? error.message : String(error);
218
+ logger.warn("[agentInput] refused an attachment URL", {
219
+ reason: reason.replaceAll(url, "<url>"),
220
+ });
221
+ throw new AgentInputError("attachment URL could not be fetched (it must be a public https URL, answer without a redirect and fit the size limit)", 400, "VALIDATION_ERROR");
222
+ }
223
+ }
224
+ /** The bytes of one attachment that is not a `data:` URI, read within `limit`. */
225
+ async function resolveAttachment(entry, roots, limit) {
226
+ if (/^https?:\/\//i.test(entry)) {
227
+ return downloadAttachment(entry, limit);
228
+ }
229
+ if (URL_SCHEME.test(entry)) {
230
+ throw new AgentInputError("attachment must be a data: URI, an https URL, or a path under an allowed directory", 400, "VALIDATION_ERROR");
231
+ }
232
+ return readAllowedPath(entry, roots, limit);
233
+ }
234
+ /**
235
+ * Replace every attachment of an agent request's `input` with vetted content.
236
+ *
237
+ * @throws {AgentInputError} for the first attachment that is refused, or for a
238
+ * request that carries too many attachments or too many bytes.
239
+ */
240
+ export async function resolveAgentInput(input, options) {
241
+ const maxAttachments = limitOr(options?.maxInputAttachments, DEFAULT_MAX_INPUT_ATTACHMENTS);
242
+ const totalBytes = limitOr(options?.maxInputTotalBytes, DEFAULT_MAX_INPUT_TOTAL_BYTES);
243
+ const perAttachment = Math.min(limitOr(options?.maxInputBytes, DEFAULT_MAX_INPUT_BYTES), totalBytes);
244
+ const roots = options?.allowedInputRoots;
245
+ const entries = [...(input.images ?? []), ...(input.files ?? [])];
246
+ // Everything that needs no I/O is judged first, so a request that is already
247
+ // too big costs the server nothing: the count, then each data: URI against
248
+ // its own cap and against the shared budget.
249
+ if (entries.length > maxAttachments) {
250
+ throw tooMany(maxAttachments);
251
+ }
252
+ let remaining = totalBytes;
253
+ for (const entry of entries) {
254
+ if (!isDataUri(entry)) {
255
+ continue;
256
+ }
257
+ if (entry.length > perAttachment) {
258
+ throw tooLarge(perAttachment);
259
+ }
260
+ remaining -= entry.length;
261
+ if (remaining < 0) {
262
+ throw budgetExceeded(totalBytes);
263
+ }
264
+ }
265
+ // One at a time: a request naming many URLs must not open them all at once,
266
+ // and each entry may use only what the ones before it left of the budget.
267
+ const resolveList = async (list) => {
268
+ if (list === undefined) {
269
+ return undefined;
270
+ }
271
+ const resolved = [];
272
+ for (const entry of list) {
273
+ // Exactly the prefix the SDK keys on, so nothing that reaches it as a
274
+ // string is anything but a data URI.
275
+ if (isDataUri(entry)) {
276
+ resolved.push(entry);
277
+ continue;
278
+ }
279
+ const limit = Math.min(perAttachment, remaining);
280
+ let bytes;
281
+ try {
282
+ bytes = await resolveAttachment(entry, roots, limit);
283
+ }
284
+ catch (error) {
285
+ if (limit < perAttachment &&
286
+ error instanceof AgentInputError &&
287
+ error.status === 413) {
288
+ throw budgetExceeded(totalBytes);
289
+ }
290
+ throw error;
291
+ }
292
+ remaining -= bytes.length;
293
+ resolved.push(bytes);
294
+ }
295
+ return resolved;
296
+ };
297
+ const images = await resolveList(input.images);
298
+ const files = await resolveList(input.files);
299
+ return {
300
+ text: input.text,
301
+ ...(images ? { images } : {}),
302
+ ...(files ? { files } : {}),
303
+ };
304
+ }
@@ -443,16 +443,51 @@ export type ClientMiddlewareContext = {
443
443
  /** Additional context data */
444
444
  [key: string]: unknown;
445
445
  };
446
+ /**
447
+ * What `type` holds on the element `NeuroLinkProvider` returns: React's context
448
+ * provider descriptor, an exotic component that carries a `$$typeof` tag and is
449
+ * not meant to be called.
450
+ *
451
+ * Spelled without importing React, for the reason given on `ClientReactElement`.
452
+ * The call signature never returns, so the descriptor cannot be called for a
453
+ * value, and `props` is `unknown` so it still fits React's `JSXElementConstructor`.
454
+ */
455
+ export type ClientReactProviderDescriptor = ((props: unknown) => never) & {
456
+ readonly $$typeof: symbol;
457
+ };
458
+ /**
459
+ * What `NeuroLinkProvider` returns, spelled without importing React.
460
+ *
461
+ * The published declaration of the hooks module cannot name a React type: `react`
462
+ * is an optional peer, and a consumer without it fails to compile the whole
463
+ * `@juspay/neurolink/client` entry point (TS2307). This is the shape of React's
464
+ * element, so a value of this type is still a valid JSX component return and
465
+ * assignable to `ReactNode` and `ReactElement`.
466
+ *
467
+ * `type` is either a host-element tag (a string) or the provider's descriptor,
468
+ * which is what the element returned by `NeuroLinkProvider` carries at runtime.
469
+ * Narrow it before use (`typeof element.type === "string"`); a string method
470
+ * called on it directly is a compile error. Nothing should rely on it: it names
471
+ * React's internal representation, which React may change.
472
+ */
473
+ export type ClientReactElement = {
474
+ type: string | ClientReactProviderDescriptor;
475
+ props: unknown;
476
+ key: string | null;
477
+ };
446
478
  /**
447
479
  * Props for the NeuroLinkProvider React component.
448
480
  *
449
- * `children` is typed as `unknown` so this module stays React-agnostic;
450
- * the provider component in reactHooks.tsx narrows it to `ReactNode`.
481
+ * `children` is typed as `unknown` so the declaration needs no React types.
482
+ * Anything React accepts as a child is accepted, and so is anything else: the
483
+ * type does not check it. The component renders it as a React node, so a value
484
+ * React cannot render (a function, a plain object) compiles and then fails in
485
+ * React at render time.
451
486
  */
452
487
  export type NeuroLinkProviderProps = {
453
488
  /** Client configuration */
454
489
  config: ClientConfig;
455
- /** Child components (ReactNode at runtime) */
490
+ /** Child components. Typed `unknown` and rendered as a React node without a type check. */
456
491
  children: unknown;
457
492
  };
458
493
  /**
@@ -434,6 +434,45 @@ export type AgentExecuteRequest = {
434
434
  /** User ID for context */
435
435
  userId?: string;
436
436
  };
437
+ /** Options for `createAgentRoutes()`. */
438
+ export type ServerAgentRouteOptions = {
439
+ /**
440
+ * Directories a request's `input.images` / `input.files` entries may name as
441
+ * local paths. Empty or omitted (the default) refuses every local path: a
442
+ * server must not read its own disk on a caller's say-so unless the operator
443
+ * names where.
444
+ */
445
+ allowedInputRoots?: string[];
446
+ /**
447
+ * Largest single attachment accepted, in bytes. Default 50 MiB. A `data:` URI
448
+ * is measured by its string length. When `maxInputTotalBytes` is set lower,
449
+ * the smaller of the two is the per-attachment cap.
450
+ */
451
+ maxInputBytes?: number;
452
+ /**
453
+ * Most attachments one request may carry, `input.images` and `input.files`
454
+ * counted together. Default 20. A longer list is refused with 413 before any
455
+ * attachment is read or downloaded.
456
+ */
457
+ maxInputAttachments?: number;
458
+ /**
459
+ * Largest sum of all attachments of one request, in bytes: images and files
460
+ * share this one budget, and a `data:` URI is charged its string length.
461
+ * Default 100 MiB. A request that overflows it is refused with 413, and a
462
+ * file or download that would overflow it stops mid-transfer.
463
+ */
464
+ maxInputTotalBytes?: number;
465
+ };
466
+ /**
467
+ * An agent request's `input` once every attachment has been vetted: URLs and
468
+ * allowed local paths are replaced by the bytes the server read itself, `data:`
469
+ * URIs pass through untouched.
470
+ */
471
+ export type ServerAgentResolvedInput = {
472
+ text: string;
473
+ images?: Array<Buffer | string>;
474
+ files?: Array<Buffer | string>;
475
+ };
437
476
  /**
438
477
  * Agent execution response
439
478
  */
@@ -1110,6 +1149,12 @@ export type CreateRoutesOptions = {
1110
1149
  * always mounted.
1111
1150
  */
1112
1151
  transcribe?: ServerTranscribeRouteOptions;
1152
+ /**
1153
+ * Options for the agent routes (`POST <basePath>/agent/execute` and
1154
+ * `/agent/stream`): where local-path attachments may be read from, and how
1155
+ * large an attachment may be.
1156
+ */
1157
+ agent?: ServerAgentRouteOptions;
1113
1158
  };
1114
1159
  /** Options for `createTranscribeRoutes()`. */
1115
1160
  export type ServerTranscribeRouteOptions = {