@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,89 @@
1
+ /**
2
+ * Inbound trust for the proxy listeners and the SDK-mounted proxy routes.
3
+ *
4
+ * Both pool engines spend the operator's subscriptions on behalf of whoever
5
+ * reaches the port. On loopback that is the operator; anywhere else it is
6
+ * anyone. This module holds the three decisions that keep that honest:
7
+ *
8
+ * - **Bind policy.** A listener may bind off-loopback only when something
9
+ * authenticates callers: the share gate, an inbound token, or the operator's
10
+ * explicit `--allow-unauthenticated`.
11
+ * - **Inbound token.** `NEUROLINK_PROXY_INBOUND_TOKEN` (or `--inbound-token`)
12
+ * is a bearer every caller must present, compared in constant time. A share
13
+ * token is an alternative credential, judged by the share gate.
14
+ * - **Operator-only surfaces.** `/accounts`, `/limits` and `/status` name the
15
+ * pool's accounts; they are refused to borrowers and, when a token is
16
+ * configured, to anyone without it.
17
+ *
18
+ * @module proxy/inboundAuth
19
+ */
20
+ import type { ProxyBindPolicyDecision, ProxyBindPolicyInput, ProxyInboundAuthOptions, ProxyShareRefusalResponse } from "../types/index.js";
21
+ /** Dedicated header, so the inbound token never collides with a client credential. */
22
+ export declare const INBOUND_TOKEN_HEADER = "x-neurolink-inbound-token";
23
+ /** Set on every refusal so a client can tell "token required" from "bad request". */
24
+ export declare const INBOUND_AUTH_HEADER = "x-neurolink-inbound-auth";
25
+ export declare const INBOUND_TOKEN_ENV = "NEUROLINK_PROXY_INBOUND_TOKEN";
26
+ export declare const ALLOW_UNAUTHENTICATED_ENV = "NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED";
27
+ /**
28
+ * Compare two secrets without leaking where they diverge. Hand-rolled rather
29
+ * than `crypto.timingSafeEqual` so the proxy routes stay loadable from a build
30
+ * whose `node:crypto` stub does not carry it.
31
+ */
32
+ export declare function secretsMatch(expected: string, presented: string): boolean;
33
+ /** The inbound token in force: an explicit value wins over the environment. */
34
+ export declare function resolveInboundToken(explicit?: string): string | undefined;
35
+ /** `NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED` as a boolean. */
36
+ export declare function isUnauthenticatedBindAllowedByEnv(): boolean;
37
+ /** Loopback addresses in every spelling a bind flag accepts. */
38
+ export declare function isLoopbackHost(host: string): boolean;
39
+ /**
40
+ * May the proxy bind to `host`?
41
+ *
42
+ * Loopback always may. Anything else needs the share gate, an inbound token,
43
+ * or the operator's explicit acknowledgement — and the acknowledgement still
44
+ * earns a warning, because the subscriptions are then one port scan away.
45
+ */
46
+ export declare function evaluateProxyBindPolicy(input: ProxyBindPolicyInput): ProxyBindPolicyDecision;
47
+ /** True when the request presents the configured inbound token. */
48
+ export declare function hasValidInboundToken(headers: Record<string, string | undefined>, token: string | undefined): boolean;
49
+ /** The 401 an unauthenticated caller receives, in the Anthropic error shape. */
50
+ export declare function buildInboundAuthRefusal(): ProxyShareRefusalResponse;
51
+ /**
52
+ * Decide an inbound request against the configured token.
53
+ *
54
+ * A request carrying a share token is handed to the share gate instead: a
55
+ * borrower holds a grant, not the operator's token, and the gate is what
56
+ * judges a grant. Everything else must present the token.
57
+ */
58
+ export declare function authorizeInboundRequest(headers: Record<string, string | undefined>, token: string | undefined): {
59
+ ok: true;
60
+ } | {
61
+ ok: false;
62
+ response: ProxyShareRefusalResponse;
63
+ };
64
+ /** `Response` form of the refusal, for route factories whose adapters pass one through. */
65
+ export declare function inboundAuthRefusalResponse(): Response;
66
+ /**
67
+ * Say, loudly and once per engine, that a proxy route group was mounted with
68
+ * nothing authenticating its callers. `logger.always` on purpose: the ordinary
69
+ * warn level is invisible outside debug runs, and an open proxy is exactly the
70
+ * thing an operator must not be able to miss. Suppressed when the caller
71
+ * recorded the decision (`allowUnauthenticated`), which the CLI does after its
72
+ * own bind check.
73
+ */
74
+ export declare function warnIfProxyRoutesUnauthenticated(engine: string, options: ProxyInboundAuthOptions | undefined, token: string | undefined): void;
75
+ /** Test isolation only. */
76
+ export declare function resetInboundAuthWarningsForTests(): void;
77
+ /** Paths that stay open on an authenticated listener: liveness and self-authenticating peers. */
78
+ export declare function isInboundAuthExemptPath(path: string): boolean;
79
+ /**
80
+ * How many pooled accounts one inbound request may be tried against.
81
+ *
82
+ * Without a cap a single request fans out to every account in the pool — N
83
+ * upstream posts, N× quota — on any rotation trigger. Default 3;
84
+ * `NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST=0` restores the unbounded
85
+ * behaviour.
86
+ */
87
+ export declare const MAX_ACCOUNTS_PER_REQUEST_ENV = "NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST";
88
+ export declare const DEFAULT_MAX_ACCOUNTS_PER_REQUEST = 3;
89
+ export declare function resolveMaxAccountsPerRequest(): number;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Inbound trust for the proxy listeners and the SDK-mounted proxy routes.
3
+ *
4
+ * Both pool engines spend the operator's subscriptions on behalf of whoever
5
+ * reaches the port. On loopback that is the operator; anywhere else it is
6
+ * anyone. This module holds the three decisions that keep that honest:
7
+ *
8
+ * - **Bind policy.** A listener may bind off-loopback only when something
9
+ * authenticates callers: the share gate, an inbound token, or the operator's
10
+ * explicit `--allow-unauthenticated`.
11
+ * - **Inbound token.** `NEUROLINK_PROXY_INBOUND_TOKEN` (or `--inbound-token`)
12
+ * is a bearer every caller must present, compared in constant time. A share
13
+ * token is an alternative credential, judged by the share gate.
14
+ * - **Operator-only surfaces.** `/accounts`, `/limits` and `/status` name the
15
+ * pool's accounts; they are refused to borrowers and, when a token is
16
+ * configured, to anyone without it.
17
+ *
18
+ * @module proxy/inboundAuth
19
+ */
20
+ import { logger } from "../utils/logger.js";
21
+ import { extractShareToken } from "./shareGate.js";
22
+ /** Dedicated header, so the inbound token never collides with a client credential. */
23
+ export const INBOUND_TOKEN_HEADER = "x-neurolink-inbound-token";
24
+ /** Set on every refusal so a client can tell "token required" from "bad request". */
25
+ export const INBOUND_AUTH_HEADER = "x-neurolink-inbound-auth";
26
+ export const INBOUND_TOKEN_ENV = "NEUROLINK_PROXY_INBOUND_TOKEN";
27
+ export const ALLOW_UNAUTHENTICATED_ENV = "NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED";
28
+ /**
29
+ * Compare two secrets without leaking where they diverge. Hand-rolled rather
30
+ * than `crypto.timingSafeEqual` so the proxy routes stay loadable from a build
31
+ * whose `node:crypto` stub does not carry it.
32
+ */
33
+ export function secretsMatch(expected, presented) {
34
+ if (expected.length !== presented.length || expected.length === 0) {
35
+ return false;
36
+ }
37
+ let difference = 0;
38
+ for (let index = 0; index < expected.length; index += 1) {
39
+ difference |= expected.charCodeAt(index) ^ presented.charCodeAt(index);
40
+ }
41
+ return difference === 0;
42
+ }
43
+ function envFlagIsOn(name) {
44
+ const raw = (process.env[name] ?? "").trim().toLowerCase();
45
+ return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
46
+ }
47
+ /** The inbound token in force: an explicit value wins over the environment. */
48
+ export function resolveInboundToken(explicit) {
49
+ const candidate = explicit?.trim() || process.env[INBOUND_TOKEN_ENV]?.trim();
50
+ return candidate ? candidate : undefined;
51
+ }
52
+ /** `NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED` as a boolean. */
53
+ export function isUnauthenticatedBindAllowedByEnv() {
54
+ return envFlagIsOn(ALLOW_UNAUTHENTICATED_ENV);
55
+ }
56
+ /** Loopback addresses in every spelling a bind flag accepts. */
57
+ export function isLoopbackHost(host) {
58
+ const trimmed = host
59
+ .trim()
60
+ .toLowerCase()
61
+ .replace(/^\[|\]$/g, "");
62
+ if (trimmed === "localhost" || trimmed === "::1") {
63
+ return true;
64
+ }
65
+ if (/^0{1,4}(:0{1,4}){6}:0{0,3}1$/.test(trimmed)) {
66
+ return true;
67
+ }
68
+ if (trimmed.startsWith("::ffff:")) {
69
+ return isLoopbackHost(trimmed.slice("::ffff:".length));
70
+ }
71
+ const octets = trimmed.split(".");
72
+ return (octets.length === 4 &&
73
+ octets[0] === "127" &&
74
+ octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255));
75
+ }
76
+ /**
77
+ * May the proxy bind to `host`?
78
+ *
79
+ * Loopback always may. Anything else needs the share gate, an inbound token,
80
+ * or the operator's explicit acknowledgement — and the acknowledgement still
81
+ * earns a warning, because the subscriptions are then one port scan away.
82
+ */
83
+ export function evaluateProxyBindPolicy(input) {
84
+ if (isLoopbackHost(input.host)) {
85
+ return { allowed: true };
86
+ }
87
+ if (input.inboundToken || input.requireGrant) {
88
+ return { allowed: true };
89
+ }
90
+ if (input.allowUnauthenticated) {
91
+ return {
92
+ allowed: true,
93
+ warning: `Binding ${input.host} with no inbound authentication: anyone who reaches ` +
94
+ `this port can spend every pooled subscription. --allow-unauthenticated ` +
95
+ `acknowledges that; put an authenticating reverse proxy in front.`,
96
+ };
97
+ }
98
+ return {
99
+ allowed: false,
100
+ reason: `Refusing to bind ${input.host}: it is not a loopback address and nothing ` +
101
+ `would authenticate inbound callers. Set ${INBOUND_TOKEN_ENV} (or pass ` +
102
+ `--inbound-token <secret>), gate the port with NEUROLINK_PROXY_REQUIRE_GRANT=1, ` +
103
+ `or pass --allow-unauthenticated to accept an open proxy.`,
104
+ };
105
+ }
106
+ function presentedInboundToken(headers) {
107
+ const dedicated = headers[INBOUND_TOKEN_HEADER];
108
+ if (dedicated?.trim()) {
109
+ return dedicated.trim();
110
+ }
111
+ const authorization = headers.authorization ?? headers.Authorization;
112
+ if (!authorization) {
113
+ return undefined;
114
+ }
115
+ const trimmed = authorization.trim();
116
+ return trimmed.toLowerCase().startsWith("bearer ")
117
+ ? trimmed.slice("bearer ".length).trim()
118
+ : undefined;
119
+ }
120
+ /** True when the request presents the configured inbound token. */
121
+ export function hasValidInboundToken(headers, token) {
122
+ if (!token) {
123
+ return false;
124
+ }
125
+ const presented = presentedInboundToken(headers);
126
+ return presented !== undefined && secretsMatch(token, presented);
127
+ }
128
+ /** The 401 an unauthenticated caller receives, in the Anthropic error shape. */
129
+ export function buildInboundAuthRefusal() {
130
+ return {
131
+ status: 401,
132
+ headers: { [INBOUND_AUTH_HEADER]: "required" },
133
+ body: {
134
+ type: "error",
135
+ error: {
136
+ type: "authentication_error",
137
+ message: "This proxy requires an inbound token. Send it as " +
138
+ `${INBOUND_TOKEN_HEADER} or as Authorization: Bearer <token>.`,
139
+ },
140
+ },
141
+ };
142
+ }
143
+ /**
144
+ * Decide an inbound request against the configured token.
145
+ *
146
+ * A request carrying a share token is handed to the share gate instead: a
147
+ * borrower holds a grant, not the operator's token, and the gate is what
148
+ * judges a grant. Everything else must present the token.
149
+ */
150
+ export function authorizeInboundRequest(headers, token) {
151
+ if (!token) {
152
+ return { ok: true };
153
+ }
154
+ if (hasValidInboundToken(headers, token)) {
155
+ return { ok: true };
156
+ }
157
+ if (extractShareToken(headers) !== undefined) {
158
+ return { ok: true };
159
+ }
160
+ return { ok: false, response: buildInboundAuthRefusal() };
161
+ }
162
+ /** `Response` form of the refusal, for route factories whose adapters pass one through. */
163
+ export function inboundAuthRefusalResponse() {
164
+ const refusal = buildInboundAuthRefusal();
165
+ return new Response(JSON.stringify(refusal.body), {
166
+ status: refusal.status,
167
+ headers: { "content-type": "application/json", ...refusal.headers },
168
+ });
169
+ }
170
+ const warnedEngines = new Set();
171
+ /**
172
+ * Say, loudly and once per engine, that a proxy route group was mounted with
173
+ * nothing authenticating its callers. `logger.always` on purpose: the ordinary
174
+ * warn level is invisible outside debug runs, and an open proxy is exactly the
175
+ * thing an operator must not be able to miss. Suppressed when the caller
176
+ * recorded the decision (`allowUnauthenticated`), which the CLI does after its
177
+ * own bind check.
178
+ */
179
+ export function warnIfProxyRoutesUnauthenticated(engine, options, token) {
180
+ if (token || options?.allowUnauthenticated || warnedEngines.has(engine)) {
181
+ return;
182
+ }
183
+ warnedEngines.add(engine);
184
+ logger.always(`[proxy] SECURITY: the ${engine} proxy routes were mounted with no inbound ` +
185
+ `authentication. Anyone who can reach them spends the pooled subscriptions. ` +
186
+ `Set ${INBOUND_TOKEN_ENV}, pass inboundAuth.token, or bind loopback only ` +
187
+ `and set inboundAuth.allowUnauthenticated to acknowledge this.`);
188
+ }
189
+ /** Test isolation only. */
190
+ export function resetInboundAuthWarningsForTests() {
191
+ warnedEngines.clear();
192
+ }
193
+ /** Paths that stay open on an authenticated listener: liveness and self-authenticating peers. */
194
+ export function isInboundAuthExemptPath(path) {
195
+ return (path === "/health" ||
196
+ path === "/internal/update-control" ||
197
+ path.startsWith("/peer/"));
198
+ }
199
+ /**
200
+ * How many pooled accounts one inbound request may be tried against.
201
+ *
202
+ * Without a cap a single request fans out to every account in the pool — N
203
+ * upstream posts, N× quota — on any rotation trigger. Default 3;
204
+ * `NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST=0` restores the unbounded
205
+ * behaviour.
206
+ */
207
+ export const MAX_ACCOUNTS_PER_REQUEST_ENV = "NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST";
208
+ export const DEFAULT_MAX_ACCOUNTS_PER_REQUEST = 3;
209
+ export function resolveMaxAccountsPerRequest() {
210
+ const raw = process.env[MAX_ACCOUNTS_PER_REQUEST_ENV]?.trim();
211
+ if (raw === undefined || raw === "") {
212
+ return DEFAULT_MAX_ACCOUNTS_PER_REQUEST;
213
+ }
214
+ const parsed = Number(raw);
215
+ if (!Number.isFinite(parsed) || parsed < 0) {
216
+ return DEFAULT_MAX_ACCOUNTS_PER_REQUEST;
217
+ }
218
+ return parsed === 0 ? Number.POSITIVE_INFINITY : Math.floor(parsed);
219
+ }
@@ -1,4 +1,10 @@
1
1
  import type { RefreshableAccount, RefreshResult, TokenPersistTarget } from "../types/index.js";
2
+ /**
3
+ * An upstream refresh error body is logged for diagnosis, never verbatim: a
4
+ * token endpoint can echo the grant it rejected, and anything that looks like
5
+ * a credential is masked before the line is written.
6
+ */
7
+ export declare function redactRefreshErrorBody(body: string): string;
2
8
  export declare function needsRefresh(account: RefreshableAccount): boolean;
3
9
  export declare function needsBackgroundRefresh(account: RefreshableAccount): boolean;
4
10
  export declare function needsInlineRefresh(account: RefreshableAccount): boolean;
@@ -17,6 +17,18 @@ const SUCCESS_CACHE_MS = 60_000;
17
17
  // slow token-store decrypt can never stall (or abort) a live token refresh.
18
18
  const PEEK_TOKENS_TIMEOUT_MS = 2_000;
19
19
  const refreshesInFlight = new Map();
20
+ /**
21
+ * An upstream refresh error body is logged for diagnosis, never verbatim: a
22
+ * token endpoint can echo the grant it rejected, and anything that looks like
23
+ * a credential is masked before the line is written.
24
+ */
25
+ export function redactRefreshErrorBody(body) {
26
+ return body
27
+ .replace(/("?(?:access_token|refresh_token|id_token|client_secret|code)"?\s*[:=]\s*"?)[^",\s]+/gi, "$1[REDACTED]")
28
+ .replace(/\bsk-ant-[A-Za-z0-9_-]+/g, "sk-ant-[REDACTED]")
29
+ .replace(/\b[A-Za-z0-9_-]{64,}\b/g, "[REDACTED]")
30
+ .slice(0, 300);
31
+ }
20
32
  function expiresWithin(account, bufferMs) {
21
33
  return !!(account.expiresAt &&
22
34
  account.expiresAt <= Date.now() + bufferMs &&
@@ -69,7 +81,7 @@ async function performTokenRefresh(account) {
69
81
  const errorBody = await resp.text();
70
82
  logger.warn(`[token-refresh] failed for ${account.label} at ${redactUrlForError(url)}`, {
71
83
  status: resp.status,
72
- error: errorBody.slice(0, 500),
84
+ error: redactRefreshErrorBody(errorBody),
73
85
  });
74
86
  const failure = {
75
87
  success: false,
@@ -7,6 +7,7 @@
7
7
  * Suppressed versions expire after 24 hours.
8
8
  */
9
9
  import fs from "fs";
10
+ import { ensurePrivateDirectorySync } from "../auth/secureFiles.js";
10
11
  import os from "os";
11
12
  import path from "path";
12
13
  import { randomUUID } from "node:crypto";
@@ -39,10 +40,9 @@ function resolveStatePath(overridePath) {
39
40
  * Ensure the parent directory of the given file path exists.
40
41
  */
41
42
  function ensureParentDir(filePath) {
42
- const dir = path.dirname(filePath);
43
- if (!fs.existsSync(dir)) {
44
- fs.mkdirSync(dir, { recursive: true });
45
- }
43
+ // `~/.neurolink` holds credential material; whoever creates it first must
44
+ // create it owner-only, and re-assert the mode when it already exists.
45
+ ensurePrivateDirectorySync(path.dirname(filePath));
46
46
  }
47
47
  /** Reject malformed persisted failure metadata before it reaches status output. */
48
48
  function isValidLastFailure(value) {
@@ -88,12 +88,6 @@ export declare class PDFLoader implements DocumentLoader {
88
88
  private loadPdfParser;
89
89
  private parsePageRange;
90
90
  }
91
- /**
92
- * Web page loader
93
- *
94
- * Fetches and extracts content from web pages.
95
- * Supports basic HTML parsing without external dependencies.
96
- */
97
91
  export declare class WebLoader implements DocumentLoader {
98
92
  private defaultUserAgent;
99
93
  load(source: string, options?: WebLoaderOptions): Promise<MDocument>;
@@ -27,6 +27,8 @@ import { existsSync } from "fs";
27
27
  import { readFile } from "fs/promises";
28
28
  import { basename, extname } from "path";
29
29
  import { logger } from "../../utils/logger.js";
30
+ import { downloadAttachment } from "../../utils/attachmentPolicy.js";
31
+ import { redactUrlForError } from "../../utils/logSanitize.js";
30
32
  import { decodeHtmlTextEntitiesOnce } from "../../utils/htmlEntities.js";
31
33
  import { removeHtmlMarkup } from "../../utils/htmlText.js";
32
34
  import { CSVProcessor, stripBom, stripMetadataLine, splitCsvLines, dedupeColumnNames, sanitizeColumnName, } from "../../utils/csvProcessor.js";
@@ -351,6 +353,8 @@ export class PDFLoader {
351
353
  * Fetches and extracts content from web pages.
352
354
  * Supports basic HTML parsing without external dependencies.
353
355
  */
356
+ /** 25 MiB — the default cap on a fetched page. */
357
+ const WEB_LOADER_MAX_BYTES = 25 * 1024 * 1024;
354
358
  export class WebLoader {
355
359
  defaultUserAgent = "Mozilla/5.0 (compatible; NeuroLink/1.0; +https://github.com/juspay/neurolink)";
356
360
  async load(source, options) {
@@ -358,23 +362,25 @@ export class WebLoader {
358
362
  throw new Error(`Invalid URL: ${source}`);
359
363
  }
360
364
  logger.debug("[WebLoader] Fetching URL", {
361
- url: source,
365
+ url: redactUrlForError(source),
362
366
  timeout: options?.timeout,
363
367
  });
364
- const response = await fetch(source, {
365
- signal: options?.timeout
366
- ? AbortSignal.timeout(options.timeout)
367
- : undefined,
368
+ // Through the attachment policy: HTTPS to a public host unless the policy
369
+ // says otherwise, DNS-pinned, redirects re-validated per hop with the
370
+ // caller's headers dropped, and the body bounded while it streams rather
371
+ // than read whole with `text()`.
372
+ const downloaded = await downloadAttachment(source, {
373
+ policy: options?.attachmentPolicy,
374
+ maxBytes: options?.maxBytes ?? WEB_LOADER_MAX_BYTES,
375
+ label: "web page",
376
+ timeoutMs: options?.timeout,
368
377
  headers: {
369
378
  "User-Agent": options?.userAgent || this.defaultUserAgent,
370
379
  Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
371
380
  ...options?.headers,
372
381
  },
373
382
  });
374
- if (!response.ok) {
375
- throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
376
- }
377
- const html = await response.text();
383
+ const html = downloaded.buffer.toString("utf-8");
378
384
  let content = html;
379
385
  // Extract main content if requested
380
386
  if (options?.extractMainContent) {
@@ -388,7 +394,7 @@ export class WebLoader {
388
394
  source,
389
395
  url: source,
390
396
  fetchedAt: new Date().toISOString(),
391
- contentType: response.headers.get("content-type") || "text/html",
397
+ contentType: downloaded.contentType || "text/html",
392
398
  ...options?.metadata,
393
399
  },
394
400
  });
@@ -1,4 +1,4 @@
1
- import type { RAGConfig, VectorQueryResult, RAGPreparedTool } from "../types/index.js";
1
+ import type { AttachmentPolicy, RAGConfig, VectorQueryResult, RAGPreparedTool } from "../types/index.js";
2
2
  /**
3
3
  * Generate deterministic embeddings for chunks.
4
4
  * Combines character-frequency (40%) with word-level hash features (60%)
@@ -30,5 +30,5 @@ declare function diversifyResults(results: VectorQueryResult[], topK: number): V
30
30
  * is the default, and the fallback when a configured embedding provider fails).
31
31
  * The returned tool rejects a query over the same length the same way.
32
32
  */
33
- export declare function prepareRAGTool(ragConfig: RAGConfig, fallbackProvider?: string): Promise<RAGPreparedTool>;
33
+ export declare function prepareRAGTool(ragConfig: RAGConfig, fallbackProvider?: string, attachmentPolicy?: AttachmentPolicy): Promise<RAGPreparedTool>;
34
34
  export {};
@@ -1,4 +1,3 @@
1
- import { existsSync, readFileSync } from "fs";
2
1
  import { extname, resolve } from "path";
3
2
  import { z } from "zod";
4
3
  import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../observability/index.js";
@@ -6,12 +5,19 @@ import { logger } from "../utils/logger.js";
6
5
  import { redactUrlForError } from "../utils/logSanitize.js";
7
6
  import { ErrorFactory } from "../utils/errorHandling.js";
8
7
  import { ImageProcessor } from "../utils/imageProcessor.js";
8
+ import { readAttachmentFile, resolveAttachmentPath, resolveAttachmentPolicy, } from "../utils/attachmentPolicy.js";
9
9
  import { createChunker } from "./ChunkerFactory.js";
10
10
  import { createVectorQueryTool, InMemoryVectorStore, } from "./retrieval/vectorQueryTool.js";
11
11
  import { ImageLoader } from "./document/imageLoader.js";
12
12
  import { IMAGE_EXTENSIONS } from "../processors/config/index.js";
13
13
  import { withSpan } from "../telemetry/withSpan.js";
14
14
  import { tracers } from "../telemetry/tracers.js";
15
+ /**
16
+ * Ceiling for one `rag.files` text source: 50 MiB. A file is chunked and
17
+ * embedded in full, so a larger one is a mistake rather than a document;
18
+ * `attachments.maxBytes` lowers it.
19
+ */
20
+ const RAG_FILE_MAX_BYTES = 50 * 1024 * 1024;
15
21
  /**
16
22
  * Maps file extensions to recommended chunking strategies
17
23
  */
@@ -173,7 +179,7 @@ function diversifyResults(results, topK) {
173
179
  * is the default, and the fallback when a configured embedding provider fails).
174
180
  * The returned tool rejects a query over the same length the same way.
175
181
  */
176
- export async function prepareRAGTool(ragConfig, fallbackProvider) {
182
+ export async function prepareRAGTool(ragConfig, fallbackProvider, attachmentPolicy = resolveAttachmentPolicy()) {
177
183
  const span = SpanSerializer.createSpan(SpanType.RAG, "rag.prepare", {
178
184
  "rag.operation": "prepare",
179
185
  "rag.files_count": ragConfig.files?.length ?? 0,
@@ -182,7 +188,7 @@ export async function prepareRAGTool(ragConfig, fallbackProvider) {
182
188
  });
183
189
  const startTime = Date.now();
184
190
  try {
185
- const result = await _prepareRAGToolInner(ragConfig, fallbackProvider);
191
+ const result = await _prepareRAGToolInner(ragConfig, fallbackProvider, attachmentPolicy);
186
192
  span.durationMs = Date.now() - startTime;
187
193
  const endedSpan = SpanSerializer.endSpan(span, SpanStatus.OK);
188
194
  endedSpan.attributes = {
@@ -202,7 +208,7 @@ export async function prepareRAGTool(ragConfig, fallbackProvider) {
202
208
  throw error;
203
209
  }
204
210
  }
205
- async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
211
+ async function _prepareRAGToolInner(ragConfig, fallbackProvider, attachmentPolicy) {
206
212
  const { files, strategy: userStrategy, chunkSize = 1000, chunkOverlap = 200, topK: userTopK = 5, toolName = "search_knowledge_base", toolDescription = "REQUIRED: Search through pre-loaded local documents to find relevant information. Use this tool FIRST before any web search or other tools. This searches an indexed knowledge base of documents the user has provided.", embeddingProvider, embeddingModel, } = ragConfig;
207
213
  if (!files || files.length === 0) {
208
214
  throw new Error("RAG config requires at least one file path in 'files'");
@@ -248,10 +254,6 @@ async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
248
254
  continue;
249
255
  }
250
256
  const resolvedPath = resolve(filePath);
251
- if (!existsSync(resolvedPath)) {
252
- logger.warn(`[RAG] File not found, skipping: ${resolvedPath}`);
253
- continue;
254
- }
255
257
  // Detect image files by extension
256
258
  const ext = extname(resolvedPath).toLowerCase();
257
259
  if (ext === ".svg") {
@@ -260,15 +262,38 @@ async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
260
262
  continue;
261
263
  }
262
264
  if (IMAGE_EXTENSIONS.includes(ext)) {
263
- imageFiles.push({ path: resolvedPath, ext });
265
+ // The same containment as the text branch below; ImageLoader then reads
266
+ // the real path this returned. A path outside the roots is skipped with
267
+ // the same warning as an unreadable one, so the tool keeps working on
268
+ // the files it may read.
269
+ try {
270
+ const real = await resolveAttachmentPath(resolvedPath, attachmentPolicy, "RAG image");
271
+ imageFiles.push({ path: real, ext });
272
+ }
273
+ catch (error) {
274
+ logger.warn(`[RAG] Failed to read image: ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`);
275
+ }
264
276
  continue;
265
277
  }
266
278
  try {
267
- const content = readFileSync(resolvedPath, "utf-8");
279
+ // Roots containment when configured, O_NOFOLLOW open of the real path,
280
+ // regular files only, and a bounded read — where this was a bare
281
+ // `readFileSync` of whatever string arrived.
282
+ const read = await readAttachmentFile(resolvedPath, {
283
+ policy: attachmentPolicy,
284
+ maxBytes: RAG_FILE_MAX_BYTES,
285
+ label: "RAG file",
286
+ });
287
+ const content = read.buffer.toString("utf-8");
268
288
  const strategy = userStrategy || detectStrategy(resolvedPath);
269
289
  fileContents.push({ path: resolvedPath, content, strategy });
270
290
  }
271
291
  catch (error) {
292
+ const code = error?.code;
293
+ if (code === "ENOENT") {
294
+ logger.warn(`[RAG] File not found, skipping: ${resolvedPath}`);
295
+ continue;
296
+ }
272
297
  logger.warn(`[RAG] Failed to read file: ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`);
273
298
  }
274
299
  }
@@ -39,6 +39,8 @@ export declare function createVectorQueryTool(config: VectorQueryToolConfig, vec
39
39
  */
40
40
  export declare class InMemoryVectorStore implements VectorStore {
41
41
  private vectors;
42
+ /** Vetted `$regex` patterns: the compiled expression, or null when refused. */
43
+ private readonly regexFilterCache;
42
44
  /**
43
45
  * Add vectors to an index
44
46
  */
@@ -65,6 +67,11 @@ export declare class InMemoryVectorStore implements VectorStore {
65
67
  * Check if metadata matches filter
66
68
  */
67
69
  private matchesFilter;
70
+ /**
71
+ * Evaluate a `$regex` filter operator. Patterns are vetted once per store
72
+ * and the verdict cached, so a refused pattern costs one analysis.
73
+ */
74
+ private matchesRegexFilter;
68
75
  /**
69
76
  * Calculate cosine similarity between two vectors
70
77
  */