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