@juspay/neurolink 13.4.5 → 13.4.7

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 CHANGED
@@ -1,8 +1,8 @@
1
- ## [13.4.5](https://github.com/juspay/neurolink/compare/v13.4.4...v13.4.5) (2026-10-18)
1
+ ## [13.4.7](https://github.com/juspay/neurolink/compare/v13.4.6...v13.4.7) (2026-10-18)
2
2
 
3
3
  ### Bug Fixes
4
4
 
5
- - **(xor):** send decisions to /v1/decisions with the xor-latest and xor-omni models ([5ef74c4](https://github.com/juspay/neurolink/commit/5ef74c42a8e6422641ff39743b65061c5c5c1385))
5
+ - **(search-index):** read comment lines inside code fences as code, not headings ([45557b3](https://github.com/juspay/neurolink/commit/45557b3bef6fdcc05105883b6ac7fec2ef0bbb37))
6
6
 
7
7
  ## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
8
8
 
@@ -502,7 +502,7 @@ export type RealtimeVoiceAgentConfig = {
502
502
  location?: string;
503
503
  model?: string;
504
504
  voice?: string;
505
- /** xai only: WebSocket base URL of the realtime endpoint. */
505
+ /** xai only: WebSocket base URL of the realtime endpoint. A `ws://` URL to a host other than localhost, 127.0.0.0/8 or [::1] is refused unless `XAI_REALTIME_ALLOW_INSECURE=true`, because the xAI key would cross the network unencrypted. */
506
506
  xaiBaseURL?: string;
507
507
  responseModality?: string;
508
508
  systemPrompt?: string;
@@ -23,6 +23,15 @@ export declare function resolveLiveKitServerConfig(): LiveKitServerConfig;
23
23
  * `VOICE_LLM_MODEL`.
24
24
  */
25
25
  export declare function resolveBrainDefaults(): LiveKitBrainDefaults;
26
+ /**
27
+ * Refuse a `ws://` URL to a host other than loopback unless
28
+ * `XAI_REALTIME_ALLOW_INSECURE=true`: the xAI plugin sends `XAI_API_KEY` to the
29
+ * URL it is given, and `ws://` sends it unencrypted. Anything else passes,
30
+ * including a value that is not a ws or wss URL, which is for the caller's own
31
+ * check. The error names the setting and the opt-in and does not repeat the
32
+ * value.
33
+ */
34
+ export declare function requireNoCleartextCrossHost(value: string, name: string): void;
26
35
  /**
27
36
  * Resolve the realtime (speech-to-speech) voice configuration from the
28
37
  * environment.
@@ -31,7 +40,10 @@ export declare function resolveBrainDefaults(): LiveKitBrainDefaults;
31
40
  * env at all. `VOICE_REALTIME_BACKEND` picks the model family (`gemini`, the
32
41
  * default, or `xai`); `backend` here overrides it and then the environment's
33
42
  * value is not read. A non-blank `xaiBaseURL` here likewise replaces
34
- * `XAI_REALTIME_BASE_URL` after the same ws or wss check.
43
+ * `XAI_REALTIME_BASE_URL` after the same ws or wss check. Either URL is
44
+ * checked whatever the backend. A `ws://` URL to a host other than `localhost`,
45
+ * `127.0.0.0/8` or `[::1]` is refused unless `XAI_REALTIME_ALLOW_INSECURE=true`,
46
+ * because the xAI plugin sends `XAI_API_KEY` to it unencrypted.
35
47
  * `VOICE_LIVE_MODEL` overrides the backend's default model.
36
48
  * Vertex `project` is resolved from `VERTEX_PROJECT`, then the service-account
37
49
  * project (`GOOGLE_AUTH_BREEZE_PROJECT_ID`), then `GOOGLE_CLOUD_PROJECT_ID`;
@@ -77,6 +77,7 @@ const DEFAULT_SYSTEM_PROMPT = "You are a concise voice assistant for a merchant
77
77
  const DEFAULT_GREETING = "Briefly greet the merchant and ask how you can help with their store today.";
78
78
  const DEFAULT_LIGHTHOUSE_URL = "http://localhost:5173";
79
79
  const DEFAULT_MCP_PATH = "/ai/mcp/v2";
80
+ const ALLOW_INSECURE_ENV = "XAI_REALTIME_ALLOW_INSECURE";
80
81
  /**
81
82
  * The backend named by `VOICE_REALTIME_BACKEND`: `gemini` or `xai`, matched
82
83
  * case-insensitively, with blank or absent meaning `gemini`. The error names the
@@ -100,20 +101,53 @@ function requireKnownBackend(value) {
100
101
  }
101
102
  throw new Error('The realtime voice backend must be "gemini" or "xai"');
102
103
  }
103
- function isWebSocketUrl(value) {
104
+ function parseWebSocketUrl(value) {
104
105
  try {
105
- const { protocol } = new URL(value);
106
- return protocol === "ws:" || protocol === "wss:";
106
+ const url = new URL(value);
107
+ return url.protocol === "ws:" || url.protocol === "wss:" ? url : undefined;
107
108
  }
108
109
  catch {
109
- return false;
110
+ return undefined;
110
111
  }
111
112
  }
112
- /** A ws or wss URL. The error names the setting and does not repeat the value. */
113
+ /**
114
+ * Whether a WHATWG-parsed hostname is `localhost`, an address in `127.0.0.0/8`
115
+ * or `[::1]`. The parser has already lowercased the name and reduced shorthand
116
+ * IPv4 forms (`127.1`, `0x7f.1`) to dotted decimal, and a userinfo prefix such
117
+ * as `localhost@host` never reaches `hostname`, so neither can pass for a
118
+ * loopback host.
119
+ */
120
+ function isLoopbackHost(hostname) {
121
+ if (hostname === "localhost" || hostname === "[::1]") {
122
+ return true;
123
+ }
124
+ const octets = hostname.split(".");
125
+ return (octets.length === 4 &&
126
+ octets[0] === "127" &&
127
+ octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255));
128
+ }
129
+ /**
130
+ * Refuse a `ws://` URL to a host other than loopback unless
131
+ * `XAI_REALTIME_ALLOW_INSECURE=true`: the xAI plugin sends `XAI_API_KEY` to the
132
+ * URL it is given, and `ws://` sends it unencrypted. Anything else passes,
133
+ * including a value that is not a ws or wss URL, which is for the caller's own
134
+ * check. The error names the setting and the opt-in and does not repeat the
135
+ * value.
136
+ */
137
+ export function requireNoCleartextCrossHost(value, name) {
138
+ const url = parseWebSocketUrl(value);
139
+ if (url?.protocol === "ws:" &&
140
+ !isLoopbackHost(url.hostname) &&
141
+ readEnv(ALLOW_INSECURE_ENV, "").toLowerCase() !== "true") {
142
+ throw new Error(`${name} is a ws:// URL to a host other than localhost, 127.0.0.0/8 or [::1], which would send XAI_API_KEY unencrypted. Use wss://, or set ${ALLOW_INSECURE_ENV}=true to accept that`);
143
+ }
144
+ }
145
+ /** A ws or wss URL that passes `requireNoCleartextCrossHost`. The error names the setting and does not repeat the value. */
113
146
  function requireWebSocketUrl(value, name) {
114
- if (!isWebSocketUrl(value)) {
147
+ if (parseWebSocketUrl(value) === undefined) {
115
148
  throw new Error(`${name} must be a ws:// or wss:// URL`);
116
149
  }
150
+ requireNoCleartextCrossHost(value, name);
117
151
  return value;
118
152
  }
119
153
  /** `XAI_REALTIME_BASE_URL`, which must be a ws or wss URL when set. */
@@ -136,7 +170,10 @@ function resolveXaiBaseUrl(override) {
136
170
  * env at all. `VOICE_REALTIME_BACKEND` picks the model family (`gemini`, the
137
171
  * default, or `xai`); `backend` here overrides it and then the environment's
138
172
  * value is not read. A non-blank `xaiBaseURL` here likewise replaces
139
- * `XAI_REALTIME_BASE_URL` after the same ws or wss check.
173
+ * `XAI_REALTIME_BASE_URL` after the same ws or wss check. Either URL is
174
+ * checked whatever the backend. A `ws://` URL to a host other than `localhost`,
175
+ * `127.0.0.0/8` or `[::1]` is refused unless `XAI_REALTIME_ALLOW_INSECURE=true`,
176
+ * because the xAI plugin sends `XAI_API_KEY` to it unencrypted.
140
177
  * `VOICE_LIVE_MODEL` overrides the backend's default model.
141
178
  * Vertex `project` is resolved from `VERTEX_PROJECT`, then the service-account
142
179
  * project (`GOOGLE_AUTH_BREEZE_PROJECT_ID`), then `GOOGLE_CLOUD_PROJECT_ID`;
@@ -28,6 +28,8 @@ export declare function requireXaiApiKey(): void;
28
28
  * The gemini branch builds exactly what the agent has always built: Vertex AI,
29
29
  * the configured model, transcripts of both sides, and project, location,
30
30
  * voice and modality when set. The xai branch needs `XAI_API_KEY` in the
31
- * environment and throws before touching the plugin when it is missing.
31
+ * environment and throws before touching the plugin when it is missing. It also
32
+ * throws before touching the plugin for a `ws://` base URL to a host other than
33
+ * loopback, unless `XAI_REALTIME_ALLOW_INSECURE=true`, as config resolution does.
32
34
  */
33
35
  export declare function createRealtimeLlm(cfg: RealtimeVoiceConfig): Promise<RealtimeLlm>;
@@ -13,6 +13,7 @@
13
13
  *
14
14
  * See docs/features/livekit-voice-agent.md.
15
15
  */
16
+ import { requireNoCleartextCrossHost } from "./config.js";
16
17
  /**
17
18
  * Fail early, naming the variable, when `XAI_API_KEY` is blank or absent.
18
19
  *
@@ -53,6 +54,11 @@ async function createGeminiRealtimeLlm(cfg) {
53
54
  }
54
55
  async function createXaiRealtimeLlm(cfg) {
55
56
  requireXaiApiKey();
57
+ // `cfg` can be built by hand, without `resolveRealtimeVoiceConfig`, so the
58
+ // key-leak check runs here as well as there.
59
+ if (cfg.xaiBaseURL) {
60
+ requireNoCleartextCrossHost(cfg.xaiBaseURL, "xaiBaseURL");
61
+ }
56
62
  const xai = await import("@livekit/agents-plugin-xai");
57
63
  // Keys are added only when set. The plugin spreads these options over its
58
64
  // own defaults, so a key holding `undefined` would erase the default: a
@@ -75,7 +81,9 @@ async function createXaiRealtimeLlm(cfg) {
75
81
  * The gemini branch builds exactly what the agent has always built: Vertex AI,
76
82
  * the configured model, transcripts of both sides, and project, location,
77
83
  * voice and modality when set. The xai branch needs `XAI_API_KEY` in the
78
- * environment and throws before touching the plugin when it is missing.
84
+ * environment and throws before touching the plugin when it is missing. It also
85
+ * throws before touching the plugin for a `ws://` base URL to a host other than
86
+ * loopback, unless `XAI_REALTIME_ALLOW_INSECURE=true`, as config resolution does.
79
87
  */
80
88
  export async function createRealtimeLlm(cfg) {
81
89
  switch (cfg.backend) {