@juspay/neurolink 13.8.17 → 13.8.19

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 (214) hide show
  1. package/CHANGELOG.md +5 -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/providers/openaiChatCompletionsClient.js +12 -3
  77. package/dist/proxy/accountLedger.js +6 -8
  78. package/dist/proxy/geminiFormat.d.ts +3 -11
  79. package/dist/proxy/geminiFormat.js +11 -2
  80. package/dist/proxy/inboundAuth.d.ts +89 -0
  81. package/dist/proxy/inboundAuth.js +219 -0
  82. package/dist/proxy/openaiFormat.d.ts +3 -9
  83. package/dist/proxy/openaiFormat.js +21 -15
  84. package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
  85. package/dist/proxy/proxyTranslationEngine.js +25 -14
  86. package/dist/proxy/routingPolicy.d.ts +3 -1
  87. package/dist/proxy/routingPolicy.js +7 -8
  88. package/dist/proxy/tokenRefresh.d.ts +6 -0
  89. package/dist/proxy/tokenRefresh.js +13 -1
  90. package/dist/proxy/updateState.js +4 -4
  91. package/dist/rag/document/loaders.d.ts +0 -6
  92. package/dist/rag/document/loaders.js +16 -10
  93. package/dist/rag/ragIntegration.d.ts +2 -2
  94. package/dist/rag/ragIntegration.js +35 -10
  95. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  96. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  97. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  98. package/dist/server/abstract/baseServerAdapter.js +119 -6
  99. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  100. package/dist/server/adapters/expressAdapter.js +78 -6
  101. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  102. package/dist/server/adapters/fastifyAdapter.js +165 -70
  103. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  104. package/dist/server/adapters/honoAdapter.js +99 -21
  105. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  106. package/dist/server/adapters/koaAdapter.js +153 -74
  107. package/dist/server/index.d.ts +2 -0
  108. package/dist/server/index.js +4 -0
  109. package/dist/server/middleware/auth.js +6 -2
  110. package/dist/server/middleware/cache.js +6 -1
  111. package/dist/server/middleware/rateLimit.js +29 -4
  112. package/dist/server/routes/agentRoutes.js +24 -11
  113. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  114. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  115. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  116. package/dist/server/routes/codexProxyRoutes.js +125 -40
  117. package/dist/server/routes/index.d.ts +4 -0
  118. package/dist/server/routes/index.js +35 -4
  119. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  120. package/dist/server/routes/mcpRoutes.js +21 -7
  121. package/dist/server/routes/memoryRoutes.js +7 -6
  122. package/dist/server/routes/toolRoutes.d.ts +12 -2
  123. package/dist/server/routes/toolRoutes.js +33 -3
  124. package/dist/server/routes/transcribeRoutes.js +60 -13
  125. package/dist/server/utils/clientAddress.d.ts +23 -0
  126. package/dist/server/utils/clientAddress.js +65 -0
  127. package/dist/server/utils/clientError.d.ts +35 -0
  128. package/dist/server/utils/clientError.js +129 -0
  129. package/dist/server/utils/pathMatch.d.ts +21 -0
  130. package/dist/server/utils/pathMatch.js +41 -0
  131. package/dist/server/utils/validation.d.ts +9 -0
  132. package/dist/server/utils/validation.js +9 -0
  133. package/dist/server/websocket/WebSocketHandler.js +22 -3
  134. package/dist/server/websocket/frameQueue.d.ts +19 -0
  135. package/dist/server/websocket/frameQueue.js +41 -3
  136. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  137. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  138. package/dist/skills/skillStores.js +5 -0
  139. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  140. package/dist/tasks/store/fileTaskStore.js +11 -4
  141. package/dist/types/attachments.d.ts +117 -0
  142. package/dist/types/attachments.js +15 -0
  143. package/dist/types/auth.d.ts +6 -0
  144. package/dist/types/cli.d.ts +4 -0
  145. package/dist/types/config.d.ts +56 -0
  146. package/dist/types/file.d.ts +8 -0
  147. package/dist/types/hitl.d.ts +8 -0
  148. package/dist/types/index.d.ts +1 -0
  149. package/dist/types/index.js +1 -0
  150. package/dist/types/mcp.d.ts +98 -0
  151. package/dist/types/multimodal.js +0 -41
  152. package/dist/types/openaiCompatible.d.ts +1 -1
  153. package/dist/types/processor.d.ts +14 -1
  154. package/dist/types/proxy.d.ts +71 -16
  155. package/dist/types/proxyClient.d.ts +2 -0
  156. package/dist/types/rag.d.ts +9 -0
  157. package/dist/types/safeFetch.d.ts +40 -0
  158. package/dist/types/server.d.ts +206 -10
  159. package/dist/types/tools.d.ts +57 -0
  160. package/dist/types/transcribe.d.ts +6 -0
  161. package/dist/utils/attachmentPolicy.d.ts +89 -0
  162. package/dist/utils/attachmentPolicy.js +327 -0
  163. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  164. package/dist/utils/dotenvBootstrap.js +172 -31
  165. package/dist/utils/fileDetector.js +115 -65
  166. package/dist/utils/json/coerce.js +11 -1
  167. package/dist/utils/logSanitize.d.ts +5 -0
  168. package/dist/utils/logSanitize.js +8 -0
  169. package/dist/utils/logger.js +73 -1
  170. package/dist/utils/messageBuilder.d.ts +2 -2
  171. package/dist/utils/messageBuilder.js +118 -105
  172. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  173. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  174. package/dist/utils/redis.d.ts +12 -0
  175. package/dist/utils/redis.js +72 -0
  176. package/dist/utils/retryAfter.d.ts +32 -0
  177. package/dist/utils/retryAfter.js +135 -11
  178. package/dist/utils/safeFetch.d.ts +22 -1
  179. package/dist/utils/safeFetch.js +132 -38
  180. package/dist/utils/safeStoreId.d.ts +19 -0
  181. package/dist/utils/safeStoreId.js +29 -0
  182. package/dist/utils/schemaConversion.d.ts +30 -3
  183. package/dist/utils/schemaConversion.js +457 -84
  184. package/dist/utils/sizeGuard.js +2 -2
  185. package/dist/utils/ssrfGuard.d.ts +3 -3
  186. package/dist/utils/ssrfGuard.js +71 -39
  187. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  188. package/dist/voice/eval/sttEvalStore.js +77 -12
  189. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  190. package/dist/voice/livekit/agentDefinition.js +22 -0
  191. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  192. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  193. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  194. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  195. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  196. package/dist/voice/livekit/voiceAgent.js +2 -1
  197. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  198. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  199. package/dist/voice/providers/GeminiLive.js +21 -6
  200. package/dist/voice/providers/GoogleSTT.js +8 -5
  201. package/dist/voice/transcribe.d.ts +2 -2
  202. package/dist/voice/transcribe.js +34 -18
  203. package/dist/voice/whistle/audio.js +11 -5
  204. package/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
  205. package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
  206. package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
  207. package/dist/workflow/workflows/consensusWorkflow.js +12 -12
  208. package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
  209. package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
  210. package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
  211. package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
  212. package/docs-site/static/search-index.json +74 -52
  213. package/package.json +173 -156
  214. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -26,7 +26,9 @@ import chalk from "chalk";
26
26
  import ora from "ora";
27
27
  import { logger } from "../../utils/logger.js";
28
28
  import { defaultTokenStore } from "../../auth/tokenStore.js";
29
- import { CLAUDE_CODE_CLIENT_ID, ANTHROPIC_AUTH_URL, ANTHROPIC_TOKEN_URL, ANTHROPIC_REDIRECT_URI, CLAUDE_CLI_USER_AGENT, OAUTH_BETA_HEADERS, } from "../../auth/anthropicOAuth.js";
29
+ import { CLAUDE_CODE_CLIENT_ID, ANTHROPIC_AUTH_URL, ANTHROPIC_TOKEN_URL, ANTHROPIC_REDIRECT_URI, CLAUDE_CLI_USER_AGENT, OAUTH_BETA_HEADERS, buildSubscriptionAuthUrl, exchangePastedSubscriptionCode, parsePastedAuthorizationCode, } from "../../auth/anthropicOAuth.js";
30
+ import { tightenProcessUmask, writePrivateFileAtomicallySync, } from "../../auth/secureFiles.js";
31
+ import { normalizeAnthropicAccountKey } from "../../proxy/accountSelection.js";
30
32
  import { flushAccountQuotas, loadAccountQuotas, saveAccountQuota, } from "../../proxy/accountQuota.js";
31
33
  import { fetchAccountUsage, listAnthropicAccountsForUsage, usageToQuota, } from "../../proxy/accountUsage.js";
32
34
  import { importCodexAuthFile } from "../../auth/codexOAuth.js";
@@ -78,6 +80,7 @@ const AUTH_LOGIN_PROVIDERS = ["anthropic", "codex"];
78
80
  * (e.g., "anthropic:alice") to support multi-account pools.
79
81
  */
80
82
  export async function handleLogin(argv) {
83
+ tightenProcessUmask();
81
84
  try {
82
85
  const provider = argv.provider?.toLowerCase();
83
86
  // Validate provider
@@ -142,7 +145,7 @@ export async function handleCodexLogin(argv) {
142
145
  credential.email ??
143
146
  credential.accountId?.slice(0, 8) ??
144
147
  Date.now().toString(36).slice(-6);
145
- const label = rawLabel.trim();
148
+ const label = validateAccountLabel(rawLabel);
146
149
  const compoundKey = `codex:${label}`;
147
150
  const scopeParts = ["codex"];
148
151
  if (credential.planType) {
@@ -1020,6 +1023,7 @@ async function handleCodexRefresh(argv) {
1020
1023
  * `neurolink auth refresh <provider>`
1021
1024
  */
1022
1025
  export async function handleRefresh(argv) {
1026
+ tightenProcessUmask();
1023
1027
  try {
1024
1028
  const provider = argv.provider?.toLowerCase();
1025
1029
  // Validate provider
@@ -1093,15 +1097,25 @@ export async function handleRefresh(argv) {
1093
1097
  createdAt: credentials.createdAt,
1094
1098
  updatedAt: Date.now(),
1095
1099
  });
1096
- // Also update the TokenStore pool entries for this provider.
1097
- // Update both the bare provider key and compound "provider:label" keys
1098
- // so that pooled accounts created via saveAccountToPool() stay current.
1100
+ // Also update the TokenStore pool entries for this provider — but only
1101
+ // the entries that held the credential just refreshed. Writing the new
1102
+ // token to every `provider:*` key collapsed a multi-account pool into
1103
+ // one identity and threw the other accounts' refresh tokens away.
1104
+ const previousAccessToken = credentials.oauth.accessToken;
1105
+ const previousRefreshToken = credentials.oauth.refreshToken;
1099
1106
  try {
1100
1107
  const allKeys = await defaultTokenStore.listProviders();
1101
1108
  for (const key of allKeys) {
1102
1109
  if (key === provider || key.startsWith(`${provider}:`)) {
1103
1110
  const existingTokens = await defaultTokenStore.loadTokens(key);
1104
- if (existingTokens) {
1111
+ const heldThisCredential = existingTokens !== null &&
1112
+ (existingTokens.accessToken === previousAccessToken ||
1113
+ (previousRefreshToken !== undefined &&
1114
+ existingTokens.refreshToken === previousRefreshToken));
1115
+ if (existingTokens && !heldThisCredential) {
1116
+ logger.debug(`Skipping TokenStore entry ${key}: it holds a different account's credential`);
1117
+ }
1118
+ if (existingTokens && heldThisCredential) {
1105
1119
  await defaultTokenStore.saveTokens(key, {
1106
1120
  accessToken: newTokens.accessToken,
1107
1121
  refreshToken: newTokens.refreshToken,
@@ -1945,11 +1959,13 @@ async function handleCreateApiKeyOAuth(provider) {
1945
1959
  logger.always(chalk.yellow("Copy that code and paste it back here.\n"));
1946
1960
  }
1947
1961
  const spinner = ora("Starting OAuth flow...").start();
1948
- // Generate PKCE challenge - OpenCode sets state = verifier
1962
+ // PKCE verifier and an unrelated random state. The state is what the pasted
1963
+ // code must come back with; the verifier never leaves this process.
1949
1964
  const codeVerifier = randomBytes(32).toString("base64url");
1950
1965
  const codeChallenge = createHash("sha256")
1951
1966
  .update(codeVerifier)
1952
1967
  .digest("base64url");
1968
+ const oauthState = randomBytes(32).toString("base64url");
1953
1969
  // Build authorization URL using CONSOLE config (for API key creation scope)
1954
1970
  // Based on opencode-anthropic-auth implementation
1955
1971
  const authUrl = new URL(ANTHROPIC_CONSOLE_OAUTH_CONFIG.authorizationUrl);
@@ -1960,8 +1976,7 @@ async function handleCreateApiKeyOAuth(provider) {
1960
1976
  authUrl.searchParams.set("scope", ANTHROPIC_CONSOLE_OAUTH_CONFIG.scope);
1961
1977
  authUrl.searchParams.set("code_challenge", codeChallenge);
1962
1978
  authUrl.searchParams.set("code_challenge_method", "S256");
1963
- // OpenCode sets state = verifier for simplicity
1964
- authUrl.searchParams.set("state", codeVerifier);
1979
+ authUrl.searchParams.set("state", oauthState);
1965
1980
  spinner.text = "Opening browser for authentication...";
1966
1981
  // Open browser
1967
1982
  try {
@@ -2005,15 +2020,17 @@ async function handleCreateApiKeyOAuth(provider) {
2005
2020
  },
2006
2021
  ]);
2007
2022
  const exchangeSpinner = ora("Exchanging authorization code for tokens...").start();
2008
- // Parse code#state format (e.g., "abc123#xyz789")
2009
- // IMPORTANT: OpenCode sets state = verifier in the auth URL, so the state
2010
- // returned in code#state IS the original verifier used for the code challenge!
2011
- const trimmedCode = authCode.trim();
2012
- const splits = trimmedCode.split("#");
2013
- const actualCode = splits[0];
2014
- const codeState = splits[1] || codeVerifier;
2015
- // Use the state as the verifier (since we set state = verifier in the auth URL)
2016
- const actualVerifier = codeState;
2023
+ // The pasted value is `code#state`. The state must be the one this login
2024
+ // generated, and the exchange always uses this process's own verifier — a
2025
+ // code from someone else's login is refused before anything is sent.
2026
+ let actualCode;
2027
+ try {
2028
+ actualCode = parsePastedAuthorizationCode(authCode, oauthState).code;
2029
+ }
2030
+ catch (error) {
2031
+ exchangeSpinner.fail("Authorization code rejected");
2032
+ throw error;
2033
+ }
2017
2034
  // Exchange code for tokens using JSON body (per opencode-anthropic-auth)
2018
2035
  const tokenResponse = await fetch(ANTHROPIC_CONSOLE_OAUTH_CONFIG.tokenUrl, {
2019
2036
  method: "POST",
@@ -2022,11 +2039,11 @@ async function handleCreateApiKeyOAuth(provider) {
2022
2039
  },
2023
2040
  body: JSON.stringify({
2024
2041
  code: actualCode,
2025
- state: codeState,
2042
+ state: oauthState,
2026
2043
  grant_type: "authorization_code",
2027
2044
  client_id: ANTHROPIC_CONSOLE_OAUTH_CONFIG.clientId,
2028
2045
  redirect_uri: ANTHROPIC_CONSOLE_OAUTH_CONFIG.redirectUri,
2029
- code_verifier: actualVerifier,
2046
+ code_verifier: codeVerifier,
2030
2047
  }),
2031
2048
  });
2032
2049
  if (!tokenResponse.ok) {
@@ -2110,23 +2127,21 @@ async function handleOAuthAuth(provider) {
2110
2127
  logger.always(chalk.yellow("Copy that code and paste it back here.\n"));
2111
2128
  }
2112
2129
  const spinner = ora("Starting OAuth flow...").start();
2113
- // Generate PKCE challenge - state = verifier (OpenCode's approach)
2130
+ // PKCE verifier and an unrelated random state, through the same builder the
2131
+ // split-PKCE provisioning flow uses so the two cannot drift. The verifier
2132
+ // never leaves this process; the state is what the pasted code must carry.
2114
2133
  const codeVerifier = randomBytes(32).toString("base64url");
2115
2134
  const codeChallenge = createHash("sha256")
2116
2135
  .update(codeVerifier)
2117
2136
  .digest("base64url");
2118
- // Build authorization URL using claude.ai (NOT console.anthropic.com)
2119
- // This is the direct OAuth flow for Claude Pro/Max
2120
- const authUrl = new URL(ANTHROPIC_OAUTH_CONFIG.authorizationUrl);
2121
- authUrl.searchParams.set("code", "true");
2122
- authUrl.searchParams.set("client_id", ANTHROPIC_OAUTH_CONFIG.clientId);
2123
- authUrl.searchParams.set("response_type", "code");
2124
- authUrl.searchParams.set("redirect_uri", ANTHROPIC_OAUTH_CONFIG.redirectUri);
2125
- authUrl.searchParams.set("scope", ANTHROPIC_OAUTH_CONFIG.scope);
2126
- authUrl.searchParams.set("code_challenge", codeChallenge);
2127
- authUrl.searchParams.set("code_challenge_method", "S256");
2128
- // OpenCode sets state = verifier for simplicity
2129
- authUrl.searchParams.set("state", codeVerifier);
2137
+ const oauthState = randomBytes(32).toString("base64url");
2138
+ const authUrl = new URL(buildSubscriptionAuthUrl({
2139
+ codeChallenge,
2140
+ state: oauthState,
2141
+ clientId: ANTHROPIC_OAUTH_CONFIG.clientId,
2142
+ redirectUri: ANTHROPIC_OAUTH_CONFIG.redirectUri,
2143
+ scope: ANTHROPIC_OAUTH_CONFIG.scope,
2144
+ }));
2130
2145
  spinner.text = "Opening browser for authentication...";
2131
2146
  // Open browser
2132
2147
  try {
@@ -2171,36 +2186,35 @@ async function handleOAuthAuth(provider) {
2171
2186
  ]);
2172
2187
  const trimmedCode = authCode.trim();
2173
2188
  const exchangeSpinner = ora("Exchanging authorization code for tokens...").start();
2174
- // Parse code#state format (e.g., "abc123#xyz789")
2175
- // OpenCode sets state = verifier in the auth URL, so the state
2176
- // returned in code#state IS the original verifier used for the code challenge
2177
- const codeParts = trimmedCode.split("#");
2178
- const actualCode = codeParts[0];
2179
- const codeState = codeParts[1] || codeVerifier;
2180
- // Use the state as the verifier (since we set state = verifier in the auth URL)
2181
- const actualVerifier = codeState;
2182
- // Exchange code for tokens with Claude CLI User-Agent
2183
- // IMPORTANT: Uses JSON body, not URLSearchParams
2184
- const tokenResponse = await fetch(ANTHROPIC_OAUTH_CONFIG.tokenUrl, {
2185
- method: "POST",
2186
- headers: {
2187
- "Content-Type": "application/json",
2188
- },
2189
- body: JSON.stringify({
2190
- code: actualCode,
2191
- state: codeState,
2192
- grant_type: "authorization_code",
2193
- client_id: ANTHROPIC_OAUTH_CONFIG.clientId,
2194
- redirect_uri: ANTHROPIC_OAUTH_CONFIG.redirectUri,
2195
- code_verifier: actualVerifier,
2196
- }),
2197
- });
2198
- if (!tokenResponse.ok) {
2199
- const errorText = await tokenResponse.text();
2189
+ // The pasted value is `code#state`: the state must match this login and the
2190
+ // exchange uses this process's verifier. A code from someone else's login is
2191
+ // refused before anything is sent.
2192
+ let exchanged;
2193
+ try {
2194
+ exchanged = await exchangePastedSubscriptionCode({
2195
+ pasted: trimmedCode,
2196
+ expectedState: oauthState,
2197
+ codeVerifier,
2198
+ clientId: ANTHROPIC_OAUTH_CONFIG.clientId,
2199
+ redirectUri: ANTHROPIC_OAUTH_CONFIG.redirectUri,
2200
+ });
2201
+ }
2202
+ catch (error) {
2200
2203
  exchangeSpinner.fail("Token exchange failed");
2201
- throw new Error(`Token exchange failed: ${tokenResponse.status} - ${errorText}`);
2204
+ throw error;
2202
2205
  }
2203
- const tokenData = (await tokenResponse.json());
2206
+ const tokenData = {
2207
+ access_token: exchanged.accessToken,
2208
+ refresh_token: exchanged.refreshToken,
2209
+ expires_in: exchanged.expiresAt !== undefined
2210
+ ? Math.max(1, Math.round((exchanged.expiresAt - Date.now()) / 1000))
2211
+ : undefined,
2212
+ token_type: exchanged.tokenType,
2213
+ scope: exchanged.scope,
2214
+ account: exchanged.accountEmail
2215
+ ? { email_address: exchanged.accountEmail }
2216
+ : undefined,
2217
+ };
2204
2218
  // Save tokens
2205
2219
  const tokens = {
2206
2220
  accessToken: tokenData.access_token,
@@ -2290,11 +2304,17 @@ async function saveAccountToPool(provider, label) {
2290
2304
  const credEmail = credentials.email;
2291
2305
  let compoundKey;
2292
2306
  if (label) {
2293
- compoundKey = `${provider}:${label}`;
2307
+ compoundKey = `${provider}:${validateAccountLabel(label)}`;
2294
2308
  }
2295
2309
  else {
2296
2310
  const autoLabel = credEmail ?? Date.now().toString(36).slice(-6);
2297
- compoundKey = `${provider}:${autoLabel}`;
2311
+ compoundKey = `${provider}:${validateAccountLabel(autoLabel)}`;
2312
+ }
2313
+ if (provider === "anthropic") {
2314
+ // The proxy compares Anthropic keys case-insensitively
2315
+ // (normalizeAnthropicAccountKey); saving them as typed let `Alice` and
2316
+ // `alice` coexist as two pool entries that every lookup treated as one.
2317
+ compoundKey = normalizeAnthropicAccountKey(compoundKey);
2298
2318
  }
2299
2319
  // Decision 10A: Auto-prune stale token-prefix entries when email is known.
2300
2320
  // When we have a real email, remove old entries whose stored token value is
@@ -2542,14 +2562,36 @@ async function getStoredCredentials(provider) {
2542
2562
  * Save credentials to file
2543
2563
  */
2544
2564
  async function saveStoredCredentials(provider, credentials) {
2545
- // Ensure config directory exists
2546
- if (!fs.existsSync(NEUROLINK_CONFIG_DIR)) {
2547
- fs.mkdirSync(NEUROLINK_CONFIG_DIR, { recursive: true });
2548
- }
2549
2565
  const credentialsFile = path.join(NEUROLINK_CONFIG_DIR, `${provider}-credentials.json`);
2550
- fs.writeFileSync(credentialsFile, JSON.stringify(credentials, null, 2), {
2551
- mode: 0o600, // Restrict permissions
2552
- });
2566
+ // Owner-only directory and an atomic owner-only write: the previous
2567
+ // truncate-in-place write kept whatever mode the file already had and
2568
+ // followed a symlink planted at the path.
2569
+ //
2570
+ // The refresh token stays in this file deliberately. It is the credential
2571
+ // source for the SDK's Anthropic provider (`providers/anthropic/client.ts`)
2572
+ // and for the proxy's legacy single-account path, both of which refresh from
2573
+ // it; removing it would silently break every caller that never ran
2574
+ // `auth login` against the pooled store.
2575
+ writePrivateFileAtomicallySync(credentialsFile, JSON.stringify(credentials, null, 2));
2576
+ }
2577
+ /**
2578
+ * An account label becomes part of a token-store key, a log line and a
2579
+ * snapshot file name. Control characters and newlines have no place in any of
2580
+ * those (a newline forges a log line), so they are refused rather than stored.
2581
+ */
2582
+ function validateAccountLabel(label) {
2583
+ const trimmed = label.trim();
2584
+ if (trimmed === "") {
2585
+ throw new Error("Account label must not be empty.");
2586
+ }
2587
+ // eslint-disable-next-line no-control-regex -- the point is to refuse them
2588
+ if (/[\u0000-\u001f\u007f]/.test(trimmed)) {
2589
+ throw new Error("Account label must not contain control characters or line breaks.");
2590
+ }
2591
+ if (trimmed.length > 200) {
2592
+ throw new Error("Account label must be 200 characters or fewer.");
2593
+ }
2594
+ return trimmed;
2553
2595
  }
2554
2596
  /**
2555
2597
  * Check existing authentication status
@@ -78,6 +78,10 @@ export declare function createProxyStartApp(params: {
78
78
  updateControlToken?: string;
79
79
  /** Port of the gate-only share listener, when one is configured. */
80
80
  sharePort?: number;
81
+ /** Bearer every inbound caller must present; omitted means none is required. */
82
+ inboundToken?: string;
83
+ /** The bind policy already ran and accepted an unauthenticated listener. */
84
+ inboundAuthAcknowledged?: boolean;
81
85
  }): Promise<{
82
86
  app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
83
87
  readiness: ProxyReadinessState;
@@ -20,6 +20,9 @@ import { raceWithAbort, withTimeout, } from "../../utils/async/withTimeout.js";
20
20
  import { startProxyHttpTrace } from "../../proxy/proxyTracer.js";
21
21
  import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
22
22
  import { configureProxyKeepAliveDispatcher } from "../../proxy/proxyDispatcher.js";
23
+ import { evaluateProxyBindPolicy, isUnauthenticatedBindAllowedByEnv, resolveInboundToken, } from "../../proxy/inboundAuth.js";
24
+ import { isGrantRequiredByEnv } from "../../proxy/shareGate.js";
25
+ import { tightenProcessUmask } from "../../auth/secureFiles.js";
23
26
  import { ProxyRuntimeConfigStore } from "../../proxy/runtimeConfig.js";
24
27
  import { sessionAffinity } from "../../proxy/sessionAffinity.js";
25
28
  import { startProxyLogCleanupScheduler } from "../../proxy/logCleanupScheduler.js";
@@ -1439,8 +1442,16 @@ export async function createProxyStartApp(params) {
1439
1442
  const { recordFinalError } = await import("../../proxy/usageStats.js");
1440
1443
  const { admitInboundShareRequest, isGrantRequiredByEnv } = await import("../../proxy/shareGate.js");
1441
1444
  const { runWithShareContext } = await import("../../proxy/shareContext.js");
1445
+ const { authorizeInboundRequest, hasValidInboundToken, isInboundAuthExemptPath, secretsMatch, } = await import("../../proxy/inboundAuth.js");
1442
1446
  const { Hono } = await import("hono");
1443
1447
  const app = new Hono();
1448
+ const inboundToken = params.inboundToken;
1449
+ const inboundAuth = {
1450
+ ...(inboundToken ? { token: inboundToken } : {}),
1451
+ // The CLI ran the bind policy; a loopback or acknowledged listener must
1452
+ // not re-trigger the route factories' unauthenticated-mount warning.
1453
+ allowUnauthenticated: params.inboundAuthAcknowledged ?? true,
1454
+ };
1444
1455
  /**
1445
1456
  * Is this request arriving on the gate-only share listener?
1446
1457
  *
@@ -1582,7 +1593,7 @@ export async function createProxyStartApp(params) {
1582
1593
  app.post("/internal/update-control", async (c) => {
1583
1594
  const suppliedToken = c.req.header("x-neurolink-update-token");
1584
1595
  const expectedToken = params.updateControlToken ?? PROXY_UPDATE_CONTROL_TOKEN;
1585
- if (!suppliedToken || suppliedToken !== expectedToken) {
1596
+ if (!suppliedToken || !secretsMatch(expectedToken, suppliedToken)) {
1586
1597
  return c.json({ error: "not_found" }, 404);
1587
1598
  }
1588
1599
  const payload = await c.req
@@ -1665,12 +1676,11 @@ export async function createProxyStartApp(params) {
1665
1676
  const runtimeConfigProvider = runtimeConfigStore
1666
1677
  ? () => runtimeConfigStore.getSnapshot()
1667
1678
  : undefined;
1668
- const routeGroup = createClaudeProxyRoutes(params.modelRouter, "", params.strategy, params.passthrough, params.primaryAccountKey, runtimeConfigProvider
1669
- ? {
1670
- accountAllowlist: params.accountAllowlist,
1671
- runtimeConfigProvider,
1672
- }
1673
- : params.accountAllowlist);
1679
+ const routeGroup = createClaudeProxyRoutes(params.modelRouter, "", params.strategy, params.passthrough, params.primaryAccountKey, {
1680
+ accountAllowlist: params.accountAllowlist,
1681
+ ...(runtimeConfigProvider ? { runtimeConfigProvider } : {}),
1682
+ inboundAuth,
1683
+ });
1674
1684
  const openaiRouteGroup = createOpenAIProxyRoutes(params.modelRouter, "", params.port, runtimeConfigProvider,
1675
1685
  // Keep bridge capabilities, accounting and cancellation in this worker
1676
1686
  // across supervisor generation switches; never re-enter the public listener.
@@ -1679,7 +1689,7 @@ export async function createProxyStartApp(params) {
1679
1689
  // Same in-process loopback discipline as the OpenAI->Anthropic bridge:
1680
1690
  // stay in this worker across supervisor generation switches, never
1681
1691
  // re-enter the public listener.
1682
- (request) => app.fetch(request));
1692
+ (request) => app.fetch(request), inboundAuth);
1683
1693
  const geminiRouteGroup = createGeminiProxyRoutes(params.modelRouter, "", params.port, runtimeConfigProvider);
1684
1694
  const allProxyRoutes = [
1685
1695
  ...routeGroup.routes,
@@ -1708,6 +1718,27 @@ export async function createProxyStartApp(params) {
1708
1718
  },
1709
1719
  }, 503);
1710
1720
  }
1721
+ // Inbound token, before the body is even read. A request carrying a
1722
+ // share token is left to the share gate below; everything else on a
1723
+ // non-exempt path must present the operator's token.
1724
+ if (inboundToken &&
1725
+ metadata?.accountingScope !== "internal" &&
1726
+ !isInboundAuthExemptPath(c.req.path)) {
1727
+ const verdict = authorizeInboundRequest(Object.fromEntries(c.req.raw.headers.entries()), inboundToken);
1728
+ if (!verdict.ok) {
1729
+ for (const [key, value] of Object.entries(verdict.response.headers)) {
1730
+ c.header(key, value);
1731
+ }
1732
+ if (metadata) {
1733
+ metadata.terminalErrorType = "inbound_auth_required";
1734
+ await recordRuntimeError(metadata, 401, "inbound_auth_required", "Request did not present the inbound token", {
1735
+ clientMessage: verdict.response.body.error.message,
1736
+ clientErrorType: verdict.response.body.error.type,
1737
+ });
1738
+ }
1739
+ return c.json(verdict.response.body, 401);
1740
+ }
1741
+ }
1711
1742
  const emptyBody = {};
1712
1743
  let body;
1713
1744
  let rawBody;
@@ -1946,6 +1977,18 @@ export async function createProxyStartApp(params) {
1946
1977
  }));
1947
1978
  });
1948
1979
  app.get("/status", async (c) => {
1980
+ // `/status` is an operator surface. With an inbound token configured it
1981
+ // is refused outright to anyone without it, not merely redacted.
1982
+ if (inboundToken &&
1983
+ !hasValidInboundToken(Object.fromEntries(c.req.raw.headers.entries()), inboundToken)) {
1984
+ return c.json({
1985
+ type: "error",
1986
+ error: {
1987
+ type: "authentication_error",
1988
+ message: "This proxy requires an inbound token.",
1989
+ },
1990
+ }, 401);
1991
+ }
1949
1992
  // A gated proxy is, by definition, one that may be exposed. `/status`
1950
1993
  // enumerates account labels — which for an OAuth account is an email — plus
1951
1994
  // quota and cooldown state, and it is not behind the request gate, so
@@ -1957,8 +2000,7 @@ export async function createProxyStartApp(params) {
1957
2000
  // token, and redacted for everyone else. Counters and health stay visible so
1958
2001
  // liveness tooling keeps working.
1959
2002
  const statusIdentityAllowed = !isGatedListener(c) ||
1960
- c.req.header("x-neurolink-update-token") ===
1961
- (params.updateControlToken ?? PROXY_UPDATE_CONTROL_TOKEN);
2003
+ secretsMatch(params.updateControlToken ?? PROXY_UPDATE_CONTROL_TOKEN, c.req.header("x-neurolink-update-token") ?? "");
1962
2004
  const runtimeConfig = params.runtimeConfigStore?.getSnapshot();
1963
2005
  const runtimeConfigStatus = params.runtimeConfigStore?.getStatus();
1964
2006
  const activeStrategy = runtimeConfig
@@ -3104,6 +3146,7 @@ async function runLaunchdProxySupervisor(argv, spinner) {
3104
3146
  }
3105
3147
  const host = argv.host ?? "127.0.0.1";
3106
3148
  const port = argv.port ?? 55669;
3149
+ enforceProxyBindPolicy(host, argv, spinner);
3107
3150
  const workerArgs = process.argv.slice(2);
3108
3151
  const supervisorStartedAt = new Date().toISOString();
3109
3152
  configureProxyLifecycleLogger({
@@ -3278,7 +3321,36 @@ export function registerProxySupervisorReload(activeWorkerPid) {
3278
3321
  process.on("SIGHUP", reload);
3279
3322
  return () => process.off("SIGHUP", reload);
3280
3323
  }
3324
+ /**
3325
+ * Decide whether this listener may bind where it was asked to, after the env
3326
+ * file has been loaded (the inbound token may live there). Refusal exits the
3327
+ * process: an open proxy on a routable address is not a configuration to
3328
+ * start and warn about.
3329
+ */
3330
+ function enforceProxyBindPolicy(host, argv, spinner) {
3331
+ const inboundToken = resolveInboundToken(argv.inboundToken);
3332
+ const allowUnauthenticated = argv.allowUnauthenticated === true || isUnauthenticatedBindAllowedByEnv();
3333
+ const decision = evaluateProxyBindPolicy({
3334
+ host,
3335
+ inboundToken,
3336
+ requireGrant: isGrantRequiredByEnv(),
3337
+ allowUnauthenticated,
3338
+ });
3339
+ if (!decision.allowed) {
3340
+ if (spinner) {
3341
+ spinner.fail(chalk.red("Refusing to start an unauthenticated proxy"));
3342
+ }
3343
+ logger.error(chalk.red(decision.reason));
3344
+ process.exit(1);
3345
+ }
3346
+ if (decision.warning) {
3347
+ logger.always(chalk.yellow(`[proxy] WARNING: ${decision.warning}`));
3348
+ }
3349
+ return { inboundToken, acknowledged: true };
3350
+ }
3281
3351
  async function startProxyCommandHandler(argv) {
3352
+ // Everything this process creates under ~/.neurolink is owner-only.
3353
+ tightenProcessUmask();
3282
3354
  const spinner = argv.quiet ? null : ora("Starting Claude proxy...").start();
3283
3355
  const isDev = argv.dev ?? false;
3284
3356
  const socketWorker = isProxySocketWorkerProcess();
@@ -3376,6 +3448,7 @@ async function startProxyCommandHandler(argv) {
3376
3448
  mainPort: port,
3377
3449
  });
3378
3450
  const host = argv.host ?? "127.0.0.1";
3451
+ const bindPolicy = enforceProxyBindPolicy(host, argv, spinner);
3379
3452
  const { app, readiness } = await createProxyStartApp({
3380
3453
  neurolink,
3381
3454
  modelRouter,
@@ -3388,6 +3461,8 @@ async function startProxyCommandHandler(argv) {
3388
3461
  accountAllowlist,
3389
3462
  runtimeConfigStore,
3390
3463
  sharePort,
3464
+ inboundToken: bindPolicy.inboundToken,
3465
+ inboundAuthAcknowledged: bindPolicy.acknowledged,
3391
3466
  });
3392
3467
  await initializeProxyOpenTelemetry();
3393
3468
  if (spinner) {
@@ -3446,7 +3521,21 @@ export const proxyStartCommand = {
3446
3521
  type: "string",
3447
3522
  alias: "H",
3448
3523
  default: "127.0.0.1",
3449
- description: "Host to bind to",
3524
+ description: "Host to bind to. A non-loopback host needs --inbound-token, " +
3525
+ "NEUROLINK_PROXY_REQUIRE_GRANT=1 or --allow-unauthenticated",
3526
+ })
3527
+ .option("inbound-token", {
3528
+ type: "string",
3529
+ alias: "inboundToken",
3530
+ description: "Bearer every caller must present (x-neurolink-inbound-token or " +
3531
+ "Authorization: Bearer). Also NEUROLINK_PROXY_INBOUND_TOKEN",
3532
+ })
3533
+ .option("allow-unauthenticated", {
3534
+ type: "boolean",
3535
+ alias: "allowUnauthenticated",
3536
+ default: false,
3537
+ description: "Bind a non-loopback host with no inbound authentication (an " +
3538
+ "authenticating reverse proxy must sit in front)",
3450
3539
  })
3451
3540
  .option("strategy", {
3452
3541
  type: "string",
@@ -5189,7 +5278,20 @@ export const proxyInstallCommand = {
5189
5278
  })
5190
5279
  .option("host", {
5191
5280
  type: "string",
5192
- description: "Proxy host",
5281
+ description: "Proxy host. A non-loopback host needs --inbound-token, " +
5282
+ "NEUROLINK_PROXY_REQUIRE_GRANT=1 or --allow-unauthenticated",
5283
+ })
5284
+ .option("inbound-token", {
5285
+ type: "string",
5286
+ alias: "inboundToken",
5287
+ description: "Bearer every caller must present; persisted into the service " +
5288
+ "environment as NEUROLINK_PROXY_INBOUND_TOKEN",
5289
+ })
5290
+ .option("allow-unauthenticated", {
5291
+ type: "boolean",
5292
+ alias: "allowUnauthenticated",
5293
+ default: false,
5294
+ description: "Install a non-loopback service with no inbound authentication",
5193
5295
  })
5194
5296
  .option("env-file", {
5195
5297
  type: "string",
@@ -5265,9 +5367,22 @@ export const proxyInstallCommand = {
5265
5367
  process.exit(1);
5266
5368
  }
5267
5369
  await loadProxyEnvFile({ explicitEnvFile: envFile });
5370
+ tightenProcessUmask();
5371
+ // The service inherits NEUROLINK_PROXY_* from this environment (see
5372
+ // isProxyServiceEnvironmentKey), so a flag given here is persisted into
5373
+ // the 0600 plist rather than echoed on the command line.
5374
+ const installArgs = argv;
5375
+ if (installArgs.inboundToken?.trim()) {
5376
+ process.env.NEUROLINK_PROXY_INBOUND_TOKEN =
5377
+ installArgs.inboundToken.trim();
5378
+ }
5379
+ if (installArgs.allowUnauthenticated === true) {
5380
+ process.env.NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED = "1";
5381
+ }
5382
+ enforceProxyBindPolicy(host, installArgs, null);
5268
5383
  const logsDir = join(homedir(), ".neurolink", "logs");
5269
5384
  if (!isProxyOtelOnly() && !existsSync(logsDir)) {
5270
- mkdirSync(logsDir, { recursive: true });
5385
+ mkdirSync(logsDir, { recursive: true, mode: 0o700 });
5271
5386
  }
5272
5387
  if (!existsSync(PLIST_DIR)) {
5273
5388
  mkdirSync(PLIST_DIR, { recursive: true });
@@ -34,7 +34,16 @@ export declare class ServeCommandFactory {
34
34
  */
35
35
  private static createAndStartServer;
36
36
  /**
37
- * Put every route except health behind the API key(s) in
37
+ * The NeuroLink instance the server runs. Its registry holds no built-in
38
+ * tool unless `--enable-builtin-tools`: those tools read and write files
39
+ * under the server's working directory, and `/tools/execute` and the agent
40
+ * routes would hand them to every caller. The switch is the SDK's own
41
+ * (NEUROLINK_DISABLE_BUILTIN_TOOLS), set before the instance and its
42
+ * providers read it, so generate() in this process does not see them either.
43
+ */
44
+ private static createServerNeuroLink;
45
+ /**
46
+ * Put every route except the health probes behind the API key(s) in
38
47
  * NEUROLINK_SERVER_API_KEY. Must run before the routes are registered.
39
48
  */
40
49
  private static applyApiKeyAuth;