@juspay/neurolink 13.8.17 → 13.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +1 -1
  3. package/dist/action/actionExecutor.d.ts +33 -3
  4. package/dist/action/actionExecutor.js +71 -15
  5. package/dist/action/actionInputs.js +4 -1
  6. package/dist/action/cliVersion.d.ts +25 -0
  7. package/dist/action/cliVersion.js +26 -0
  8. package/dist/action/index.d.ts +2 -1
  9. package/dist/action/index.js +2 -1
  10. package/dist/adapters/video/directorPipeline.js +46 -36
  11. package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
  12. package/dist/adapters/video/ffmpegAdapter.js +32 -5
  13. package/dist/adapters/video/frameExtractor.js +1 -1
  14. package/dist/adapters/video/videoMerger.js +5 -2
  15. package/dist/agent/arithmeticEvaluator.d.ts +32 -0
  16. package/dist/agent/arithmeticEvaluator.js +349 -0
  17. package/dist/agent/directTools.d.ts +6 -18
  18. package/dist/agent/directTools.js +17 -56
  19. package/dist/artifacts/artifactStore.d.ts +22 -1
  20. package/dist/artifacts/artifactStore.js +66 -6
  21. package/dist/auth/anthropicOAuth.d.ts +26 -1
  22. package/dist/auth/anthropicOAuth.js +81 -5
  23. package/dist/auth/secureFiles.d.ts +37 -0
  24. package/dist/auth/secureFiles.js +95 -0
  25. package/dist/auth/tokenStore.d.ts +43 -6
  26. package/dist/auth/tokenStore.js +260 -79
  27. package/dist/avatar/providers/DIDAvatar.js +49 -45
  28. package/dist/avatar/providers/ReplicateAvatar.js +33 -40
  29. package/dist/browser/neurolink.min.js +485 -558
  30. package/dist/cli/commands/auth.js +112 -70
  31. package/dist/cli/commands/proxy.d.ts +4 -0
  32. package/dist/cli/commands/proxy.js +128 -13
  33. package/dist/cli/commands/serve.d.ts +10 -1
  34. package/dist/cli/commands/serve.js +98 -17
  35. package/dist/cli/commands/server.js +17 -5
  36. package/dist/cli/index.js +146 -5
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/utils/envManager.js +17 -3
  39. package/dist/cli/utils/serverUtils.d.ts +39 -3
  40. package/dist/cli/utils/serverUtils.js +63 -6
  41. package/dist/core/baseProvider.js +7 -1
  42. package/dist/core/modules/MessageBuilder.js +5 -0
  43. package/dist/core/modules/ToolsManager.d.ts +46 -1
  44. package/dist/core/modules/ToolsManager.js +190 -43
  45. package/dist/files/fileReferenceRegistry.d.ts +2 -23
  46. package/dist/files/fileReferenceRegistry.js +55 -5
  47. package/dist/hitl/hitlManager.js +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +6 -1
  50. package/dist/mcp/auth/tokenStorage.d.ts +7 -0
  51. package/dist/mcp/auth/tokenStorage.js +23 -4
  52. package/dist/mcp/externalServerManager.d.ts +13 -3
  53. package/dist/mcp/externalServerManager.js +154 -136
  54. package/dist/mcp/index.d.ts +2 -0
  55. package/dist/mcp/index.js +3 -0
  56. package/dist/mcp/mcpClientFactory.d.ts +14 -0
  57. package/dist/mcp/mcpClientFactory.js +70 -7
  58. package/dist/mcp/projectConfigTrust.d.ts +72 -0
  59. package/dist/mcp/projectConfigTrust.js +237 -0
  60. package/dist/mcp/transportUrlPolicy.d.ts +39 -0
  61. package/dist/mcp/transportUrlPolicy.js +172 -0
  62. package/dist/music/providers/BeatovenMusic.js +35 -46
  63. package/dist/music/providers/ReplicateMusic.js +33 -40
  64. package/dist/neurolink.d.ts +16 -1
  65. package/dist/neurolink.js +64 -11
  66. package/dist/processors/archive/ArchiveProcessor.js +15 -1
  67. package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
  68. package/dist/processors/archive/zipEntryIndex.js +33 -0
  69. package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
  70. package/dist/processors/base/BaseFileProcessor.js +40 -111
  71. package/dist/processors/data/YamlProcessor.d.ts +13 -1
  72. package/dist/processors/data/YamlProcessor.js +93 -2
  73. package/dist/processors/document/OpenDocumentProcessor.js +7 -2
  74. package/dist/processors/document/PptxProcessor.js +12 -7
  75. package/dist/processors/media/VideoProcessor.js +93 -15
  76. package/dist/proxy/inboundAuth.d.ts +89 -0
  77. package/dist/proxy/inboundAuth.js +219 -0
  78. package/dist/proxy/tokenRefresh.d.ts +6 -0
  79. package/dist/proxy/tokenRefresh.js +13 -1
  80. package/dist/proxy/updateState.js +4 -4
  81. package/dist/rag/document/loaders.d.ts +0 -6
  82. package/dist/rag/document/loaders.js +16 -10
  83. package/dist/rag/ragIntegration.d.ts +2 -2
  84. package/dist/rag/ragIntegration.js +35 -10
  85. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  86. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  87. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  88. package/dist/server/abstract/baseServerAdapter.js +119 -6
  89. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  90. package/dist/server/adapters/expressAdapter.js +78 -6
  91. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  92. package/dist/server/adapters/fastifyAdapter.js +165 -70
  93. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  94. package/dist/server/adapters/honoAdapter.js +99 -21
  95. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  96. package/dist/server/adapters/koaAdapter.js +153 -74
  97. package/dist/server/index.d.ts +2 -0
  98. package/dist/server/index.js +4 -0
  99. package/dist/server/middleware/auth.js +6 -2
  100. package/dist/server/middleware/cache.js +6 -1
  101. package/dist/server/middleware/rateLimit.js +29 -4
  102. package/dist/server/routes/agentRoutes.js +24 -11
  103. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  104. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  105. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  106. package/dist/server/routes/codexProxyRoutes.js +125 -40
  107. package/dist/server/routes/index.d.ts +4 -0
  108. package/dist/server/routes/index.js +35 -4
  109. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  110. package/dist/server/routes/mcpRoutes.js +21 -7
  111. package/dist/server/routes/memoryRoutes.js +7 -6
  112. package/dist/server/routes/toolRoutes.d.ts +12 -2
  113. package/dist/server/routes/toolRoutes.js +33 -3
  114. package/dist/server/routes/transcribeRoutes.js +60 -13
  115. package/dist/server/utils/clientAddress.d.ts +23 -0
  116. package/dist/server/utils/clientAddress.js +65 -0
  117. package/dist/server/utils/clientError.d.ts +35 -0
  118. package/dist/server/utils/clientError.js +129 -0
  119. package/dist/server/utils/pathMatch.d.ts +21 -0
  120. package/dist/server/utils/pathMatch.js +41 -0
  121. package/dist/server/utils/validation.d.ts +9 -0
  122. package/dist/server/utils/validation.js +9 -0
  123. package/dist/server/websocket/WebSocketHandler.js +22 -3
  124. package/dist/server/websocket/frameQueue.d.ts +19 -0
  125. package/dist/server/websocket/frameQueue.js +41 -3
  126. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  127. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  128. package/dist/skills/skillStores.js +5 -0
  129. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  130. package/dist/tasks/store/fileTaskStore.js +11 -4
  131. package/dist/types/attachments.d.ts +117 -0
  132. package/dist/types/attachments.js +15 -0
  133. package/dist/types/auth.d.ts +6 -0
  134. package/dist/types/cli.d.ts +4 -0
  135. package/dist/types/config.d.ts +56 -0
  136. package/dist/types/file.d.ts +8 -0
  137. package/dist/types/hitl.d.ts +8 -0
  138. package/dist/types/index.d.ts +1 -0
  139. package/dist/types/index.js +1 -0
  140. package/dist/types/mcp.d.ts +98 -0
  141. package/dist/types/multimodal.js +0 -41
  142. package/dist/types/processor.d.ts +14 -1
  143. package/dist/types/proxy.d.ts +62 -3
  144. package/dist/types/rag.d.ts +9 -0
  145. package/dist/types/safeFetch.d.ts +40 -0
  146. package/dist/types/server.d.ts +206 -10
  147. package/dist/types/tools.d.ts +57 -0
  148. package/dist/types/transcribe.d.ts +6 -0
  149. package/dist/utils/attachmentPolicy.d.ts +89 -0
  150. package/dist/utils/attachmentPolicy.js +327 -0
  151. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  152. package/dist/utils/dotenvBootstrap.js +172 -31
  153. package/dist/utils/fileDetector.js +115 -65
  154. package/dist/utils/json/coerce.js +11 -1
  155. package/dist/utils/logSanitize.d.ts +5 -0
  156. package/dist/utils/logSanitize.js +8 -0
  157. package/dist/utils/logger.js +73 -1
  158. package/dist/utils/messageBuilder.d.ts +2 -2
  159. package/dist/utils/messageBuilder.js +118 -105
  160. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  161. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  162. package/dist/utils/redis.d.ts +12 -0
  163. package/dist/utils/redis.js +72 -0
  164. package/dist/utils/safeFetch.d.ts +22 -1
  165. package/dist/utils/safeFetch.js +132 -38
  166. package/dist/utils/safeStoreId.d.ts +19 -0
  167. package/dist/utils/safeStoreId.js +29 -0
  168. package/dist/utils/schemaConversion.d.ts +30 -3
  169. package/dist/utils/schemaConversion.js +457 -84
  170. package/dist/utils/sizeGuard.js +2 -2
  171. package/dist/utils/ssrfGuard.d.ts +3 -3
  172. package/dist/utils/ssrfGuard.js +71 -39
  173. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  174. package/dist/voice/eval/sttEvalStore.js +77 -12
  175. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  176. package/dist/voice/livekit/agentDefinition.js +22 -0
  177. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  178. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  179. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  180. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  181. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  182. package/dist/voice/livekit/voiceAgent.js +2 -1
  183. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  184. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  185. package/dist/voice/providers/GeminiLive.js +21 -6
  186. package/dist/voice/providers/GoogleSTT.js +8 -5
  187. package/dist/voice/transcribe.d.ts +2 -2
  188. package/dist/voice/transcribe.js +34 -18
  189. package/dist/voice/whistle/audio.js +11 -5
  190. package/docs-site/static/search-index.json +71 -49
  191. package/package.json +171 -156
  192. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -4,9 +4,8 @@
4
4
  * Uses multi-strategy approach for reliable type identification
5
5
  */
6
6
  import { open, readFile, realpath } from "fs/promises";
7
- import { STATUS_CODES } from "node:http";
8
7
  import { basename, isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
9
- import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
8
+ import { AttachmentRefusedError, downloadAttachment, getAttachmentPolicy, readAttachmentFile, } from "./attachmentPolicy.js";
10
9
  // Lazy-loaded processor singletons — avoids loading heavy media deps
11
10
  // (mediabunny, fluent-ffmpeg, music-metadata, adm-zip) on every generate() call.
12
11
  async function getVideoProcessor() {
@@ -33,7 +32,7 @@ import { detectIsoBmffImageMimeType, hasFtypBoxSignature } from "./isoBmff.js";
33
32
  import { logger } from "./logger.js";
34
33
  import { looksLikeSvgMarkup } from "./markupSniff.js";
35
34
  import { withTimeout } from "./errorHandling.js";
36
- import { normalizeUrlForCache, redactUrlForError, sanitizeErrorCause, } from "./logSanitize.js";
35
+ import { normalizeUrlForCache, sanitizeErrorCause } from "./logSanitize.js";
37
36
  import { mimeHintToExtension, mimeHintToFileType, normalizeMimeHint, } from "./mimeTypeHints.js";
38
37
  import { PDFProcessor } from "./pdfProcessor.js";
39
38
  import { formatKeyframeTimestamp } from "./mediaDuration.js";
@@ -161,14 +160,58 @@ const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];
161
160
  * @param error - Error to check
162
161
  * @returns True if error is retryable (transient network issue)
163
162
  */
163
+ /**
164
+ * The innermost `code` on an error's `cause` chain (bounded walk). Wrappers such
165
+ * as undici's "fetch failed" carry the Node transport code one level down.
166
+ */
167
+ /** The deepest `Error` on a `cause` chain (bounded walk); the input when it has none. */
168
+ function innermostError(error) {
169
+ let cursor = error;
170
+ for (let depth = 0; depth < 6; depth++) {
171
+ const next = cursor instanceof Error ? cursor.cause : undefined;
172
+ if (!(next instanceof Error)) {
173
+ return cursor;
174
+ }
175
+ cursor = next;
176
+ }
177
+ return cursor;
178
+ }
179
+ function innermostErrorCode(error) {
180
+ let found;
181
+ let cursor = error;
182
+ for (let depth = 0; depth < 6 && cursor instanceof Error; depth++) {
183
+ const code = cursor.code;
184
+ if (typeof code === "string") {
185
+ found = code;
186
+ }
187
+ cursor = cursor.cause;
188
+ }
189
+ return found;
190
+ }
164
191
  function isRetryableNetworkError(error) {
165
192
  if (!(error instanceof Error)) {
166
193
  return false;
167
194
  }
168
- const errorMessage = error.message.toLowerCase();
169
- // Extract error code from various error shapes
170
- const errorWithCode = error;
171
- const errorCode = errorWithCode.code?.toUpperCase();
195
+ // `fetch()` (undici) reports a transport failure as a bare "fetch failed"
196
+ // TypeError and keeps the real reason — ECONNRESET, ETIMEDOUT, ... — on
197
+ // `cause`. The safe downloader wraps that again. Walk the chain (bounded) so
198
+ // the classification sees the innermost code and message, not the wrapper.
199
+ const chain = [];
200
+ let cursor = error;
201
+ while (cursor instanceof Error && chain.length < 6) {
202
+ chain.push(cursor);
203
+ cursor = cursor.cause;
204
+ }
205
+ const errorMessage = chain
206
+ .map((e) => e.message)
207
+ .join(" | ")
208
+ .toLowerCase();
209
+ // Extract error code from various error shapes, innermost first
210
+ const errorCode = chain
211
+ .map((e) => e.code)
212
+ .reverse()
213
+ .find((c) => typeof c === "string")
214
+ ?.toUpperCase();
172
215
  // Check for retryable network error codes
173
216
  if (errorCode && RETRYABLE_ERROR_CODES.includes(errorCode)) {
174
217
  return true;
@@ -669,7 +712,7 @@ export class FileDetector {
669
712
  const confidenceThreshold = options?.confidenceThreshold ?? 80;
670
713
  const strategies = [
671
714
  new MagicBytesStrategy(),
672
- new MimeTypeStrategy(responseMimeType),
715
+ new MimeTypeStrategy(responseMimeType, getAttachmentPolicy(options)),
673
716
  new ExtensionStrategy(),
674
717
  new ContentHeuristicStrategy(),
675
718
  ];
@@ -1606,55 +1649,27 @@ export class FileDetector {
1606
1649
  static async loadUrl(url, options) {
1607
1650
  const maxSize = options?.maxSize || 200 * 1024 * 1024; // 200MB default (matches Curator memory-safety cap)
1608
1651
  const timeout = options?.timeout || FileDetector.DEFAULT_NETWORK_TIMEOUT;
1652
+ const policy = getAttachmentPolicy(options);
1609
1653
  return withRetry(async () => {
1610
1654
  try {
1611
- // Dynamic import keeps undici off the package's static import
1612
- // graph — it is only needed once a URL-referenced attachment
1613
- // actually has to be fetched. See
1614
- // test/continuous-test-suite-import-cost.ts.
1615
- const { request } = await import("undici");
1616
- const response = await request(url, {
1617
- dispatcher: await redirectFollowingDispatcher(5),
1618
- method: "GET",
1619
- headersTimeout: timeout,
1620
- bodyTimeout: timeout,
1655
+ // `downloadAttachment` applies the instance's URL policy (HTTPS and
1656
+ // public hosts unless opened up), pins the connection to the vetted
1657
+ // addresses, re-validates every redirect hop and bounds the body
1658
+ // while it streams — a declared oversize response is refused at
1659
+ // the headers, an undeclared one as soon as it passes the cap.
1660
+ const downloaded = await downloadAttachment(url, {
1661
+ policy,
1662
+ maxBytes: maxSize,
1663
+ label: "attached URL",
1664
+ // One budget for headers and body, where the two undici timeouts
1665
+ // used to apply separately.
1666
+ timeoutMs: timeout * 2,
1621
1667
  });
1622
- if (response.statusCode !== 200) {
1623
- // #360: pair the code with its standard reason phrase (e.g. 404 →
1624
- // "Not Found") so the message is readable rather than a bare
1625
- // number. Sourced from Node's own `http.STATUS_CODES` table
1626
- // rather than a hand-written map that could drift from it, and
1627
- // falls back gracefully for a non-standard code that table
1628
- // doesn't recognize. Query string / fragment stays stripped — a
1629
- // presigned URL's token must not be echoed into a thrown error.
1630
- const statusText = STATUS_CODES[response.statusCode];
1631
- throw new Error(`HTTP ${response.statusCode}${statusText ? ` ${statusText}` : ""} fetching ${redactUrlForError(url)}`);
1632
- }
1633
- const contentType = response.headers["content-type"] || "";
1634
- const declaredLength = Number(response.headers["content-length"]);
1635
- if (Number.isFinite(declaredLength) && declaredLength > maxSize) {
1636
- // `request()` resolves after response headers arrive. Close the
1637
- // unread stream before throwing so a declared oversize response
1638
- // never enters the body-reading loop or holds its connection.
1639
- // Undici emits an abort error asynchronously when the stream is
1640
- // destroyed, so consume that expected event before closing it.
1641
- response.body.once("error", () => { });
1642
- response.body.destroy();
1643
- throw new Error(`File too large: ${formatFileSize(declaredLength)} (max: ${formatFileSize(maxSize)})`);
1644
- }
1668
+ const contentType = downloaded.contentType;
1645
1669
  // #323: cache the Content-Type from this GET so a subsequent detection
1646
1670
  // of the same URL needs no HEAD.
1647
1671
  setCachedUrlContentType(url, contentType, Date.now());
1648
- const chunks = [];
1649
- let totalSize = 0;
1650
- for await (const chunk of response.body) {
1651
- totalSize += chunk.length;
1652
- if (totalSize > maxSize) {
1653
- throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
1654
- }
1655
- chunks.push(chunk);
1656
- }
1657
- return { content: Buffer.concat(chunks), contentType };
1672
+ return { content: downloaded.buffer, contentType };
1658
1673
  }
1659
1674
  catch (error) {
1660
1675
  // Node/undici DNS, TLS, and connect-timeout errors embed the full
@@ -1673,10 +1688,13 @@ export class FileDetector {
1673
1688
  // string/object can just as easily carry the full URL), so there is
1674
1689
  // no unconditional `throw error` fallback that would bypass
1675
1690
  // redaction for that case.
1676
- const cause = sanitizeErrorCause(error);
1691
+ // Read the transport code off the ORIGINAL chain before sanitizing:
1692
+ // undici's "fetch failed" TypeError keeps ECONNRESET/ETIMEDOUT on
1693
+ // `cause`, and the sanitized copy deliberately drops that chain.
1694
+ const code = innermostErrorCode(error);
1695
+ const cause = sanitizeErrorCause(innermostError(error));
1677
1696
  const redacted = new Error(cause.message, { cause });
1678
1697
  redacted.name = cause.name;
1679
- const code = cause.code;
1680
1698
  if (code !== undefined) {
1681
1699
  redacted.code = code;
1682
1700
  }
@@ -1709,6 +1727,38 @@ export class FileDetector {
1709
1727
  // check and the open() call routes the read outside the sandbox even
1710
1728
  // though validation passed (TOCTOU). With no sandbox configured there's no
1711
1729
  // boundary to defend, so the original path is used as given.
1730
+ //
1731
+ // Without an `allowedBaseDir` the instance's attachment policy decides:
1732
+ // its `allowedRoots` (when configured) bound the read the same way, and
1733
+ // either way the file is opened O_NOFOLLOW on its real path, regular
1734
+ // files only, under the size cap — see `readAttachmentFile`.
1735
+ if (!options?.allowedBaseDir) {
1736
+ try {
1737
+ const read = await readAttachmentFile(filePath, {
1738
+ policy: getAttachmentPolicy(options),
1739
+ maxBytes: maxSize,
1740
+ label: "attached file",
1741
+ });
1742
+ return read.buffer;
1743
+ }
1744
+ catch (error) {
1745
+ if (error instanceof AttachmentRefusedError) {
1746
+ throw error;
1747
+ }
1748
+ // A failed open/stat (ENOENT, EACCES, ELOOP, …) embeds the full path
1749
+ // in its message. Throw a copy whose message and cause are redacted
1750
+ // to the basename, keeping the code so callers can still classify it.
1751
+ logger.debug("loadFromPath: read failed", { filePath, error });
1752
+ const cause = sanitizeErrorCause(error, { filePath });
1753
+ const failed = new Error(cause.message, { cause });
1754
+ failed.name = cause.name;
1755
+ const code = cause.code;
1756
+ if (code !== undefined) {
1757
+ failed.code = code;
1758
+ }
1759
+ throw failed;
1760
+ }
1761
+ }
1712
1762
  let pathToOpen = filePath;
1713
1763
  if (options?.allowedBaseDir) {
1714
1764
  let base;
@@ -2352,8 +2402,10 @@ class MagicBytesStrategy {
2352
2402
  */
2353
2403
  class MimeTypeStrategy {
2354
2404
  responseMimeType;
2355
- constructor(responseMimeType) {
2405
+ attachmentPolicy;
2406
+ constructor(responseMimeType, attachmentPolicy) {
2356
2407
  this.responseMimeType = responseMimeType;
2408
+ this.attachmentPolicy = attachmentPolicy;
2357
2409
  }
2358
2410
  async detect(input) {
2359
2411
  if (this.responseMimeType !== undefined) {
@@ -2378,20 +2430,18 @@ class MimeTypeStrategy {
2378
2430
  if (contentType === undefined) {
2379
2431
  // Wrap the whole HEAD (request + body drain) in withTimeout so a stalled
2380
2432
  // dump() can't hang detection, per the project's async-timeout guideline.
2433
+ // The same guard as the GET: a HEAD to a private or loopback target
2434
+ // is a request too, and the old pre-flight followed redirects to
2435
+ // wherever they pointed.
2381
2436
  contentType = await withTimeout((async () => {
2382
- // Dynamic import keeps undici off the package's static import
2383
- // graph — it is only needed once a URL-referenced attachment
2384
- // actually has to be fetched. See
2385
- // test/continuous-test-suite-import-cost.ts.
2386
- const { request } = await import("undici");
2387
- const response = await request(input, {
2388
- dispatcher: await redirectFollowingDispatcher(5),
2437
+ const head = await downloadAttachment(input, {
2438
+ policy: this.attachmentPolicy,
2439
+ maxBytes: 0,
2440
+ label: "attached URL",
2389
2441
  method: "HEAD",
2390
- headersTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
2391
- bodyTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
2442
+ timeoutMs: FileDetector.DEFAULT_HEAD_TIMEOUT,
2392
2443
  });
2393
- await response.body.dump();
2394
- return response.headers["content-type"] || "";
2444
+ return head.contentType;
2395
2445
  })(), FileDetector.DEFAULT_HEAD_TIMEOUT);
2396
2446
  setCachedUrlContentType(input, contentType, now);
2397
2447
  }
@@ -181,7 +181,17 @@ function deepUnwrapJsonStrings(value, depth = 0) {
181
181
  if (r.changed) {
182
182
  changed = true;
183
183
  }
184
- out[k] = r.value;
184
+ // Define, never assign: `out[k] = …` with a model-supplied key of
185
+ // `__proto__` invokes the prototype setter and swaps `out`'s prototype
186
+ // for the model's object, so fields hidden from `Object.keys` read back
187
+ // through property access. `JSON.parse` creates such a key as an own
188
+ // data property, and this keeps it one.
189
+ Object.defineProperty(out, k, {
190
+ value: r.value,
191
+ enumerable: true,
192
+ writable: true,
193
+ configurable: true,
194
+ });
185
195
  }
186
196
  return { value: changed ? out : value, changed };
187
197
  }
@@ -15,6 +15,11 @@
15
15
  * jina_ (Jina), fish- (Fish Audio)
16
16
  * - Generic key=value pairs: api_key=…, access_token: …, secret_key=…
17
17
  */
18
+ /**
19
+ * Whether `key` names a value that must never appear in a log — the same list
20
+ * {@link sanitizeRecord} masks, matched case-insensitively.
21
+ */
22
+ export declare function isSensitiveLogKey(key: string): boolean;
18
23
  /**
19
24
  * Truncate `text` to `maxLen` chars then replace embedded secrets with `***`.
20
25
  *
@@ -85,6 +85,14 @@ const SENSITIVE_OBJECT_KEYS = [
85
85
  // PII, not a secret — but it has no business in debug log dumps.
86
86
  "userEmail",
87
87
  ];
88
+ /**
89
+ * Whether `key` names a value that must never appear in a log — the same list
90
+ * {@link sanitizeRecord} masks, matched case-insensitively.
91
+ */
92
+ export function isSensitiveLogKey(key) {
93
+ const lower = key.toLowerCase();
94
+ return SENSITIVE_OBJECT_KEYS.some((name) => name.toLowerCase() === lower);
95
+ }
88
96
  /**
89
97
  * Truncate `text` to `maxLen` chars then replace embedded secrets with `***`.
90
98
  *
@@ -14,6 +14,7 @@
14
14
  * - Tabular data display
15
15
  */
16
16
  import { AsyncLocalStorage } from "node:async_hooks";
17
+ import { isSensitiveLogKey, sanitizeForLog, sanitizeRecord, } from "./logSanitize.js";
17
18
  /**
18
19
  * Identifies which NeuroLink instance is emitting, for the async operation
19
20
  * currently on the stack. Populated by `runInInstanceScope()` — which the SDK
@@ -85,6 +86,69 @@ function cloneLogData(data) {
85
86
  return data;
86
87
  }
87
88
  }
89
+ /**
90
+ * Per-string cap for central redaction. Deliberately far above anything a
91
+ * debug payload carries, so this pass redacts without truncating — the
92
+ * logger's "zero truncation" promise for debug data stands.
93
+ */
94
+ const REDACTION_STRING_CAP = 1_000_000;
95
+ /** Depth past which a payload is handed to `sanitizeRecord` wholesale. */
96
+ const REDACTION_MAX_DEPTH = 32;
97
+ function isPlainObject(value) {
98
+ const proto = Object.getPrototypeOf(value);
99
+ return proto === Object.prototype || proto === null;
100
+ }
101
+ /**
102
+ * Central redaction for `debug` / `info` payloads — the same key list and
103
+ * secret patterns as `sanitizeRecord`, applied to plain objects and arrays.
104
+ *
105
+ * `sanitizeRecord` itself is not used directly on the whole payload because
106
+ * its `Object.entries` walk flattens what debug data is full of: an `Error`
107
+ * becomes `{}` (message and stack gone), a `Buffer` becomes thousands of
108
+ * numeric keys, a `Date`/`Map`/`Set` becomes `{}`. Those are left as they are
109
+ * — `serializeData` and `structuredClone` already know how to carry them —
110
+ * and only plain containers are walked, with sensitive keys masked and
111
+ * string values scrubbed of bearer tokens and known key prefixes. An `Error`
112
+ * gets its message scrubbed in place of the instance.
113
+ *
114
+ * Cost: one recursive pass over objects a call site already chose to log,
115
+ * and only when `shouldLog(level)` was true. Nothing runs for the 99% of
116
+ * `logger.debug` calls that are filtered out.
117
+ */
118
+ function redactLogData(data, depth = 0) {
119
+ if (data === null || data === undefined) {
120
+ return data;
121
+ }
122
+ if (typeof data === "string") {
123
+ return sanitizeForLog(data, REDACTION_STRING_CAP);
124
+ }
125
+ if (typeof data !== "object") {
126
+ return data;
127
+ }
128
+ if (data instanceof Error) {
129
+ const copy = new Error(sanitizeForLog(data.message, REDACTION_STRING_CAP));
130
+ copy.name = data.name;
131
+ copy.stack = data.stack
132
+ ? sanitizeForLog(data.stack, REDACTION_STRING_CAP)
133
+ : undefined;
134
+ return copy;
135
+ }
136
+ if (depth >= REDACTION_MAX_DEPTH) {
137
+ return sanitizeRecord(data, REDACTION_STRING_CAP);
138
+ }
139
+ if (Array.isArray(data)) {
140
+ return data.map((item) => redactLogData(item, depth + 1));
141
+ }
142
+ if (!isPlainObject(data)) {
143
+ // Buffer, Date, Map, Set, class instances: carried as they are.
144
+ return data;
145
+ }
146
+ const out = {};
147
+ for (const [key, value] of Object.entries(data)) {
148
+ out[key] = isSensitiveLogKey(key) ? "***" : redactLogData(value, depth + 1);
149
+ }
150
+ return out;
151
+ }
88
152
  class NeuroLinkLogger {
89
153
  logLevel = "info";
90
154
  logs = [];
@@ -379,10 +443,18 @@ class NeuroLinkLogger {
379
443
  * @param message - The message text to log
380
444
  * @param data - Optional additional context data to include
381
445
  */
382
- log(level, message, data) {
446
+ log(level, message, rawData) {
383
447
  if (!this.shouldLog(level)) {
384
448
  return;
385
449
  }
450
+ // Central redaction: a `debug`/`info` payload is masked by key
451
+ // (`apiKey`, `authorization`, `credentials`, …) and scrubbed of
452
+ // bearer/known-prefix tokens before it is stored, emitted or printed, so
453
+ // the next `logger.debug("…", options)` cannot leak a credential into
454
+ // `getLogs()`, a `log-event` sink or the console. `warn`/`error` are
455
+ // left verbatim: they carry the diagnostics an operator is debugging
456
+ // with, and every credential-bearing path already redacts at source.
457
+ const data = level === "debug" || level === "info" ? redactLogData(rawData) : rawData;
386
458
  const entry = {
387
459
  level,
388
460
  message,
@@ -1,4 +1,4 @@
1
- import type { GenerateOptions, MultimodalChatMessage, StreamOptions, TextGenerationOptions, VisionImageOutputFormat } from "../types/index.js";
1
+ import type { AttachmentPolicy, GenerateOptions, MultimodalChatMessage, StreamOptions, TextGenerationOptions, VisionImageOutputFormat } from "../types/index.js";
2
2
  import type { ModelMessage, ToolHistoryRow, ToolReplayMode } from "../types/index.js";
3
3
  /**
4
4
  * Type-safe conversion from MultimodalChatMessage[] to ModelMessage[]
@@ -100,4 +100,4 @@ export declare function buildMultimodalModelMessages(options: GenerateOptions, p
100
100
  * Entries that are `http(s)` URLs are left alone — they have no bytes yet, and
101
101
  * `processImageToBase64` converts them once they are downloaded.
102
102
  */
103
- export declare function normalizeVisionImageFormats(input: GenerateOptions["input"], outputFormat?: VisionImageOutputFormat): Promise<void>;
103
+ export declare function normalizeVisionImageFormats(input: GenerateOptions["input"], outputFormat?: VisionImageOutputFormat, attachmentPolicy?: AttachmentPolicy): Promise<void>;