@juspay/neurolink 13.8.17 → 13.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +1 -1
  3. package/dist/action/actionExecutor.d.ts +33 -3
  4. package/dist/action/actionExecutor.js +71 -15
  5. package/dist/action/actionInputs.js +4 -1
  6. package/dist/action/cliVersion.d.ts +25 -0
  7. package/dist/action/cliVersion.js +26 -0
  8. package/dist/action/index.d.ts +2 -1
  9. package/dist/action/index.js +2 -1
  10. package/dist/adapters/video/directorPipeline.js +46 -36
  11. package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
  12. package/dist/adapters/video/ffmpegAdapter.js +32 -5
  13. package/dist/adapters/video/frameExtractor.js +1 -1
  14. package/dist/adapters/video/videoMerger.js +5 -2
  15. package/dist/agent/arithmeticEvaluator.d.ts +32 -0
  16. package/dist/agent/arithmeticEvaluator.js +349 -0
  17. package/dist/agent/directTools.d.ts +6 -18
  18. package/dist/agent/directTools.js +17 -56
  19. package/dist/artifacts/artifactStore.d.ts +22 -1
  20. package/dist/artifacts/artifactStore.js +66 -6
  21. package/dist/auth/anthropicOAuth.d.ts +26 -1
  22. package/dist/auth/anthropicOAuth.js +81 -5
  23. package/dist/auth/secureFiles.d.ts +37 -0
  24. package/dist/auth/secureFiles.js +95 -0
  25. package/dist/auth/tokenStore.d.ts +43 -6
  26. package/dist/auth/tokenStore.js +260 -79
  27. package/dist/avatar/providers/DIDAvatar.js +49 -45
  28. package/dist/avatar/providers/ReplicateAvatar.js +33 -40
  29. package/dist/browser/neurolink.min.js +485 -558
  30. package/dist/cli/commands/auth.js +112 -70
  31. package/dist/cli/commands/proxy.d.ts +4 -0
  32. package/dist/cli/commands/proxy.js +128 -13
  33. package/dist/cli/commands/serve.d.ts +10 -1
  34. package/dist/cli/commands/serve.js +98 -17
  35. package/dist/cli/commands/server.js +17 -5
  36. package/dist/cli/index.js +146 -5
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/utils/envManager.js +17 -3
  39. package/dist/cli/utils/serverUtils.d.ts +39 -3
  40. package/dist/cli/utils/serverUtils.js +63 -6
  41. package/dist/core/baseProvider.js +7 -1
  42. package/dist/core/modules/MessageBuilder.js +5 -0
  43. package/dist/core/modules/ToolsManager.d.ts +46 -1
  44. package/dist/core/modules/ToolsManager.js +190 -43
  45. package/dist/files/fileReferenceRegistry.d.ts +2 -23
  46. package/dist/files/fileReferenceRegistry.js +55 -5
  47. package/dist/hitl/hitlManager.js +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +6 -1
  50. package/dist/mcp/auth/tokenStorage.d.ts +7 -0
  51. package/dist/mcp/auth/tokenStorage.js +23 -4
  52. package/dist/mcp/externalServerManager.d.ts +13 -3
  53. package/dist/mcp/externalServerManager.js +154 -136
  54. package/dist/mcp/index.d.ts +2 -0
  55. package/dist/mcp/index.js +3 -0
  56. package/dist/mcp/mcpClientFactory.d.ts +14 -0
  57. package/dist/mcp/mcpClientFactory.js +70 -7
  58. package/dist/mcp/projectConfigTrust.d.ts +72 -0
  59. package/dist/mcp/projectConfigTrust.js +237 -0
  60. package/dist/mcp/transportUrlPolicy.d.ts +39 -0
  61. package/dist/mcp/transportUrlPolicy.js +172 -0
  62. package/dist/music/providers/BeatovenMusic.js +35 -46
  63. package/dist/music/providers/ReplicateMusic.js +33 -40
  64. package/dist/neurolink.d.ts +16 -1
  65. package/dist/neurolink.js +64 -11
  66. package/dist/processors/archive/ArchiveProcessor.js +15 -1
  67. package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
  68. package/dist/processors/archive/zipEntryIndex.js +33 -0
  69. package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
  70. package/dist/processors/base/BaseFileProcessor.js +40 -111
  71. package/dist/processors/data/YamlProcessor.d.ts +13 -1
  72. package/dist/processors/data/YamlProcessor.js +93 -2
  73. package/dist/processors/document/OpenDocumentProcessor.js +7 -2
  74. package/dist/processors/document/PptxProcessor.js +12 -7
  75. package/dist/processors/media/VideoProcessor.js +93 -15
  76. package/dist/proxy/inboundAuth.d.ts +89 -0
  77. package/dist/proxy/inboundAuth.js +219 -0
  78. package/dist/proxy/tokenRefresh.d.ts +6 -0
  79. package/dist/proxy/tokenRefresh.js +13 -1
  80. package/dist/proxy/updateState.js +4 -4
  81. package/dist/rag/document/loaders.d.ts +0 -6
  82. package/dist/rag/document/loaders.js +16 -10
  83. package/dist/rag/ragIntegration.d.ts +2 -2
  84. package/dist/rag/ragIntegration.js +35 -10
  85. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  86. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  87. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  88. package/dist/server/abstract/baseServerAdapter.js +119 -6
  89. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  90. package/dist/server/adapters/expressAdapter.js +78 -6
  91. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  92. package/dist/server/adapters/fastifyAdapter.js +165 -70
  93. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  94. package/dist/server/adapters/honoAdapter.js +99 -21
  95. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  96. package/dist/server/adapters/koaAdapter.js +153 -74
  97. package/dist/server/index.d.ts +2 -0
  98. package/dist/server/index.js +4 -0
  99. package/dist/server/middleware/auth.js +6 -2
  100. package/dist/server/middleware/cache.js +6 -1
  101. package/dist/server/middleware/rateLimit.js +29 -4
  102. package/dist/server/routes/agentRoutes.js +24 -11
  103. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  104. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  105. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  106. package/dist/server/routes/codexProxyRoutes.js +125 -40
  107. package/dist/server/routes/index.d.ts +4 -0
  108. package/dist/server/routes/index.js +35 -4
  109. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  110. package/dist/server/routes/mcpRoutes.js +21 -7
  111. package/dist/server/routes/memoryRoutes.js +7 -6
  112. package/dist/server/routes/toolRoutes.d.ts +12 -2
  113. package/dist/server/routes/toolRoutes.js +33 -3
  114. package/dist/server/routes/transcribeRoutes.js +60 -13
  115. package/dist/server/utils/clientAddress.d.ts +23 -0
  116. package/dist/server/utils/clientAddress.js +65 -0
  117. package/dist/server/utils/clientError.d.ts +35 -0
  118. package/dist/server/utils/clientError.js +129 -0
  119. package/dist/server/utils/pathMatch.d.ts +21 -0
  120. package/dist/server/utils/pathMatch.js +41 -0
  121. package/dist/server/utils/validation.d.ts +9 -0
  122. package/dist/server/utils/validation.js +9 -0
  123. package/dist/server/websocket/WebSocketHandler.js +22 -3
  124. package/dist/server/websocket/frameQueue.d.ts +19 -0
  125. package/dist/server/websocket/frameQueue.js +41 -3
  126. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  127. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  128. package/dist/skills/skillStores.js +5 -0
  129. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  130. package/dist/tasks/store/fileTaskStore.js +11 -4
  131. package/dist/types/attachments.d.ts +117 -0
  132. package/dist/types/attachments.js +15 -0
  133. package/dist/types/auth.d.ts +6 -0
  134. package/dist/types/cli.d.ts +4 -0
  135. package/dist/types/config.d.ts +56 -0
  136. package/dist/types/file.d.ts +8 -0
  137. package/dist/types/hitl.d.ts +8 -0
  138. package/dist/types/index.d.ts +1 -0
  139. package/dist/types/index.js +1 -0
  140. package/dist/types/mcp.d.ts +98 -0
  141. package/dist/types/multimodal.js +0 -41
  142. package/dist/types/processor.d.ts +14 -1
  143. package/dist/types/proxy.d.ts +62 -3
  144. package/dist/types/rag.d.ts +9 -0
  145. package/dist/types/safeFetch.d.ts +40 -0
  146. package/dist/types/server.d.ts +206 -10
  147. package/dist/types/tools.d.ts +57 -0
  148. package/dist/types/transcribe.d.ts +6 -0
  149. package/dist/utils/attachmentPolicy.d.ts +89 -0
  150. package/dist/utils/attachmentPolicy.js +327 -0
  151. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  152. package/dist/utils/dotenvBootstrap.js +172 -31
  153. package/dist/utils/fileDetector.js +115 -65
  154. package/dist/utils/json/coerce.js +11 -1
  155. package/dist/utils/logSanitize.d.ts +5 -0
  156. package/dist/utils/logSanitize.js +8 -0
  157. package/dist/utils/logger.js +73 -1
  158. package/dist/utils/messageBuilder.d.ts +2 -2
  159. package/dist/utils/messageBuilder.js +118 -105
  160. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  161. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  162. package/dist/utils/redis.d.ts +12 -0
  163. package/dist/utils/redis.js +72 -0
  164. package/dist/utils/safeFetch.d.ts +22 -1
  165. package/dist/utils/safeFetch.js +132 -38
  166. package/dist/utils/safeStoreId.d.ts +19 -0
  167. package/dist/utils/safeStoreId.js +29 -0
  168. package/dist/utils/schemaConversion.d.ts +30 -3
  169. package/dist/utils/schemaConversion.js +457 -84
  170. package/dist/utils/sizeGuard.js +2 -2
  171. package/dist/utils/ssrfGuard.d.ts +3 -3
  172. package/dist/utils/ssrfGuard.js +71 -39
  173. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  174. package/dist/voice/eval/sttEvalStore.js +77 -12
  175. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  176. package/dist/voice/livekit/agentDefinition.js +22 -0
  177. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  178. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  179. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  180. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  181. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  182. package/dist/voice/livekit/voiceAgent.js +2 -1
  183. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  184. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  185. package/dist/voice/providers/GeminiLive.js +21 -6
  186. package/dist/voice/providers/GoogleSTT.js +8 -5
  187. package/dist/voice/transcribe.d.ts +2 -2
  188. package/dist/voice/transcribe.js +34 -18
  189. package/dist/voice/whistle/audio.js +11 -5
  190. package/docs-site/static/search-index.json +71 -49
  191. package/package.json +171 -156
  192. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -39,6 +39,8 @@ import { logRequest, logRequestAttempt, logBodyCapture, isProxyBodyCaptureEnable
39
39
  import { createRawStreamCapture } from "../../proxy/rawStreamCapture.js";
40
40
  import { resolveProxyLogTraceContext, getProxyRequestTraceContext, } from "../../proxy/proxyTraceContext.js";
41
41
  import { isBorrowedRequest } from "../../proxy/shareContext.js";
42
+ import { authorizeInboundRequest, inboundAuthRefusalResponse, resolveInboundToken, resolveMaxAccountsPerRequest, warnIfProxyRoutesUnauthenticated, } from "../../proxy/inboundAuth.js";
43
+ import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
42
44
  import { ProxyTracer, recordFallbackAttempt } from "../../proxy/proxyTracer.js";
43
45
  import { parseRetryAfterMs } from "../../proxy/routingPolicy.js";
44
46
  import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
@@ -153,20 +155,52 @@ async function refreshCodexTokenOnceWithDependencies(key, refreshToken, store, r
153
155
  async function refreshCodexTokenOnce(key, refreshToken) {
154
156
  return refreshCodexTokenOnceWithDependencies(key, refreshToken, tokenStore, refreshCodexToken);
155
157
  }
156
- // Headers we never forward upstream (hop-by-hop, client creds, or things we
157
- // re-derive). The client's own auth is replaced with the pooled account's.
158
- const BLOCKED_UPSTREAM_HEADERS = new Set([
159
- "host",
160
- "connection",
161
- "content-length",
162
- "transfer-encoding",
163
- "cookie",
164
- "proxy-authorization",
165
- "authorization",
166
- "x-api-key",
167
- "chatgpt-account-id",
168
- "accept-encoding",
158
+ /**
159
+ * Protocol headers a client may influence upstream: the documented Codex
160
+ * client fingerprint and content negotiation. Everything else — proxy-internal
161
+ * `x-neurolink-*` (a borrower's share token among them), trace context, CDN and
162
+ * forwarding identity, cookies, hop-by-hop — is dropped. The client's own
163
+ * credentials are replaced with the pooled account's, never forwarded.
164
+ */
165
+ const FORWARDABLE_UPSTREAM_HEADERS = new Set([
166
+ "accept",
167
+ "content-type",
168
+ "user-agent",
169
+ "originator",
170
+ "session_id",
171
+ "conversation_id",
172
+ "openai-beta",
169
173
  ]);
174
+ const FORWARDABLE_UPSTREAM_HEADER_PREFIXES = ["x-codex-"];
175
+ function isForwardableCodexHeader(lowerName) {
176
+ return (FORWARDABLE_UPSTREAM_HEADERS.has(lowerName) ||
177
+ FORWARDABLE_UPSTREAM_HEADER_PREFIXES.some((prefix) => lowerName.startsWith(prefix)));
178
+ }
179
+ /**
180
+ * Body fields that reference or create account-side state at OpenAI. A
181
+ * borrower may not read another caller's stored response, pin a cache key or
182
+ * stamp `metadata`/`user` onto the lender's account; their own `instructions`
183
+ * are their prompt and stay.
184
+ */
185
+ const BORROWER_STRIPPED_BODY_FIELDS = [
186
+ "previous_response_id",
187
+ "prompt_cache_key",
188
+ "metadata",
189
+ "user",
190
+ ];
191
+ function stripBorrowerAccountState(body) {
192
+ if (!isBorrowedRequest()) {
193
+ return body;
194
+ }
195
+ const stripped = { ...body };
196
+ for (const field of BORROWER_STRIPPED_BODY_FIELDS) {
197
+ delete stripped[field];
198
+ }
199
+ if ("store" in stripped) {
200
+ stripped.store = false;
201
+ }
202
+ return stripped;
203
+ }
170
204
  /** Build a Codex error body as a Response with the intended status. */
171
205
  function buildCodexErrorResponse(status, message, code) {
172
206
  return new Response(JSON.stringify({
@@ -290,7 +324,7 @@ function buildCodexUpstreamHeaders(clientHeaders, account) {
290
324
  const headers = {};
291
325
  for (const [name, value] of Object.entries(clientHeaders)) {
292
326
  const lower = name.toLowerCase();
293
- if (BLOCKED_UPSTREAM_HEADERS.has(lower)) {
327
+ if (!isForwardableCodexHeader(lower) || typeof value !== "string") {
294
328
  continue;
295
329
  }
296
330
  headers[lower] = value;
@@ -361,16 +395,20 @@ function planCodexCooldown(quota, retryAfterMs, now, errorText = "") {
361
395
  // skew. Taken literally the account is eligible again immediately and the
362
396
  // pool re-sends to something the provider just rejected.
363
397
  const floor = now + DEFAULT_TRANSIENT_COOLDOWN_MS;
398
+ // A reset is upstream's claim about the future. Bound it by what the window
399
+ // can mean, as the Claude engine does: a bogus far-future reset must not park
400
+ // an account until someone edits the cooldown file by hand.
401
+ const clamp = (untilMs, reason) => Math.min(Math.max(untilMs, floor), now + (MAX_COOLDOWN_MS_BY_REASON[reason] ?? MAX_TRANSIENT_COOLDOWN_MS));
364
402
  if (quota) {
365
403
  if (quota.weeklyStatus === "rejected" && quota.weeklyResetAt > 0) {
366
404
  return {
367
- coolingUntil: Math.max(quota.weeklyResetAt * 1000, floor),
405
+ coolingUntil: clamp(quota.weeklyResetAt * 1000, "weekly"),
368
406
  reason: "weekly",
369
407
  };
370
408
  }
371
409
  if (quota.sessionStatus === "rejected" && quota.sessionResetAt > 0) {
372
410
  return {
373
- coolingUntil: Math.max(quota.sessionResetAt * 1000, floor),
411
+ coolingUntil: clamp(quota.sessionResetAt * 1000, "session"),
374
412
  reason: "session",
375
413
  };
376
414
  }
@@ -379,7 +417,7 @@ function planCodexCooldown(quota, retryAfterMs, now, errorText = "") {
379
417
  if (bodyQuota) {
380
418
  if (bodyQuota.scope !== "unknown" && bodyQuota.resetAt > 0) {
381
419
  return {
382
- coolingUntil: Math.max(bodyQuota.resetAt, floor),
420
+ coolingUntil: clamp(bodyQuota.resetAt, bodyQuota.scope),
383
421
  reason: bodyQuota.scope,
384
422
  };
385
423
  }
@@ -400,8 +438,13 @@ function publishCodexHeaders(ctx, account, attempt, quota) {
400
438
  if (!ctx.responseHeaders) {
401
439
  ctx.responseHeaders = {};
402
440
  }
403
- ctx.responseHeaders["x-neurolink-account"] = account.label;
404
- ctx.responseHeaders["x-neurolink-account-type"] = "codex-oauth";
441
+ // The label is the lender's identity (an email for a ChatGPT account). A
442
+ // borrower gets the quota it routes on, not whose account served it — the
443
+ // same redaction the Claude engine applies for borrowed traffic.
444
+ if (!isBorrowedRequest()) {
445
+ ctx.responseHeaders["x-neurolink-account"] = account.label;
446
+ ctx.responseHeaders["x-neurolink-account-type"] = "codex-oauth";
447
+ }
405
448
  ctx.responseHeaders["x-neurolink-served-by"] = "codex";
406
449
  ctx.responseHeaders["x-neurolink-attempt"] = String(attempt);
407
450
  ctx.responseHeaders["x-neurolink-quota-source"] = quota ? "live" : "none";
@@ -1536,6 +1579,29 @@ async function handleCodexUpstreamFailure(rt, args) {
1536
1579
  lastErrorMessage: "Codex account rate-limited",
1537
1580
  };
1538
1581
  }
1582
+ // Any other 4xx is upstream's verdict on the request itself — a malformed
1583
+ // body, an unknown model, an over-long input. Every other account would
1584
+ // answer the same way, so trying them only multiplies the cost: end the
1585
+ // request here, with the upstream status.
1586
+ if (upstream.status >= 400 &&
1587
+ upstream.status < 500 &&
1588
+ upstream.status !== 401 &&
1589
+ upstream.status !== 403) {
1590
+ const errorMessage = summarizeCodexUpstreamError(errText, "Codex error");
1591
+ recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, upstream.status);
1592
+ writeAttempt(account, attempt, attemptStartedAt, upstream.status, {
1593
+ errorType: "invalid_request_error",
1594
+ errorMessage,
1595
+ retryable: false,
1596
+ });
1597
+ return {
1598
+ lastFailure: { errorType: "invalid_request_error", retryable: false },
1599
+ lastErrorStatus: upstream.status,
1600
+ lastErrorCode: getProxyUpstreamFailure({ responseBody: errText })?.code,
1601
+ lastErrorMessage: errorMessage,
1602
+ refusedRequest: true,
1603
+ };
1604
+ }
1539
1605
  // Other non-ok → record and rotate.
1540
1606
  if (upstream.status === 401 || upstream.status === 403) {
1541
1607
  // Reached when the account has no refresh token to retry with, or when
@@ -1748,7 +1814,7 @@ loopbackPort, internalDispatch) {
1748
1814
  body: state.body,
1749
1815
  });
1750
1816
  state.contextPreflight = preparedContext.evidence;
1751
- state.body = preparedContext.body;
1817
+ state.body = stripBorrowerAccountState(preparedContext.body);
1752
1818
  state.bodyStr = JSON.stringify(state.body);
1753
1819
  }
1754
1820
  catch (error) {
@@ -1774,10 +1840,12 @@ loopbackPort, internalDispatch) {
1774
1840
  }
1775
1841
  const now = Date.now();
1776
1842
  const ordered = orderCodexAccounts(accounts, now);
1777
- const eligible = ordered.filter((a) => !(a.coolingUntil !== undefined && a.coolingUntil > now));
1778
- if (eligible.length === 0) {
1843
+ const notCooling = ordered.filter((a) => !(a.coolingUntil !== undefined && a.coolingUntil > now));
1844
+ if (notCooling.length === 0) {
1779
1845
  return respondCodexPoolCooling(rt, ordered, now);
1780
1846
  }
1847
+ // One inbound request may be tried against at most this many accounts.
1848
+ const eligible = notCooling.slice(0, resolveMaxAccountsPerRequest());
1781
1849
  let attempt = 0;
1782
1850
  let lastErrorMessage = "All Codex accounts failed";
1783
1851
  let lastErrorStatus = 502;
@@ -2022,20 +2090,26 @@ async function handleCodexModelsRequest(ctx) {
2022
2090
  }
2023
2091
  const now = Date.now();
2024
2092
  const ordered = orderCodexAccounts(accounts, now);
2025
- // A cooling account is rate-limited for completions, not barred from
2026
- // answering what models exist. Healthy accounts go first, but a cooling one
2027
- // is still a candidate rather than a reason to fail discovery outright.
2028
- const isCooling = (a) => a.coolingUntil !== undefined && a.coolingUntil > now;
2029
- const candidates = [
2030
- ...ordered.filter((a) => !isCooling(a)),
2031
- ...ordered.filter(isCooling),
2032
- ];
2033
- // Forward the CLI's own query — it sends client_version, and upstream
2034
- // *requires* it: without it ChatGPT answers 400 with a pydantic
2035
- // "Field required" on ('query','client_version'). Rebuild from ctx.query,
2036
- // not ctx.path: path carries no query string, so reading it there silently
2037
- // dropped the parameter and produced exactly that 400.
2038
- const params = new URLSearchParams(ctx.query ?? {});
2093
+ // Discovery used to reach every account, cooling ones included, and relay
2094
+ // the client's whole query. Only healthy accounts answer it now, capped by
2095
+ // the per-request account limit. Every call still reaches upstream: the
2096
+ // limits it reports (context window, output ceiling) are recorded per model
2097
+ // and must refresh whenever upstream changes them.
2098
+ const candidates = ordered
2099
+ .filter((a) => !(a.coolingUntil !== undefined && a.coolingUntil > now))
2100
+ .slice(0, resolveMaxAccountsPerRequest());
2101
+ if (candidates.length === 0) {
2102
+ return buildCodexErrorResponse(503, "Every Codex account is cooling; model discovery is unavailable until one recovers.");
2103
+ }
2104
+ // Forward only the CLI's `client_version` — upstream *requires* it: without
2105
+ // it ChatGPT answers 400 with a pydantic "Field required" on
2106
+ // ('query','client_version'). Rebuild from ctx.query, not ctx.path: path
2107
+ // carries no query string. Nothing else a client puts in the query travels.
2108
+ const params = new URLSearchParams();
2109
+ const clientVersion = ctx.query?.client_version;
2110
+ if (typeof clientVersion === "string" && clientVersion !== "") {
2111
+ params.set("client_version", clientVersion);
2112
+ }
2039
2113
  const query = params.toString();
2040
2114
  const url = query ? `${CODEX_MODELS_URL}?${query}` : CODEX_MODELS_URL;
2041
2115
  let lastErrorStatus = 502;
@@ -2099,10 +2173,10 @@ async function handleCodexModelsRequest(ctx) {
2099
2173
  // query was not forwarded correctly, and hiding it behind a retry would
2100
2174
  // bury the exact regression this route was added to fix.
2101
2175
  const body = await upstream.text();
2176
+ const contentType = upstream.headers.get("content-type") ?? "application/json";
2102
2177
  if (upstream.ok) {
2103
2178
  registerCodexDiscoveredLimits(body);
2104
2179
  }
2105
- const contentType = upstream.headers.get("content-type") ?? "application/json";
2106
2180
  return new Response(body, {
2107
2181
  status: upstream.status,
2108
2182
  headers: { "content-type": contentType },
@@ -2175,8 +2249,19 @@ function warnIfAnthropicOutboundFallbackUnreachable(runtimeConfigProvider, loopb
2175
2249
  * `loopbackPort` when omitted.
2176
2250
  * @returns RouteGroup with the Codex backend Responses endpoint.
2177
2251
  */
2178
- export function createCodexProxyRoutes(basePath = "", runtimeConfigProvider, loopbackPort, internalDispatch) {
2252
+ export function createCodexProxyRoutes(basePath = "", runtimeConfigProvider, loopbackPort, internalDispatch, inboundAuth) {
2179
2253
  warnIfAnthropicOutboundFallbackUnreachable(runtimeConfigProvider, loopbackPort, internalDispatch);
2254
+ const inboundToken = resolveInboundToken(inboundAuth?.token);
2255
+ warnIfProxyRoutesUnauthenticated("Codex", inboundAuth, inboundToken);
2256
+ const guard = (handler) => (ctx) => {
2257
+ if (inboundToken) {
2258
+ const verdict = authorizeInboundRequest(ctx.headers, inboundToken);
2259
+ if (!verdict.ok) {
2260
+ return Promise.resolve(inboundAuthRefusalResponse());
2261
+ }
2262
+ }
2263
+ return handler(ctx);
2264
+ };
2180
2265
  return {
2181
2266
  prefix: `${basePath}/backend-api/codex`,
2182
2267
  routes: [
@@ -2184,13 +2269,13 @@ export function createCodexProxyRoutes(basePath = "", runtimeConfigProvider, loo
2184
2269
  method: "POST",
2185
2270
  path: `${basePath}/backend-api/codex/responses`,
2186
2271
  description: "Codex ChatGPT-backend Responses API (account pool)",
2187
- handler: (ctx) => handleCodexResponsesRequest(ctx, runtimeConfigProvider, loopbackPort, internalDispatch),
2272
+ handler: guard((ctx) => handleCodexResponsesRequest(ctx, runtimeConfigProvider, loopbackPort, internalDispatch)),
2188
2273
  },
2189
2274
  {
2190
2275
  method: "GET",
2191
2276
  path: `${basePath}/backend-api/codex/models`,
2192
2277
  description: "Codex model discovery, relayed upstream (account pool)",
2193
- handler: (ctx) => handleCodexModelsRequest(ctx),
2278
+ handler: guard((ctx) => handleCodexModelsRequest(ctx)),
2194
2279
  },
2195
2280
  ],
2196
2281
  };
@@ -42,4 +42,8 @@ export declare function registerAllRoutes(adapter: {
42
42
  registerRouteGroup: (group: RouteGroup) => void;
43
43
  listRoutes?: () => RouteDefinition[];
44
44
  enableMiddlewareStateCarry?: () => void;
45
+ hasAuthenticationMiddleware?: () => boolean;
46
+ noteSensitiveRoutes?: (options?: {
47
+ allowUnauthenticated?: boolean;
48
+ }) => void;
45
49
  }, basePath?: string, options?: CreateRoutesOptions): void;
@@ -40,13 +40,30 @@ export { createTranscribeRoutes } from "./transcribeRoutes.js";
40
40
  * and docs/features/codex-proxy-support.md.
41
41
  */
42
42
  export function createAllRoutes(basePath = "/api", options) {
43
+ // The tool and MCP execution routes act on the host. They refuse callers
44
+ // the server has not authenticated, when they can tell: registerAllRoutes()
45
+ // tells them through `hasAuthentication`; an explicit opt-out, by option or
46
+ // by NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1, opens them regardless.
47
+ const sensitive = {
48
+ allowUnauthenticated: options?.allowUnauthenticated === true || allowUnauthenticatedFromEnv(),
49
+ hasAuthentication: options?.hasAuthentication,
50
+ };
51
+ // The proxy doors spend pooled subscriptions; their inbound token defaults to
52
+ // NEUROLINK_PROXY_INBOUND_TOKEN and the factories warn loudly without one.
53
+ const proxyAuth = {
54
+ ...options?.proxyAuth,
55
+ allowUnauthenticated: options?.proxyAuth?.allowUnauthenticated ??
56
+ options?.allowUnauthenticated === true,
57
+ };
43
58
  const routes = [
44
59
  createAgentRoutes(basePath, {
45
60
  ...options?.agent,
46
61
  perUserAuth: options?.perUserAuth,
47
62
  }),
48
- createToolRoutes(basePath),
49
- createMCPRoutes(basePath),
63
+ ...(options?.tools === false
64
+ ? []
65
+ : [createToolRoutes(basePath, sensitive)]),
66
+ createMCPRoutes(basePath, sensitive),
50
67
  createMemoryRoutes(basePath),
51
68
  createHealthRoutes(basePath),
52
69
  createTranscribeRoutes(basePath, options?.transcribe),
@@ -64,13 +81,13 @@ export function createAllRoutes(basePath = "/api", options) {
64
81
  const enableCodexProxy = options?.proxy || options?.codexProxy;
65
82
  const enableGeminiProxy = options?.proxy || options?.geminiProxy;
66
83
  if (enableClaudeProxy) {
67
- routes.push(createClaudeProxyRoutes(undefined, basePath));
84
+ routes.push(createClaudeProxyRoutes(undefined, basePath, "fill-first", false, undefined, { inboundAuth: proxyAuth }));
68
85
  }
69
86
  if (enableOpenAIProxy) {
70
87
  routes.push(createOpenAIProxyRoutes(undefined, basePath));
71
88
  }
72
89
  if (enableCodexProxy) {
73
- routes.push(createCodexProxyRoutes(basePath, options?.runtimeConfigProvider));
90
+ routes.push(createCodexProxyRoutes(basePath, options?.runtimeConfigProvider, undefined, undefined, proxyAuth));
74
91
  }
75
92
  if (enableGeminiProxy) {
76
93
  routes.push(createGeminiProxyRoutes(undefined, basePath));
@@ -92,13 +109,27 @@ export function registerAllRoutes(adapter, basePath = "/api", options) {
92
109
  if (options?.perUserAuth) {
93
110
  adapter.enableMiddlewareStateCarry?.();
94
111
  }
112
+ const allowUnauthenticated = options?.allowUnauthenticated === true || allowUnauthenticatedFromEnv();
113
+ // The adapter knows whether authentication middleware is registered, and is
114
+ // asked on every request, so authentication registered after the routes
115
+ // counts. It also warns at start() when the routes that act on the host are
116
+ // mounted with no such middleware (see BaseServerAdapter).
117
+ adapter.noteSensitiveRoutes?.({ allowUnauthenticated });
95
118
  // If adapter has listRoutes and getRoutes not provided, use adapter's listRoutes
96
119
  const routeOptions = {
97
120
  ...options,
98
121
  getRoutes: options?.getRoutes ?? adapter.listRoutes?.bind(adapter),
122
+ hasAuthentication: options?.hasAuthentication ??
123
+ (adapter.hasAuthenticationMiddleware
124
+ ? () => adapter.hasAuthenticationMiddleware?.() === true
125
+ : undefined),
99
126
  };
100
127
  const routeGroups = createAllRoutes(basePath, routeOptions);
101
128
  for (const group of routeGroups) {
102
129
  adapter.registerRouteGroup(group);
103
130
  }
104
131
  }
132
+ /** `NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1`: the operator's opt-out, read when routes are built. */
133
+ function allowUnauthenticatedFromEnv() {
134
+ return process.env.NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED?.trim() === "1";
135
+ }
@@ -2,8 +2,12 @@
2
2
  * MCP Routes
3
3
  * Endpoints for MCP server management
4
4
  */
5
- import type { RouteGroup } from "../../types/index.js";
5
+ import type { RouteGroup, ServerSensitiveRouteOptions } from "../../types/index.js";
6
6
  /**
7
7
  * Create MCP server management routes
8
+ *
9
+ * @param options - `hasAuthentication` / `allowUnauthenticated`: whether the
10
+ * routes that change or use servers (reconnect, remove, execute a tool)
11
+ * answer an unauthenticated caller (see `ServerSensitiveRouteOptions`).
8
12
  */
9
- export declare function createMCPRoutes(basePath?: string): RouteGroup;
13
+ export declare function createMCPRoutes(basePath?: string, options?: ServerSensitiveRouteOptions): RouteGroup;
@@ -6,7 +6,14 @@ import { SpanStatusCode } from "@opentelemetry/api";
6
6
  import { z } from "zod";
7
7
  import { withSpan } from "../../telemetry/withSpan.js";
8
8
  import { tracers } from "../../telemetry/tracers.js";
9
+ import { shapeRouteError } from "../utils/clientError.js";
10
+ import { refuseUnauthenticatedExecution } from "./toolRoutes.js";
9
11
  import { createErrorResponse, ServerNameParamSchema, ToolArgumentsSchema, validateParams, validateRequest, } from "../utils/validation.js";
12
+ /** The route's error response for an exception: a fixed message, the detail logged. */
13
+ function mcpFailure(code, ctx, error, details) {
14
+ const shaped = shapeRouteError("mcpRoutes", ctx.requestId, error);
15
+ return createErrorResponse(code, shaped.message, details, ctx.requestId, shaped.status);
16
+ }
10
17
  /**
11
18
  * Wrap a route handler with an OTel span for HTTP observability.
12
19
  */
@@ -166,7 +173,7 @@ async function handleReconnectServer(ctx) {
166
173
  };
167
174
  }
168
175
  catch (error) {
169
- return createErrorResponse("RECONNECT_FAILED", error instanceof Error ? error.message : String(error), undefined, ctx.requestId);
176
+ return mcpFailure("RECONNECT_FAILED", ctx, error);
170
177
  }
171
178
  }
172
179
  /**
@@ -198,7 +205,7 @@ async function handleRemoveServer(ctx) {
198
205
  };
199
206
  }
200
207
  catch (error) {
201
- return createErrorResponse("REMOVE_FAILED", error instanceof Error ? error.message : String(error), undefined, ctx.requestId);
208
+ return mcpFailure("REMOVE_FAILED", ctx, error);
202
209
  }
203
210
  }
204
211
  /**
@@ -278,7 +285,9 @@ async function handleExecuteTool(ctx) {
278
285
  };
279
286
  }
280
287
  catch (error) {
281
- return createErrorResponse("EXECUTION_FAILED", error instanceof Error ? error.message : String(error), { duration: Date.now() - startTime }, ctx.requestId);
288
+ return mcpFailure("EXECUTION_FAILED", ctx, error, {
289
+ duration: Date.now() - startTime,
290
+ });
282
291
  }
283
292
  }
284
293
  /**
@@ -316,8 +325,13 @@ async function handleMCPHealth(ctx) {
316
325
  }
317
326
  /**
318
327
  * Create MCP server management routes
328
+ *
329
+ * @param options - `hasAuthentication` / `allowUnauthenticated`: whether the
330
+ * routes that change or use servers (reconnect, remove, execute a tool)
331
+ * answer an unauthenticated caller (see `ServerSensitiveRouteOptions`).
319
332
  */
320
- export function createMCPRoutes(basePath = "/api") {
333
+ export function createMCPRoutes(basePath = "/api", options) {
334
+ const guarded = (handler) => async (ctx) => refuseUnauthenticatedExecution(ctx, options) ?? (await handler(ctx));
321
335
  return {
322
336
  prefix: `${basePath}/mcp`,
323
337
  routes: [
@@ -338,14 +352,14 @@ export function createMCPRoutes(basePath = "/api") {
338
352
  {
339
353
  method: "POST",
340
354
  path: `${basePath}/mcp/servers/:name/reconnect`,
341
- handler: tracedMcpHandler("http.mcp.reconnectServer", `${basePath}/mcp/servers/:name/reconnect`, handleReconnectServer),
355
+ handler: tracedMcpHandler("http.mcp.reconnectServer", `${basePath}/mcp/servers/:name/reconnect`, guarded(handleReconnectServer)),
342
356
  description: "Reconnect to an MCP server",
343
357
  tags: ["mcp"],
344
358
  },
345
359
  {
346
360
  method: "DELETE",
347
361
  path: `${basePath}/mcp/servers/:name`,
348
- handler: tracedMcpHandler("http.mcp.removeServer", `${basePath}/mcp/servers/:name`, handleRemoveServer),
362
+ handler: tracedMcpHandler("http.mcp.removeServer", `${basePath}/mcp/servers/:name`, guarded(handleRemoveServer)),
349
363
  description: "Remove an MCP server",
350
364
  tags: ["mcp"],
351
365
  },
@@ -359,7 +373,7 @@ export function createMCPRoutes(basePath = "/api") {
359
373
  {
360
374
  method: "POST",
361
375
  path: `${basePath}/mcp/servers/:name/tools/:toolName/execute`,
362
- handler: tracedMcpHandler("http.mcp.executeTool", `${basePath}/mcp/servers/:name/tools/:toolName/execute`, handleExecuteTool),
376
+ handler: tracedMcpHandler("http.mcp.executeTool", `${basePath}/mcp/servers/:name/tools/:toolName/execute`, guarded(handleExecuteTool)),
363
377
  description: "Execute a tool from a specific MCP server",
364
378
  tags: ["mcp", "tools"],
365
379
  },
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { withSpan } from "../../telemetry/withSpan.js";
6
6
  import { tracers } from "../../telemetry/tracers.js";
7
+ import { shapeRouteError } from "../utils/clientError.js";
7
8
  import { createErrorResponse, IdParamSchema, SessionIdParamSchema, validateParams, } from "../utils/validation.js";
8
9
  /**
9
10
  * Wrap a route handler with an OTel span for HTTP observability.
@@ -81,7 +82,7 @@ async function handleGetSessionMessages(ctx) {
81
82
  };
82
83
  }
83
84
  catch (error) {
84
- return createErrorResponse("MEMORY_ERROR", error instanceof Error ? error.message : "Failed to get session messages", undefined, ctx.requestId);
85
+ return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
85
86
  }
86
87
  }
87
88
  /**
@@ -132,7 +133,7 @@ async function handleGetSession(ctx) {
132
133
  return createErrorResponse("MEMORY_ERROR", "Session retrieval not supported for this memory type", undefined, ctx.requestId);
133
134
  }
134
135
  catch (error) {
135
- return createErrorResponse("MEMORY_ERROR", error instanceof Error ? error.message : "Failed to get session", undefined, ctx.requestId);
136
+ return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
136
137
  }
137
138
  }
138
139
  /**
@@ -202,7 +203,7 @@ async function handleListSessions(ctx) {
202
203
  };
203
204
  }
204
205
  catch (error) {
205
- return createErrorResponse("MEMORY_ERROR", error instanceof Error ? error.message : "Failed to list sessions", undefined, ctx.requestId);
206
+ return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
206
207
  }
207
208
  }
208
209
  /**
@@ -226,7 +227,7 @@ async function handleGetStats(ctx) {
226
227
  };
227
228
  }
228
229
  catch (error) {
229
- return createErrorResponse("MEMORY_ERROR", error instanceof Error ? error.message : "Failed to get statistics", undefined, ctx.requestId);
230
+ return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
230
231
  }
231
232
  }
232
233
  /**
@@ -260,7 +261,7 @@ async function handleClearSession(ctx) {
260
261
  };
261
262
  }
262
263
  catch (error) {
263
- return createErrorResponse("MEMORY_ERROR", error instanceof Error ? error.message : "Failed to clear session", undefined, ctx.requestId);
264
+ return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
264
265
  }
265
266
  }
266
267
  /**
@@ -284,7 +285,7 @@ async function handleClearAllSessions(ctx) {
284
285
  };
285
286
  }
286
287
  catch (error) {
287
- return createErrorResponse("MEMORY_ERROR", error instanceof Error ? error.message : "Failed to clear sessions", undefined, ctx.requestId);
288
+ return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
288
289
  }
289
290
  }
290
291
  /**
@@ -2,8 +2,18 @@
2
2
  * Tool Routes
3
3
  * Endpoints for tool listing, discovery, and execution
4
4
  */
5
- import type { RouteGroup } from "../../types/index.js";
5
+ import type { ErrorResponse, RouteGroup, ServerContext, ServerSensitiveRouteOptions } from "../../types/index.js";
6
+ /**
7
+ * The 403 for an execution route on a server that authenticates nobody, or
8
+ * `undefined` when the request may proceed: the server authenticates, the
9
+ * operator opted out, or the routes were built without a way to tell.
10
+ */
11
+ export declare function refuseUnauthenticatedExecution(ctx: ServerContext, options: ServerSensitiveRouteOptions | undefined): ErrorResponse | undefined;
6
12
  /**
7
13
  * Create tool routes
14
+ *
15
+ * @param options - `hasAuthentication` / `allowUnauthenticated`: whether the
16
+ * execution routes answer an unauthenticated caller (see
17
+ * `ServerSensitiveRouteOptions`). `createAllRoutes` supplies them.
8
18
  */
9
- export declare function createToolRoutes(basePath?: string): RouteGroup;
19
+ export declare function createToolRoutes(basePath?: string, options?: ServerSensitiveRouteOptions): RouteGroup;
@@ -5,11 +5,29 @@
5
5
  import { SpanStatusCode } from "@opentelemetry/api";
6
6
  import { withSpan } from "../../telemetry/withSpan.js";
7
7
  import { tracers } from "../../telemetry/tracers.js";
8
+ import { shapeRouteError } from "../utils/clientError.js";
8
9
  import { createErrorResponse, ToolArgumentsSchema, ToolExecuteRequestSchema, ToolNameParamSchema, validateParams, validateRequest, } from "../utils/validation.js";
10
+ /**
11
+ * The 403 for an execution route on a server that authenticates nobody, or
12
+ * `undefined` when the request may proceed: the server authenticates, the
13
+ * operator opted out, or the routes were built without a way to tell.
14
+ */
15
+ export function refuseUnauthenticatedExecution(ctx, options) {
16
+ if (!options?.hasAuthentication ||
17
+ options.allowUnauthenticated === true ||
18
+ options.hasAuthentication()) {
19
+ return undefined;
20
+ }
21
+ return createErrorResponse("FORBIDDEN", "This route executes tools on the server and is not served to unauthenticated callers. Register authentication middleware, or opt out with allowUnauthenticated / NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1.", undefined, ctx.requestId, 403);
22
+ }
9
23
  /**
10
24
  * Create tool routes
25
+ *
26
+ * @param options - `hasAuthentication` / `allowUnauthenticated`: whether the
27
+ * execution routes answer an unauthenticated caller (see
28
+ * `ServerSensitiveRouteOptions`). `createAllRoutes` supplies them.
11
29
  */
12
- export function createToolRoutes(basePath = "/api") {
30
+ export function createToolRoutes(basePath = "/api", options) {
13
31
  return {
14
32
  prefix: `${basePath}/tools`,
15
33
  // IMPORTANT: Route ordering matters for proper matching!
@@ -89,6 +107,10 @@ export function createToolRoutes(basePath = "/api") {
89
107
  method: "POST",
90
108
  path: `${basePath}/tools/execute`,
91
109
  handler: async (ctx) => {
110
+ const refused = refuseUnauthenticatedExecution(ctx, options);
111
+ if (refused) {
112
+ return refused;
113
+ }
92
114
  // Validate request body
93
115
  const validation = validateRequest(ToolExecuteRequestSchema, ctx.body, ctx.requestId);
94
116
  if (!validation.success) {
@@ -141,9 +163,12 @@ export function createToolRoutes(basePath = "/api") {
141
163
  message: errorMessage,
142
164
  });
143
165
  span.recordException(error instanceof Error ? error : new Error(errorMessage));
166
+ // The tool's own exception text (paths, upstream bodies) stays
167
+ // in the log under the request id.
144
168
  return {
145
169
  success: false,
146
- error: errorMessage,
170
+ error: shapeRouteError("toolRoutes", ctx.requestId, error)
171
+ .message,
147
172
  duration: Date.now() - startTime,
148
173
  };
149
174
  }
@@ -157,6 +182,10 @@ export function createToolRoutes(basePath = "/api") {
157
182
  method: "POST",
158
183
  path: `${basePath}/tools/:name/execute`,
159
184
  handler: async (ctx) => {
185
+ const refused = refuseUnauthenticatedExecution(ctx, options);
186
+ if (refused) {
187
+ return refused;
188
+ }
160
189
  // Validate params
161
190
  const paramValidation = validateParams(ToolNameParamSchema, ctx.params, ctx.requestId);
162
191
  if (!paramValidation.success) {
@@ -216,7 +245,8 @@ export function createToolRoutes(basePath = "/api") {
216
245
  span.recordException(error instanceof Error ? error : new Error(errorMessage));
217
246
  return {
218
247
  success: false,
219
- error: errorMessage,
248
+ error: shapeRouteError("toolRoutes", ctx.requestId, error)
249
+ .message,
220
250
  duration: Date.now() - startTime,
221
251
  };
222
252
  }