@juspay/neurolink 13.8.16 → 13.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +1 -1
  3. package/dist/action/actionExecutor.d.ts +33 -3
  4. package/dist/action/actionExecutor.js +71 -15
  5. package/dist/action/actionInputs.js +4 -1
  6. package/dist/action/cliVersion.d.ts +25 -0
  7. package/dist/action/cliVersion.js +26 -0
  8. package/dist/action/index.d.ts +2 -1
  9. package/dist/action/index.js +2 -1
  10. package/dist/adapters/video/directorPipeline.js +46 -36
  11. package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
  12. package/dist/adapters/video/ffmpegAdapter.js +32 -5
  13. package/dist/adapters/video/frameExtractor.js +1 -1
  14. package/dist/adapters/video/videoMerger.js +5 -2
  15. package/dist/agent/arithmeticEvaluator.d.ts +32 -0
  16. package/dist/agent/arithmeticEvaluator.js +349 -0
  17. package/dist/agent/directTools.d.ts +6 -18
  18. package/dist/agent/directTools.js +17 -56
  19. package/dist/artifacts/artifactStore.d.ts +22 -1
  20. package/dist/artifacts/artifactStore.js +66 -6
  21. package/dist/auth/anthropicOAuth.d.ts +26 -1
  22. package/dist/auth/anthropicOAuth.js +81 -5
  23. package/dist/auth/secureFiles.d.ts +37 -0
  24. package/dist/auth/secureFiles.js +95 -0
  25. package/dist/auth/tokenStore.d.ts +43 -6
  26. package/dist/auth/tokenStore.js +260 -79
  27. package/dist/avatar/providers/DIDAvatar.js +49 -45
  28. package/dist/avatar/providers/ReplicateAvatar.js +33 -40
  29. package/dist/browser/neurolink.min.js +485 -558
  30. package/dist/cli/commands/auth.js +112 -70
  31. package/dist/cli/commands/proxy.d.ts +4 -0
  32. package/dist/cli/commands/proxy.js +128 -13
  33. package/dist/cli/commands/serve.d.ts +10 -1
  34. package/dist/cli/commands/serve.js +98 -17
  35. package/dist/cli/commands/server.js +17 -5
  36. package/dist/cli/index.js +146 -5
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/utils/envManager.js +17 -3
  39. package/dist/cli/utils/serverUtils.d.ts +39 -3
  40. package/dist/cli/utils/serverUtils.js +63 -6
  41. package/dist/core/baseProvider.js +7 -1
  42. package/dist/core/modules/MessageBuilder.js +5 -0
  43. package/dist/core/modules/ToolsManager.d.ts +46 -1
  44. package/dist/core/modules/ToolsManager.js +190 -43
  45. package/dist/files/fileReferenceRegistry.d.ts +2 -23
  46. package/dist/files/fileReferenceRegistry.js +55 -5
  47. package/dist/hitl/hitlManager.js +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +6 -1
  50. package/dist/mcp/auth/tokenStorage.d.ts +7 -0
  51. package/dist/mcp/auth/tokenStorage.js +23 -4
  52. package/dist/mcp/externalServerManager.d.ts +13 -3
  53. package/dist/mcp/externalServerManager.js +154 -136
  54. package/dist/mcp/index.d.ts +2 -0
  55. package/dist/mcp/index.js +3 -0
  56. package/dist/mcp/mcpClientFactory.d.ts +14 -0
  57. package/dist/mcp/mcpClientFactory.js +70 -7
  58. package/dist/mcp/projectConfigTrust.d.ts +72 -0
  59. package/dist/mcp/projectConfigTrust.js +237 -0
  60. package/dist/mcp/transportUrlPolicy.d.ts +39 -0
  61. package/dist/mcp/transportUrlPolicy.js +172 -0
  62. package/dist/music/providers/BeatovenMusic.js +35 -46
  63. package/dist/music/providers/ReplicateMusic.js +33 -40
  64. package/dist/neurolink.d.ts +16 -1
  65. package/dist/neurolink.js +64 -11
  66. package/dist/processors/archive/ArchiveProcessor.js +15 -1
  67. package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
  68. package/dist/processors/archive/zipEntryIndex.js +33 -0
  69. package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
  70. package/dist/processors/base/BaseFileProcessor.js +40 -111
  71. package/dist/processors/data/YamlProcessor.d.ts +13 -1
  72. package/dist/processors/data/YamlProcessor.js +93 -2
  73. package/dist/processors/document/OpenDocumentProcessor.js +7 -2
  74. package/dist/processors/document/PptxProcessor.js +12 -7
  75. package/dist/processors/media/VideoProcessor.js +93 -15
  76. package/dist/proxy/inboundAuth.d.ts +89 -0
  77. package/dist/proxy/inboundAuth.js +219 -0
  78. package/dist/proxy/tokenRefresh.d.ts +6 -0
  79. package/dist/proxy/tokenRefresh.js +13 -1
  80. package/dist/proxy/updateState.js +4 -4
  81. package/dist/rag/document/loaders.d.ts +0 -6
  82. package/dist/rag/document/loaders.js +16 -10
  83. package/dist/rag/ragIntegration.d.ts +2 -2
  84. package/dist/rag/ragIntegration.js +35 -10
  85. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  86. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  87. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  88. package/dist/server/abstract/baseServerAdapter.js +119 -6
  89. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  90. package/dist/server/adapters/expressAdapter.js +83 -8
  91. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  92. package/dist/server/adapters/fastifyAdapter.js +165 -70
  93. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  94. package/dist/server/adapters/honoAdapter.js +99 -21
  95. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  96. package/dist/server/adapters/koaAdapter.js +153 -74
  97. package/dist/server/index.d.ts +2 -0
  98. package/dist/server/index.js +4 -0
  99. package/dist/server/middleware/auth.js +6 -2
  100. package/dist/server/middleware/cache.js +6 -1
  101. package/dist/server/middleware/rateLimit.js +29 -4
  102. package/dist/server/routes/agentRoutes.js +24 -11
  103. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  104. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  105. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  106. package/dist/server/routes/codexProxyRoutes.js +125 -40
  107. package/dist/server/routes/index.d.ts +4 -0
  108. package/dist/server/routes/index.js +35 -4
  109. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  110. package/dist/server/routes/mcpRoutes.js +21 -7
  111. package/dist/server/routes/memoryRoutes.js +7 -6
  112. package/dist/server/routes/toolRoutes.d.ts +12 -2
  113. package/dist/server/routes/toolRoutes.js +33 -3
  114. package/dist/server/routes/transcribeRoutes.js +60 -13
  115. package/dist/server/utils/clientAddress.d.ts +23 -0
  116. package/dist/server/utils/clientAddress.js +65 -0
  117. package/dist/server/utils/clientError.d.ts +35 -0
  118. package/dist/server/utils/clientError.js +129 -0
  119. package/dist/server/utils/expressBody.d.ts +33 -0
  120. package/dist/server/utils/expressBody.js +353 -0
  121. package/dist/server/utils/pathMatch.d.ts +21 -0
  122. package/dist/server/utils/pathMatch.js +41 -0
  123. package/dist/server/utils/validation.d.ts +9 -0
  124. package/dist/server/utils/validation.js +9 -0
  125. package/dist/server/websocket/WebSocketHandler.js +22 -3
  126. package/dist/server/websocket/frameQueue.d.ts +19 -0
  127. package/dist/server/websocket/frameQueue.js +41 -3
  128. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  129. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  130. package/dist/skills/skillStores.js +5 -0
  131. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  132. package/dist/tasks/store/fileTaskStore.js +11 -4
  133. package/dist/types/attachments.d.ts +117 -0
  134. package/dist/types/attachments.js +15 -0
  135. package/dist/types/auth.d.ts +6 -0
  136. package/dist/types/cli.d.ts +4 -0
  137. package/dist/types/config.d.ts +56 -0
  138. package/dist/types/file.d.ts +8 -0
  139. package/dist/types/hitl.d.ts +8 -0
  140. package/dist/types/index.d.ts +1 -0
  141. package/dist/types/index.js +1 -0
  142. package/dist/types/mcp.d.ts +98 -0
  143. package/dist/types/multimodal.js +0 -41
  144. package/dist/types/processor.d.ts +14 -1
  145. package/dist/types/proxy.d.ts +62 -3
  146. package/dist/types/rag.d.ts +9 -0
  147. package/dist/types/safeFetch.d.ts +40 -0
  148. package/dist/types/server.d.ts +255 -10
  149. package/dist/types/tools.d.ts +57 -0
  150. package/dist/types/transcribe.d.ts +6 -0
  151. package/dist/utils/attachmentPolicy.d.ts +89 -0
  152. package/dist/utils/attachmentPolicy.js +327 -0
  153. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  154. package/dist/utils/dotenvBootstrap.js +172 -31
  155. package/dist/utils/fileDetector.js +115 -65
  156. package/dist/utils/json/coerce.js +11 -1
  157. package/dist/utils/logSanitize.d.ts +5 -0
  158. package/dist/utils/logSanitize.js +8 -0
  159. package/dist/utils/logger.js +73 -1
  160. package/dist/utils/messageBuilder.d.ts +2 -2
  161. package/dist/utils/messageBuilder.js +118 -105
  162. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  163. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  164. package/dist/utils/redis.d.ts +12 -0
  165. package/dist/utils/redis.js +72 -0
  166. package/dist/utils/safeFetch.d.ts +22 -1
  167. package/dist/utils/safeFetch.js +132 -38
  168. package/dist/utils/safeStoreId.d.ts +19 -0
  169. package/dist/utils/safeStoreId.js +29 -0
  170. package/dist/utils/schemaConversion.d.ts +30 -3
  171. package/dist/utils/schemaConversion.js +457 -84
  172. package/dist/utils/sizeGuard.js +2 -2
  173. package/dist/utils/ssrfGuard.d.ts +3 -3
  174. package/dist/utils/ssrfGuard.js +71 -39
  175. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  176. package/dist/voice/eval/sttEvalStore.js +77 -12
  177. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  178. package/dist/voice/livekit/agentDefinition.js +22 -0
  179. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  180. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  181. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  182. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  183. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  184. package/dist/voice/livekit/voiceAgent.js +2 -1
  185. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  186. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  187. package/dist/voice/providers/GeminiLive.js +21 -6
  188. package/dist/voice/providers/GoogleSTT.js +8 -5
  189. package/dist/voice/transcribe.d.ts +2 -2
  190. package/dist/voice/transcribe.js +34 -18
  191. package/dist/voice/whistle/audio.js +11 -5
  192. package/docs-site/static/search-index.json +75 -52
  193. package/package.json +171 -156
  194. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Attachment policy — one answer for every SDK loader that turns a
3
+ * caller-supplied string into bytes.
4
+ *
5
+ * Two primitives, both policy-aware:
6
+ *
7
+ * - {@link readAttachmentFile}: realpath containment in `allowedRoots`
8
+ * (when configured), `O_NOFOLLOW` open on the real path, regular files
9
+ * only, size-capped read that notices a file growing under it.
10
+ * - {@link downloadAttachment}: `safeDownloadWithMeta` with the policy's
11
+ * URL switches — HTTPS-only and public hosts by default, redirects
12
+ * re-validated per hop, caller headers never replayed across one.
13
+ *
14
+ * The policy itself is resolved by {@link resolveAttachmentPolicy} from the
15
+ * constructor's `attachments` config and the `NEUROLINK_ATTACHMENT_*`
16
+ * variables, and travels on the per-call options (`attachmentPolicy`). A
17
+ * loader that is reached without one — a host calling `WebLoader` directly, a
18
+ * processor used on its own — falls back to the environment-derived default,
19
+ * so the operator switches apply everywhere.
20
+ *
21
+ * @module utils/attachmentPolicy
22
+ */
23
+ import { constants as fsConstants } from "node:fs";
24
+ import { open, realpath } from "node:fs/promises";
25
+ import { basename, delimiter as pathDelimiter, isAbsolute, relative, resolve as resolvePath, sep, } from "node:path";
26
+ import { logger } from "./logger.js";
27
+ import { redactUrlForError } from "./logSanitize.js";
28
+ import { defaultRedirectHops, safeDownloadWithMeta } from "./safeFetch.js";
29
+ const POLICY_KIND = "neurolink-attachment-policy";
30
+ /** Bytes read per `handle.read()` while draining a file under its cap. */
31
+ const READ_CHUNK_BYTES = 1024 * 1024;
32
+ const ENV_ROOTS = "NEUROLINK_ATTACHMENT_ROOTS";
33
+ const ENV_ALLOW_INSECURE = "NEUROLINK_ATTACHMENT_ALLOW_INSECURE_URLS";
34
+ const ENV_ALLOW_PRIVATE = "NEUROLINK_ATTACHMENT_ALLOW_PRIVATE_NETWORK";
35
+ const ENV_FOLLOW_REDIRECTS = "NEUROLINK_ATTACHMENT_FOLLOW_REDIRECTS";
36
+ const ENV_MAX_BYTES = "NEUROLINK_ATTACHMENT_MAX_BYTES";
37
+ /**
38
+ * An error the loaders throw when the policy refuses an attachment. Carries a
39
+ * stable `code`; the message names the option that would allow the read.
40
+ */
41
+ export class AttachmentRefusedError extends Error {
42
+ code;
43
+ constructor(code, message, cause) {
44
+ super(message, cause === undefined ? undefined : { cause });
45
+ this.name = "AttachmentRefusedError";
46
+ this.code = code;
47
+ }
48
+ }
49
+ function envFlag(name) {
50
+ const raw = process.env[name];
51
+ if (raw === undefined) {
52
+ return undefined;
53
+ }
54
+ const value = raw.trim().toLowerCase();
55
+ if (value === "" || value === "0" || value === "false" || value === "no") {
56
+ return false;
57
+ }
58
+ return true;
59
+ }
60
+ function envRoots() {
61
+ const raw = process.env[ENV_ROOTS];
62
+ if (raw === undefined || raw.trim() === "") {
63
+ return undefined;
64
+ }
65
+ // `path.delimiter` is `:` on POSIX and `;` on Windows; accept both so a
66
+ // value copied between platforms still parses.
67
+ const roots = raw
68
+ .split(/[;:]/)
69
+ .map((part) => part.trim())
70
+ .filter((part) => part.length > 0);
71
+ if (pathDelimiter === ";" && roots.length > 0) {
72
+ // On Windows a drive letter also contains `:`; re-split on `;` only.
73
+ return raw
74
+ .split(";")
75
+ .map((part) => part.trim())
76
+ .filter((part) => part.length > 0);
77
+ }
78
+ return roots.length > 0 ? roots : undefined;
79
+ }
80
+ function envMaxBytes() {
81
+ const raw = process.env[ENV_MAX_BYTES];
82
+ if (raw === undefined || raw.trim() === "") {
83
+ return undefined;
84
+ }
85
+ const parsed = Number(raw);
86
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : undefined;
87
+ }
88
+ /**
89
+ * Resolve the policy an instance runs under. An explicit config field wins
90
+ * over its environment variable; the defaults are the safe ones.
91
+ */
92
+ export function resolveAttachmentPolicy(config) {
93
+ const roots = config?.allowedRoots ?? envRoots();
94
+ const resolvedRoots = roots && roots.length > 0
95
+ ? roots.map((root) => resolvePath(root))
96
+ : undefined;
97
+ const maxBytes = config?.maxBytes !== undefined && config.maxBytes > 0
98
+ ? Math.floor(config.maxBytes)
99
+ : envMaxBytes();
100
+ return Object.freeze({
101
+ kind: POLICY_KIND,
102
+ allowedRoots: resolvedRoots ? Object.freeze(resolvedRoots) : undefined,
103
+ allowInsecureUrls: config?.allowInsecureUrls ?? envFlag(ENV_ALLOW_INSECURE) ?? false,
104
+ allowPrivateNetwork: config?.allowPrivateNetwork ?? envFlag(ENV_ALLOW_PRIVATE) ?? false,
105
+ followRedirects: config?.followRedirects ?? envFlag(ENV_FOLLOW_REDIRECTS) ?? true,
106
+ maxBytes,
107
+ });
108
+ }
109
+ /** Runtime guard for a policy that travelled on an untyped options bag. */
110
+ export function isAttachmentPolicy(value) {
111
+ return (typeof value === "object" &&
112
+ value !== null &&
113
+ value.kind === POLICY_KIND);
114
+ }
115
+ /**
116
+ * The policy stamped on a call's options, or the environment-derived default.
117
+ * Accepts any options bag so loaders deep in the file pipeline can read it off
118
+ * whatever reached them.
119
+ */
120
+ export function getAttachmentPolicy(carrier) {
121
+ const stamped = carrier?.attachmentPolicy;
122
+ return isAttachmentPolicy(stamped) ? stamped : resolveAttachmentPolicy();
123
+ }
124
+ /** The lower of a loader's own cap and the policy's global cap. */
125
+ export function effectiveAttachmentCap(policy, loaderCap) {
126
+ return policy.maxBytes !== undefined
127
+ ? Math.min(loaderCap, policy.maxBytes)
128
+ : loaderCap;
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // Local files
132
+ // ---------------------------------------------------------------------------
133
+ let warnedUnrestrictedRoots = false;
134
+ /**
135
+ * Reset the one-time "no roots configured" warning. Test hook.
136
+ * @internal
137
+ */
138
+ export function resetAttachmentRootsWarning() {
139
+ warnedUnrestrictedRoots = false;
140
+ }
141
+ function warnUnrestrictedOnce(label) {
142
+ if (warnedUnrestrictedRoots) {
143
+ return;
144
+ }
145
+ warnedUnrestrictedRoots = true;
146
+ logger.warn(`[NeuroLink] Reading a local ${label} with no attachment roots configured. ` +
147
+ "Any readable file on this host can be attached by naming its path. " +
148
+ "If paths come from untrusted callers, set `attachments.allowedRoots` " +
149
+ `on the NeuroLink instance or the ${ENV_ROOTS} environment variable.`);
150
+ }
151
+ function isUnder(candidate, root) {
152
+ const rel = relative(root, candidate);
153
+ return (rel === "" ||
154
+ (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel)));
155
+ }
156
+ /**
157
+ * Check `filePath` against the policy's roots and return the real path to
158
+ * open. Without roots the real path is still resolved (so the later
159
+ * `O_NOFOLLOW` open behaves identically for a symlinked input), and the
160
+ * one-time warning fires.
161
+ *
162
+ * @throws {AttachmentRefusedError} `ATTACHMENT_OUTSIDE_ROOTS`
163
+ */
164
+ export async function resolveAttachmentPath(filePath, policy, label) {
165
+ const lexical = resolvePath(filePath);
166
+ const roots = policy.allowedRoots;
167
+ if (!roots || roots.length === 0) {
168
+ warnUnrestrictedOnce(label);
169
+ // Without roots there is no boundary whose shape a message could leak, so
170
+ // the OS error (ENOENT, EACCES, ELOOP) surfaces unchanged for callers to map.
171
+ return await realpath(lexical);
172
+ }
173
+ const denied = () => new AttachmentRefusedError("ATTACHMENT_OUTSIDE_ROOTS", `Access denied: ${label} "${basename(filePath)}" is outside the configured attachment roots (attachments.allowedRoots / ${ENV_ROOTS})`);
174
+ const realRoots = (await Promise.all(roots.map(async (root) => {
175
+ try {
176
+ return [root, await realpath(root)];
177
+ }
178
+ catch {
179
+ return [root];
180
+ }
181
+ }))).flat();
182
+ // Lexical check first, so a path outside every root is refused before the
183
+ // file system is consulted and the answer for a missing file and a
184
+ // forbidden one is the same.
185
+ if (!realRoots.some((root) => isUnder(lexical, root))) {
186
+ throw denied();
187
+ }
188
+ let real;
189
+ try {
190
+ real = await realpath(lexical);
191
+ }
192
+ catch (error) {
193
+ const code = error?.code;
194
+ if (code === "ENOENT" || code === "ENOTDIR") {
195
+ throw error;
196
+ }
197
+ throw denied();
198
+ }
199
+ if (!realRoots.some((root) => isUnder(real, root))) {
200
+ throw denied();
201
+ }
202
+ return real;
203
+ }
204
+ /**
205
+ * Read the whole file, refusing one that outgrew `limit` since it was
206
+ * measured: reads one byte past the limit, so growth is seen, not buffered.
207
+ */
208
+ async function readWithinLimit(handle, limit) {
209
+ const chunks = [];
210
+ let total = 0;
211
+ while (total <= limit) {
212
+ const chunk = Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, limit + 1 - total));
213
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, total);
214
+ if (bytesRead === 0) {
215
+ break;
216
+ }
217
+ chunks.push(chunk.subarray(0, bytesRead));
218
+ total += bytesRead;
219
+ }
220
+ if (total > limit) {
221
+ return null;
222
+ }
223
+ return Buffer.concat(chunks, total);
224
+ }
225
+ /**
226
+ * Read a local attachment under the policy.
227
+ *
228
+ * Containment is judged on the real path (see {@link resolveAttachmentPath});
229
+ * the open is `O_NOFOLLOW | O_NONBLOCK` on that real path so a symlink swapped
230
+ * in after the check is refused by the kernel and a FIFO cannot park a worker;
231
+ * the descriptor is then stat'd (regular files only, size under the cap) and
232
+ * drained under the same cap.
233
+ *
234
+ * @throws {AttachmentRefusedError} with `ATTACHMENT_OUTSIDE_ROOTS`,
235
+ * `ATTACHMENT_NOT_A_FILE` or `ATTACHMENT_TOO_LARGE`; other file-system
236
+ * errors (ENOENT, EACCES, ELOOP, …) propagate unchanged.
237
+ */
238
+ export async function readAttachmentFile(filePath, options) {
239
+ const policy = options.policy ?? resolveAttachmentPolicy();
240
+ const limit = effectiveAttachmentCap(policy, options.maxBytes);
241
+ const real = await resolveAttachmentPath(filePath, policy, options.label);
242
+ const handle = await open(real, fsConstants.O_RDONLY |
243
+ (fsConstants.O_NOFOLLOW ?? 0) |
244
+ (fsConstants.O_NONBLOCK ?? 0));
245
+ try {
246
+ const stat = await handle.stat();
247
+ if (!stat.isFile()) {
248
+ throw new AttachmentRefusedError("ATTACHMENT_NOT_A_FILE", `Not a file: ${options.label} "${basename(filePath)}"`);
249
+ }
250
+ if (stat.size > limit) {
251
+ throw new AttachmentRefusedError("ATTACHMENT_TOO_LARGE", `${options.label} "${basename(filePath)}" is ${stat.size} bytes; the limit is ${limit} bytes`);
252
+ }
253
+ const buffer = await readWithinLimit(handle, limit);
254
+ if (buffer === null) {
255
+ throw new AttachmentRefusedError("ATTACHMENT_TOO_LARGE", `${options.label} "${basename(filePath)}" grew past the ${limit}-byte limit while being read`);
256
+ }
257
+ return { buffer, realPath: real };
258
+ }
259
+ finally {
260
+ await handle.close().catch(() => undefined);
261
+ }
262
+ }
263
+ // ---------------------------------------------------------------------------
264
+ // URLs
265
+ // ---------------------------------------------------------------------------
266
+ /**
267
+ * Quick scheme check that names the switch, so a refused `http://` URL tells
268
+ * the caller what to set rather than only that HTTPS is required.
269
+ *
270
+ * @throws {AttachmentRefusedError} `ATTACHMENT_URL_REFUSED`
271
+ */
272
+ export function assertAttachmentUrlScheme(url, policy, label) {
273
+ let parsed;
274
+ try {
275
+ parsed = new URL(url);
276
+ }
277
+ catch {
278
+ throw new AttachmentRefusedError("ATTACHMENT_URL_REFUSED", `${label}: "${redactUrlForError(url)}" is not a valid URL`);
279
+ }
280
+ if (parsed.protocol === "https:") {
281
+ return;
282
+ }
283
+ if (parsed.protocol === "http:") {
284
+ if (policy.allowInsecureUrls) {
285
+ return;
286
+ }
287
+ throw new AttachmentRefusedError("ATTACHMENT_URL_REFUSED", `${label}: plain http:// URLs are refused by default (${redactUrlForError(url)}). ` +
288
+ `Set attachments.allowInsecureUrls on the NeuroLink instance or ${ENV_ALLOW_INSECURE}=1 to allow them.`);
289
+ }
290
+ throw new AttachmentRefusedError("ATTACHMENT_URL_REFUSED", `${label}: only http(s) URLs can be fetched; got "${parsed.protocol}//"`);
291
+ }
292
+ /**
293
+ * Download a caller-supplied URL under the policy. Builds on
294
+ * `safeDownloadWithMeta`: DNS-pinned connection, bounded body, redirects only
295
+ * when the policy allows and then re-validated per hop with the caller's
296
+ * headers dropped.
297
+ *
298
+ * @throws {AttachmentRefusedError} `ATTACHMENT_URL_REFUSED` when the URL is
299
+ * refused by the policy or the SSRF guard; transport and HTTP errors
300
+ * propagate as plain errors from `safeDownloadWithMeta`.
301
+ */
302
+ export async function downloadAttachment(url, options) {
303
+ const policy = options.policy ?? resolveAttachmentPolicy();
304
+ assertAttachmentUrlScheme(url, policy, options.label);
305
+ try {
306
+ return await safeDownloadWithMeta(url, {
307
+ maxBytes: effectiveAttachmentCap(policy, options.maxBytes),
308
+ label: options.label,
309
+ headers: options.headers,
310
+ timeoutMs: options.timeoutMs,
311
+ signal: options.signal,
312
+ method: options.method,
313
+ maxRedirects: defaultRedirectHops(policy.followRedirects),
314
+ allowInsecure: policy.allowInsecureUrls,
315
+ allowPrivateNetwork: policy.allowPrivateNetwork,
316
+ });
317
+ }
318
+ catch (error) {
319
+ const message = error instanceof Error ? error.message : String(error);
320
+ if (/rejected:|blocked range|is permitted|internal host|single-label/.test(message)) {
321
+ throw new AttachmentRefusedError("ATTACHMENT_URL_REFUSED", `${options.label}: ${message}. ` +
322
+ `Private, loopback and link-local targets need attachments.allowPrivateNetwork (${ENV_ALLOW_PRIVATE}=1); ` +
323
+ `plain http needs attachments.allowInsecureUrls (${ENV_ALLOW_INSECURE}=1).`, error);
324
+ }
325
+ throw error;
326
+ }
327
+ }
@@ -1,25 +1,54 @@
1
1
  /**
2
- * Implicit `.env` bootstrap, shared by the SDK entry point and the CLI.
2
+ * Implicit `.env` bootstrap for the CLI, and an explicit one for SDK hosts.
3
3
  *
4
- * Both sites used to call dotenv's `config()` with no options beyond
5
- * `quiet`, and a direct `config()` call does NOT read `DOTENV_CONFIG_PATH`
6
- * — only dotenv's own `dotenv/config` preload entry does. So the strip this
7
- * repo documents and uses to vet suites,
4
+ * Who loads `.env`:
8
5
  *
9
- * env -i HOME=… PATH=… CI=true DOTENV_CONFIG_PATH=/dev/null …
6
+ * - The **CLI** loads `<cwd>/.env` at startup (or `DOTENV_CONFIG_PATH`),
7
+ * because a terminal session in a project directory is the use case the
8
+ * file exists for. `NEUROLINK_DOTENV=0` turns that off.
9
+ * - The **SDK** does not. Importing a library must not read the working
10
+ * directory: a dependency that runs inside an untrusted checkout would
11
+ * otherwise pick up that checkout's `.env`. A host that wants the old
12
+ * behaviour calls `loadDotenv()` itself, or starts the process with
13
+ * `NEUROLINK_DOTENV=1` in the real environment (the variable cannot come
14
+ * from the file it enables).
10
15
  *
11
- * stripped nothing the moment a suite imported the built SDK: importing
12
- * `dist/index.js` loads `src/lib/neurolink.ts`, which loaded `.env` from the
13
- * working directory regardless. A credential the strip was supposed to
14
- * remove was present at every read site, and a suite that only passes
15
- * because of it looks identical to one that does not need it. See #1744;
16
- * the CI-only failure in #1735 is what surfaced it.
16
+ * What is refused even when a file is loaded. dotenv never overrides a
17
+ * variable that is already set, but it sets every one that is not — and
18
+ * among those are the variables that decide *where* a request goes: the
19
+ * provider `*_BASE_URL` / `*_ENDPOINT(S)` overrides, `NEUROLINK_*_DIR`
20
+ * directories, and `DOTENV_CONFIG_PATH`. A `.env` that supplies one of them
21
+ * can redirect a key exported in the shell to a host of its choosing. So,
22
+ * for exactly those names, the file has to be trustworthy: owned by the
23
+ * current user and not group/world-writable. Otherwise they are deleted
24
+ * again and named on stderr. When they are kept, a one-line notice says
25
+ * which ones the file set.
17
26
  *
18
- * Honouring the variable here makes that strip real. It is additive: with
19
- * `DOTENV_CONFIG_PATH` unset — which is every existing caller, SDK consumers
20
- * included — this does exactly what it did before, loading `.env` from the
21
- * current working directory. Only a caller that explicitly sets the variable
22
- * sees a difference, and what it then sees is dotenv's own documented
23
- * meaning for it.
27
+ * `DOTENV_CONFIG_PATH` is honoured because a direct `config()` call does not
28
+ * read it on its own (only dotenv's `dotenv/config` preload does), and the
29
+ * test strip `DOTENV_CONFIG_PATH=/dev/null` depends on it (#1744).
24
30
  */
25
- export declare function loadProcessDotenv(): Promise<void>;
31
+ import type { DotenvBootstrapOptions, DotenvBootstrapResult } from "../types/index.js";
32
+ /** `0` / `false` disables every implicit load; `1` / `true` opts the SDK in. */
33
+ export declare const DOTENV_TOGGLE_ENV = "NEUROLINK_DOTENV";
34
+ /**
35
+ * Names whose value decides where traffic or file access goes. Built from
36
+ * the provider descriptors at load time as well (see
37
+ * `collectDescriptorEndpointEnvNames`); this pattern is the floor that
38
+ * holds even when the descriptors cannot be imported.
39
+ */
40
+ export declare const ENDPOINT_OVERRIDE_ENV_PATTERN: RegExp;
41
+ /** True when `NEUROLINK_DOTENV` explicitly opts an SDK import into loading. */
42
+ export declare function isSdkDotenvOptIn(): boolean;
43
+ /**
44
+ * Load `.env` into `process.env` under the rules in this module's header and
45
+ * report what happened. Never throws: a missing dotenv module degrades to
46
+ * "nothing loaded".
47
+ */
48
+ export declare function loadProcessDotenv(options?: DotenvBootstrapOptions): Promise<DotenvBootstrapResult>;
49
+ /**
50
+ * Explicit opt-in for SDK hosts that want `.env` loaded the way the CLI does,
51
+ * with the same endpoint-override checks. Call it before constructing
52
+ * `NeuroLink`.
53
+ */
54
+ export declare function loadDotenv(options?: DotenvBootstrapOptions): Promise<DotenvBootstrapResult>;
@@ -1,46 +1,187 @@
1
1
  /**
2
- * Implicit `.env` bootstrap, shared by the SDK entry point and the CLI.
2
+ * Implicit `.env` bootstrap for the CLI, and an explicit one for SDK hosts.
3
3
  *
4
- * Both sites used to call dotenv's `config()` with no options beyond
5
- * `quiet`, and a direct `config()` call does NOT read `DOTENV_CONFIG_PATH`
6
- * — only dotenv's own `dotenv/config` preload entry does. So the strip this
7
- * repo documents and uses to vet suites,
4
+ * Who loads `.env`:
8
5
  *
9
- * env -i HOME=… PATH=… CI=true DOTENV_CONFIG_PATH=/dev/null …
6
+ * - The **CLI** loads `<cwd>/.env` at startup (or `DOTENV_CONFIG_PATH`),
7
+ * because a terminal session in a project directory is the use case the
8
+ * file exists for. `NEUROLINK_DOTENV=0` turns that off.
9
+ * - The **SDK** does not. Importing a library must not read the working
10
+ * directory: a dependency that runs inside an untrusted checkout would
11
+ * otherwise pick up that checkout's `.env`. A host that wants the old
12
+ * behaviour calls `loadDotenv()` itself, or starts the process with
13
+ * `NEUROLINK_DOTENV=1` in the real environment (the variable cannot come
14
+ * from the file it enables).
10
15
  *
11
- * stripped nothing the moment a suite imported the built SDK: importing
12
- * `dist/index.js` loads `src/lib/neurolink.ts`, which loaded `.env` from the
13
- * working directory regardless. A credential the strip was supposed to
14
- * remove was present at every read site, and a suite that only passes
15
- * because of it looks identical to one that does not need it. See #1744;
16
- * the CI-only failure in #1735 is what surfaced it.
16
+ * What is refused even when a file is loaded. dotenv never overrides a
17
+ * variable that is already set, but it sets every one that is not — and
18
+ * among those are the variables that decide *where* a request goes: the
19
+ * provider `*_BASE_URL` / `*_ENDPOINT(S)` overrides, `NEUROLINK_*_DIR`
20
+ * directories, and `DOTENV_CONFIG_PATH`. A `.env` that supplies one of them
21
+ * can redirect a key exported in the shell to a host of its choosing. So,
22
+ * for exactly those names, the file has to be trustworthy: owned by the
23
+ * current user and not group/world-writable. Otherwise they are deleted
24
+ * again and named on stderr. When they are kept, a one-line notice says
25
+ * which ones the file set.
17
26
  *
18
- * Honouring the variable here makes that strip real. It is additive: with
19
- * `DOTENV_CONFIG_PATH` unset — which is every existing caller, SDK consumers
20
- * included — this does exactly what it did before, loading `.env` from the
21
- * current working directory. Only a caller that explicitly sets the variable
22
- * sees a difference, and what it then sees is dotenv's own documented
23
- * meaning for it.
27
+ * `DOTENV_CONFIG_PATH` is honoured because a direct `config()` call does not
28
+ * read it on its own (only dotenv's `dotenv/config` preload does), and the
29
+ * test strip `DOTENV_CONFIG_PATH=/dev/null` depends on it (#1744).
24
30
  */
25
- export async function loadProcessDotenv() {
31
+ import { statSync } from "node:fs";
32
+ import { homedir } from "node:os";
33
+ import * as path from "node:path";
34
+ /** `0` / `false` disables every implicit load; `1` / `true` opts the SDK in. */
35
+ export const DOTENV_TOGGLE_ENV = "NEUROLINK_DOTENV";
36
+ /**
37
+ * Names whose value decides where traffic or file access goes. Built from
38
+ * the provider descriptors at load time as well (see
39
+ * `collectDescriptorEndpointEnvNames`); this pattern is the floor that
40
+ * holds even when the descriptors cannot be imported.
41
+ */
42
+ export const ENDPOINT_OVERRIDE_ENV_PATTERN = /_BASE_URL$|_ENDPOINTS?$|^NEUROLINK_.*_DIR$|^DOTENV_CONFIG_PATH$/;
43
+ function isDisabled() {
44
+ const value = process.env[DOTENV_TOGGLE_ENV];
45
+ return value === "0" || value === "false";
46
+ }
47
+ /** True when `NEUROLINK_DOTENV` explicitly opts an SDK import into loading. */
48
+ export function isSdkDotenvOptIn() {
49
+ const value = process.env[DOTENV_TOGGLE_ENV];
50
+ return value === "1" || value === "true";
51
+ }
52
+ async function collectDescriptorEndpointEnvNames() {
53
+ const names = new Set();
54
+ try {
55
+ const { PROVIDER_DESCRIPTORS } = await import("../factories/providerDescriptors.js");
56
+ for (const descriptor of PROVIDER_DESCRIPTORS) {
57
+ const { baseURL, baseURLFallbacks, extraRequired } = descriptor.envVars;
58
+ if (baseURL) {
59
+ names.add(baseURL);
60
+ }
61
+ for (const name of baseURLFallbacks ?? []) {
62
+ names.add(name);
63
+ }
64
+ for (const name of extraRequired ?? []) {
65
+ if (/ENDPOINT|URL|HOST/.test(name)) {
66
+ names.add(name);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ catch {
72
+ // The pattern above still covers every shipped name.
73
+ }
74
+ return names;
75
+ }
76
+ function inspectDotenvFile(envPath) {
77
+ const relativeToHome = path.relative(homedir(), envPath);
78
+ const outsideHome = relativeToHome.startsWith("..") || path.isAbsolute(relativeToHome);
79
+ try {
80
+ const stat = statSync(envPath);
81
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
82
+ if (uid !== null && stat.uid !== uid) {
83
+ return {
84
+ trustworthy: false,
85
+ reason: "the file is not owned by the current user",
86
+ outsideHome,
87
+ };
88
+ }
89
+ if (process.platform !== "win32" && (stat.mode & 0o022) !== 0) {
90
+ return {
91
+ trustworthy: false,
92
+ reason: "the file is group- or world-writable",
93
+ outsideHome,
94
+ };
95
+ }
96
+ return { trustworthy: true, outsideHome };
97
+ }
98
+ catch (error) {
99
+ return {
100
+ trustworthy: false,
101
+ reason: `the file could not be inspected (${error instanceof Error ? error.message : String(error)})`,
102
+ outsideHome,
103
+ };
104
+ }
105
+ }
106
+ /**
107
+ * Load `.env` into `process.env` under the rules in this module's header and
108
+ * report what happened. Never throws: a missing dotenv module degrades to
109
+ * "nothing loaded".
110
+ */
111
+ export async function loadProcessDotenv(options = {}) {
112
+ const notify = options.notify ??
113
+ ((line) => {
114
+ process.stderr.write(`${line}\n`);
115
+ });
116
+ const empty = (reason) => ({
117
+ loaded: false,
118
+ supplied: [],
119
+ endpointOverrides: [],
120
+ refused: [],
121
+ ...(reason ? { reason } : {}),
122
+ });
123
+ if (isDisabled()) {
124
+ return empty(`${DOTENV_TOGGLE_ENV} disables .env loading`);
125
+ }
26
126
  try {
27
127
  // Suppress dotenv v17's stdout banner — it pollutes CLI JSON output.
28
- // Redundant next to the explicit `quiet` below, which is what actually
29
- // silences a direct `config()` call; kept because both call sites set it
30
- // and something downstream may still read it.
31
128
  process.env.DOTENV_CONFIG_QUIET = process.env.DOTENV_CONFIG_QUIET ?? "true";
32
129
  const { config } = await import("dotenv");
33
- const path = process.env.DOTENV_CONFIG_PATH;
130
+ const configuredPath = process.env.DOTENV_CONFIG_PATH;
131
+ const envPath = path.resolve(process.cwd(), configuredPath && configuredPath.length > 0 ? configuredPath : ".env");
132
+ const before = new Set(Object.keys(process.env));
34
133
  // Pass `path` only when set: `{ path: undefined }` is not the same as
35
- // omitting it for every dotenv version, and the default must stay
36
- // byte-for-byte what it was.
37
- config(path ? { quiet: true, path } : { quiet: true });
134
+ // omitting it for every dotenv version.
135
+ const outcome = configuredPath
136
+ ? config({ quiet: true, path: configuredPath })
137
+ : config({ quiet: true });
138
+ const parsed = outcome.parsed ?? {};
139
+ const supplied = Object.keys(parsed).filter((name) => !before.has(name) && process.env[name] !== undefined);
140
+ const result = {
141
+ loaded: outcome.error === undefined,
142
+ path: envPath,
143
+ supplied,
144
+ endpointOverrides: [],
145
+ refused: [],
146
+ };
147
+ if (supplied.length === 0) {
148
+ return result;
149
+ }
150
+ const descriptorNames = await collectDescriptorEndpointEnvNames();
151
+ const endpointOverrides = supplied.filter((name) => ENDPOINT_OVERRIDE_ENV_PATTERN.test(name) || descriptorNames.has(name));
152
+ result.endpointOverrides = endpointOverrides;
153
+ if (endpointOverrides.length === 0) {
154
+ return result;
155
+ }
156
+ const verdict = inspectDotenvFile(envPath);
157
+ const names = endpointOverrides.join(", ");
158
+ if (!verdict.trustworthy) {
159
+ for (const name of endpointOverrides) {
160
+ delete process.env[name];
161
+ }
162
+ result.refused = endpointOverrides;
163
+ result.endpointOverrides = [];
164
+ result.reason = verdict.reason;
165
+ notify(`[neurolink] Refused ${endpointOverrides.length} endpoint override(s) from ${envPath} because ${verdict.reason}: ${names}. Export them in your shell to use them.`);
166
+ return result;
167
+ }
168
+ notify(`[neurolink] .env at ${envPath} set endpoint override(s): ${names}${verdict.outsideHome
169
+ ? " (the file is outside your home directory; make sure you trust this project)"
170
+ : ""}`);
171
+ return result;
38
172
  }
39
173
  catch {
40
- // dotenv ships as a normal runtime dependency (see package.json), so this
41
- // branch should not be reachable in a standard install. It stays as a
42
- // defensive fallback for an install that has pruned or bundled
43
- // dependencies unusually, so a missing module degrades to "no .env
44
- // loaded" instead of crashing the SDK/CLI entry point.
174
+ // dotenv ships as a runtime dependency, so this is only reachable in an
175
+ // install that pruned or bundled dependencies unusually. A missing module
176
+ // degrades to "no .env loaded" instead of crashing the entry point.
177
+ return empty("dotenv is not available");
45
178
  }
46
179
  }
180
+ /**
181
+ * Explicit opt-in for SDK hosts that want `.env` loaded the way the CLI does,
182
+ * with the same endpoint-override checks. Call it before constructing
183
+ * `NeuroLink`.
184
+ */
185
+ export async function loadDotenv(options = {}) {
186
+ return loadProcessDotenv({ caller: "sdk", ...options });
187
+ }