@juspay/neurolink 13.8.16 → 13.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +1 -1
  3. package/dist/action/actionExecutor.d.ts +33 -3
  4. package/dist/action/actionExecutor.js +71 -15
  5. package/dist/action/actionInputs.js +4 -1
  6. package/dist/action/cliVersion.d.ts +25 -0
  7. package/dist/action/cliVersion.js +26 -0
  8. package/dist/action/index.d.ts +2 -1
  9. package/dist/action/index.js +2 -1
  10. package/dist/adapters/video/directorPipeline.js +46 -36
  11. package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
  12. package/dist/adapters/video/ffmpegAdapter.js +32 -5
  13. package/dist/adapters/video/frameExtractor.js +1 -1
  14. package/dist/adapters/video/videoMerger.js +5 -2
  15. package/dist/agent/arithmeticEvaluator.d.ts +32 -0
  16. package/dist/agent/arithmeticEvaluator.js +349 -0
  17. package/dist/agent/directTools.d.ts +6 -18
  18. package/dist/agent/directTools.js +17 -56
  19. package/dist/artifacts/artifactStore.d.ts +22 -1
  20. package/dist/artifacts/artifactStore.js +66 -6
  21. package/dist/auth/anthropicOAuth.d.ts +26 -1
  22. package/dist/auth/anthropicOAuth.js +81 -5
  23. package/dist/auth/secureFiles.d.ts +37 -0
  24. package/dist/auth/secureFiles.js +95 -0
  25. package/dist/auth/tokenStore.d.ts +43 -6
  26. package/dist/auth/tokenStore.js +260 -79
  27. package/dist/avatar/providers/DIDAvatar.js +49 -45
  28. package/dist/avatar/providers/ReplicateAvatar.js +33 -40
  29. package/dist/browser/neurolink.min.js +485 -558
  30. package/dist/cli/commands/auth.js +112 -70
  31. package/dist/cli/commands/proxy.d.ts +4 -0
  32. package/dist/cli/commands/proxy.js +128 -13
  33. package/dist/cli/commands/serve.d.ts +10 -1
  34. package/dist/cli/commands/serve.js +98 -17
  35. package/dist/cli/commands/server.js +17 -5
  36. package/dist/cli/index.js +146 -5
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/utils/envManager.js +17 -3
  39. package/dist/cli/utils/serverUtils.d.ts +39 -3
  40. package/dist/cli/utils/serverUtils.js +63 -6
  41. package/dist/core/baseProvider.js +7 -1
  42. package/dist/core/modules/MessageBuilder.js +5 -0
  43. package/dist/core/modules/ToolsManager.d.ts +46 -1
  44. package/dist/core/modules/ToolsManager.js +190 -43
  45. package/dist/files/fileReferenceRegistry.d.ts +2 -23
  46. package/dist/files/fileReferenceRegistry.js +55 -5
  47. package/dist/hitl/hitlManager.js +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +6 -1
  50. package/dist/mcp/auth/tokenStorage.d.ts +7 -0
  51. package/dist/mcp/auth/tokenStorage.js +23 -4
  52. package/dist/mcp/externalServerManager.d.ts +13 -3
  53. package/dist/mcp/externalServerManager.js +154 -136
  54. package/dist/mcp/index.d.ts +2 -0
  55. package/dist/mcp/index.js +3 -0
  56. package/dist/mcp/mcpClientFactory.d.ts +14 -0
  57. package/dist/mcp/mcpClientFactory.js +70 -7
  58. package/dist/mcp/projectConfigTrust.d.ts +72 -0
  59. package/dist/mcp/projectConfigTrust.js +237 -0
  60. package/dist/mcp/transportUrlPolicy.d.ts +39 -0
  61. package/dist/mcp/transportUrlPolicy.js +172 -0
  62. package/dist/music/providers/BeatovenMusic.js +35 -46
  63. package/dist/music/providers/ReplicateMusic.js +33 -40
  64. package/dist/neurolink.d.ts +16 -1
  65. package/dist/neurolink.js +64 -11
  66. package/dist/processors/archive/ArchiveProcessor.js +15 -1
  67. package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
  68. package/dist/processors/archive/zipEntryIndex.js +33 -0
  69. package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
  70. package/dist/processors/base/BaseFileProcessor.js +40 -111
  71. package/dist/processors/data/YamlProcessor.d.ts +13 -1
  72. package/dist/processors/data/YamlProcessor.js +93 -2
  73. package/dist/processors/document/OpenDocumentProcessor.js +7 -2
  74. package/dist/processors/document/PptxProcessor.js +12 -7
  75. package/dist/processors/media/VideoProcessor.js +93 -15
  76. package/dist/proxy/inboundAuth.d.ts +89 -0
  77. package/dist/proxy/inboundAuth.js +219 -0
  78. package/dist/proxy/tokenRefresh.d.ts +6 -0
  79. package/dist/proxy/tokenRefresh.js +13 -1
  80. package/dist/proxy/updateState.js +4 -4
  81. package/dist/rag/document/loaders.d.ts +0 -6
  82. package/dist/rag/document/loaders.js +16 -10
  83. package/dist/rag/ragIntegration.d.ts +2 -2
  84. package/dist/rag/ragIntegration.js +35 -10
  85. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  86. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  87. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  88. package/dist/server/abstract/baseServerAdapter.js +119 -6
  89. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  90. package/dist/server/adapters/expressAdapter.js +83 -8
  91. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  92. package/dist/server/adapters/fastifyAdapter.js +165 -70
  93. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  94. package/dist/server/adapters/honoAdapter.js +99 -21
  95. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  96. package/dist/server/adapters/koaAdapter.js +153 -74
  97. package/dist/server/index.d.ts +2 -0
  98. package/dist/server/index.js +4 -0
  99. package/dist/server/middleware/auth.js +6 -2
  100. package/dist/server/middleware/cache.js +6 -1
  101. package/dist/server/middleware/rateLimit.js +29 -4
  102. package/dist/server/routes/agentRoutes.js +24 -11
  103. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  104. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  105. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  106. package/dist/server/routes/codexProxyRoutes.js +125 -40
  107. package/dist/server/routes/index.d.ts +4 -0
  108. package/dist/server/routes/index.js +35 -4
  109. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  110. package/dist/server/routes/mcpRoutes.js +21 -7
  111. package/dist/server/routes/memoryRoutes.js +7 -6
  112. package/dist/server/routes/toolRoutes.d.ts +12 -2
  113. package/dist/server/routes/toolRoutes.js +33 -3
  114. package/dist/server/routes/transcribeRoutes.js +60 -13
  115. package/dist/server/utils/clientAddress.d.ts +23 -0
  116. package/dist/server/utils/clientAddress.js +65 -0
  117. package/dist/server/utils/clientError.d.ts +35 -0
  118. package/dist/server/utils/clientError.js +129 -0
  119. package/dist/server/utils/expressBody.d.ts +33 -0
  120. package/dist/server/utils/expressBody.js +353 -0
  121. package/dist/server/utils/pathMatch.d.ts +21 -0
  122. package/dist/server/utils/pathMatch.js +41 -0
  123. package/dist/server/utils/validation.d.ts +9 -0
  124. package/dist/server/utils/validation.js +9 -0
  125. package/dist/server/websocket/WebSocketHandler.js +22 -3
  126. package/dist/server/websocket/frameQueue.d.ts +19 -0
  127. package/dist/server/websocket/frameQueue.js +41 -3
  128. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  129. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  130. package/dist/skills/skillStores.js +5 -0
  131. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  132. package/dist/tasks/store/fileTaskStore.js +11 -4
  133. package/dist/types/attachments.d.ts +117 -0
  134. package/dist/types/attachments.js +15 -0
  135. package/dist/types/auth.d.ts +6 -0
  136. package/dist/types/cli.d.ts +4 -0
  137. package/dist/types/config.d.ts +56 -0
  138. package/dist/types/file.d.ts +8 -0
  139. package/dist/types/hitl.d.ts +8 -0
  140. package/dist/types/index.d.ts +1 -0
  141. package/dist/types/index.js +1 -0
  142. package/dist/types/mcp.d.ts +98 -0
  143. package/dist/types/multimodal.js +0 -41
  144. package/dist/types/processor.d.ts +14 -1
  145. package/dist/types/proxy.d.ts +62 -3
  146. package/dist/types/rag.d.ts +9 -0
  147. package/dist/types/safeFetch.d.ts +40 -0
  148. package/dist/types/server.d.ts +255 -10
  149. package/dist/types/tools.d.ts +57 -0
  150. package/dist/types/transcribe.d.ts +6 -0
  151. package/dist/utils/attachmentPolicy.d.ts +89 -0
  152. package/dist/utils/attachmentPolicy.js +327 -0
  153. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  154. package/dist/utils/dotenvBootstrap.js +172 -31
  155. package/dist/utils/fileDetector.js +115 -65
  156. package/dist/utils/json/coerce.js +11 -1
  157. package/dist/utils/logSanitize.d.ts +5 -0
  158. package/dist/utils/logSanitize.js +8 -0
  159. package/dist/utils/logger.js +73 -1
  160. package/dist/utils/messageBuilder.d.ts +2 -2
  161. package/dist/utils/messageBuilder.js +118 -105
  162. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  163. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  164. package/dist/utils/redis.d.ts +12 -0
  165. package/dist/utils/redis.js +72 -0
  166. package/dist/utils/safeFetch.d.ts +22 -1
  167. package/dist/utils/safeFetch.js +132 -38
  168. package/dist/utils/safeStoreId.d.ts +19 -0
  169. package/dist/utils/safeStoreId.js +29 -0
  170. package/dist/utils/schemaConversion.d.ts +30 -3
  171. package/dist/utils/schemaConversion.js +457 -84
  172. package/dist/utils/sizeGuard.js +2 -2
  173. package/dist/utils/ssrfGuard.d.ts +3 -3
  174. package/dist/utils/ssrfGuard.js +71 -39
  175. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  176. package/dist/voice/eval/sttEvalStore.js +77 -12
  177. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  178. package/dist/voice/livekit/agentDefinition.js +22 -0
  179. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  180. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  181. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  182. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  183. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  184. package/dist/voice/livekit/voiceAgent.js +2 -1
  185. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  186. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  187. package/dist/voice/providers/GeminiLive.js +21 -6
  188. package/dist/voice/providers/GoogleSTT.js +8 -5
  189. package/dist/voice/transcribe.d.ts +2 -2
  190. package/dist/voice/transcribe.js +34 -18
  191. package/dist/voice/whistle/audio.js +11 -5
  192. package/docs-site/static/search-index.json +75 -52
  193. package/package.json +171 -156
  194. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -331,7 +331,8 @@ export declare class AnthropicOAuth {
331
331
  * const tokens = await oauth.exchangeCodeForTokens(result.code, codeVerifier);
332
332
  * ```
333
333
  */
334
- export declare function startCallbackServer(port?: number, path?: string, timeout?: number): Promise<CallbackResult>;
334
+ export declare function startCallbackServer(port?: number, path?: string, timeout?: number, // 5 minutes default
335
+ expectedState?: string): Promise<CallbackResult>;
335
336
  /**
336
337
  * Stops the callback server if running
337
338
  * Note: The server automatically stops after receiving a callback or timing out
@@ -448,3 +449,27 @@ export declare function performOAuthFlow(oauth: AnthropicOAuth, options?: {
448
449
  /** Whether to automatically open browser (default: true) */
449
450
  openBrowser?: boolean;
450
451
  }): Promise<OAuthFlowTokens>;
452
+ /**
453
+ * Split a pasted `code#state` string and bind it to the login in progress.
454
+ *
455
+ * The CLI paste flows used to set `state` to the PKCE verifier and then trust
456
+ * whatever the user pasted after `#` as that verifier, so the code was bound to
457
+ * nothing the CLI held: a phished `code#state` from someone else's login landed
458
+ * their account in the victim's pool. The state is now an unrelated random
459
+ * value, it must come back exactly, and the exchange always uses the verifier
460
+ * this process generated.
461
+ */
462
+ export declare function parsePastedAuthorizationCode(pasted: string, expectedState: string): {
463
+ code: string;
464
+ };
465
+ /**
466
+ * Validate a pasted `code#state` against the login in progress and exchange it
467
+ * with the local verifier. Nothing is sent when the state does not match.
468
+ */
469
+ export declare function exchangePastedSubscriptionCode(params: {
470
+ pasted: string;
471
+ expectedState: string;
472
+ codeVerifier: string;
473
+ clientId?: string;
474
+ redirectUri?: string;
475
+ }): ReturnType<typeof exchangeSubscriptionCode>;
@@ -914,7 +914,8 @@ export class AnthropicOAuth {
914
914
  * const tokens = await oauth.exchangeCodeForTokens(result.code, codeVerifier);
915
915
  * ```
916
916
  */
917
- export function startCallbackServer(port = DEFAULT_CALLBACK_PORT, path = "/callback", timeout = 5 * 60 * 1000) {
917
+ export function startCallbackServer(port = DEFAULT_CALLBACK_PORT, path = "/callback", timeout = 5 * 60 * 1000, // 5 minutes default
918
+ expectedState) {
918
919
  return new Promise((resolve, reject) => {
919
920
  let server = null;
920
921
  let timeoutId = null;
@@ -986,6 +987,26 @@ export function startCallbackServer(port = DEFAULT_CALLBACK_PORT, path = "/callb
986
987
  reject(new OAuthCallbackServerError("No authorization code in callback"));
987
988
  return;
988
989
  }
990
+ // A callback whose state is not ours is answered but never resolves the
991
+ // flow: on an all-interfaces listener that was how a LAN peer could abort
992
+ // a login or hand it a code of their choosing. The server keeps waiting
993
+ // for the genuine redirect.
994
+ if (expectedState !== undefined && state !== expectedState) {
995
+ res.writeHead(400, { "Content-Type": "text/html" });
996
+ res.end(`
997
+ <!DOCTYPE html>
998
+ <html>
999
+ <head><title>State Mismatch</title></head>
1000
+ <body>
1001
+ <h1>Authentication Failed</h1>
1002
+ <p>The callback did not match the login in progress.</p>
1003
+ <p>You can close this window.</p>
1004
+ </body>
1005
+ </html>
1006
+ `);
1007
+ logger.warn("OAuth callback ignored: state mismatch");
1008
+ return;
1009
+ }
989
1010
  // Success response
990
1011
  res.writeHead(200, { "Content-Type": "text/html" });
991
1012
  res.end(`
@@ -1010,8 +1031,10 @@ export function startCallbackServer(port = DEFAULT_CALLBACK_PORT, path = "/callb
1010
1031
  cleanup();
1011
1032
  reject(new OAuthCallbackServerError(`Failed to start callback server: ${error.message}`));
1012
1033
  });
1013
- server.listen(port, () => {
1014
- logger.info(`OAuth callback server listening on port ${port}`);
1034
+ // Loopback only: the redirect comes from the operator's own browser, and an
1035
+ // all-interfaces bind on a fixed port let anything on the network reach it.
1036
+ server.listen(port, "127.0.0.1", () => {
1037
+ logger.info(`OAuth callback server listening on 127.0.0.1:${port}`);
1015
1038
  });
1016
1039
  });
1017
1040
  }
@@ -1184,8 +1207,8 @@ export async function performOAuthFlow(oauth, options = {}) {
1184
1207
  const pkce = await AnthropicOAuth.generatePKCE();
1185
1208
  // Generate state for CSRF protection
1186
1209
  const state = randomBytes(32).toString("base64url");
1187
- // Start callback server
1188
- const callbackPromise = startCallbackServer(port, "/callback", timeout);
1210
+ // Start callback server; it resolves only for a redirect carrying our state.
1211
+ const callbackPromise = startCallbackServer(port, "/callback", timeout, state);
1189
1212
  // Generate auth URL
1190
1213
  const authUrl = oauth.generateAuthUrl({
1191
1214
  codeChallenge: pkce.codeChallenge,
@@ -1219,3 +1242,56 @@ export async function performOAuthFlow(oauth, options = {}) {
1219
1242
  const tokens = await oauth.exchangeCodeForTokens(callbackResult.code, pkce.codeVerifier);
1220
1243
  return tokens;
1221
1244
  }
1245
+ /**
1246
+ * Split a pasted `code#state` string and bind it to the login in progress.
1247
+ *
1248
+ * The CLI paste flows used to set `state` to the PKCE verifier and then trust
1249
+ * whatever the user pasted after `#` as that verifier, so the code was bound to
1250
+ * nothing the CLI held: a phished `code#state` from someone else's login landed
1251
+ * their account in the victim's pool. The state is now an unrelated random
1252
+ * value, it must come back exactly, and the exchange always uses the verifier
1253
+ * this process generated.
1254
+ */
1255
+ export function parsePastedAuthorizationCode(pasted, expectedState) {
1256
+ const trimmed = pasted.trim();
1257
+ const separator = trimmed.indexOf("#");
1258
+ const code = separator === -1 ? trimmed : trimmed.slice(0, separator);
1259
+ const state = separator === -1 ? "" : trimmed.slice(separator + 1).trim();
1260
+ if (!code) {
1261
+ throw new OAuthError("Authorization code is empty. Paste the full code shown by the browser.", "INVALID_AUTHORIZATION_CODE");
1262
+ }
1263
+ if (!state) {
1264
+ throw new OAuthError("Authorization code is missing its state. Paste the full code#state value " +
1265
+ "the browser showed, from this login attempt.", "STATE_MISMATCH");
1266
+ }
1267
+ if (state.length !== expectedState.length ||
1268
+ !timingSafeStringEqual(state, expectedState)) {
1269
+ throw new OAuthError("Authorization code does not belong to this login attempt (state mismatch). " +
1270
+ "Start the login again and paste only the code it produces.", "STATE_MISMATCH");
1271
+ }
1272
+ return { code };
1273
+ }
1274
+ function timingSafeStringEqual(left, right) {
1275
+ if (left.length !== right.length) {
1276
+ return false;
1277
+ }
1278
+ let difference = 0;
1279
+ for (let index = 0; index < left.length; index += 1) {
1280
+ difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
1281
+ }
1282
+ return difference === 0;
1283
+ }
1284
+ /**
1285
+ * Validate a pasted `code#state` against the login in progress and exchange it
1286
+ * with the local verifier. Nothing is sent when the state does not match.
1287
+ */
1288
+ export async function exchangePastedSubscriptionCode(params) {
1289
+ const { code } = parsePastedAuthorizationCode(params.pasted, params.expectedState);
1290
+ return exchangeSubscriptionCode({
1291
+ code,
1292
+ state: params.expectedState,
1293
+ codeVerifier: params.codeVerifier,
1294
+ ...(params.clientId ? { clientId: params.clientId } : {}),
1295
+ ...(params.redirectUri ? { redirectUri: params.redirectUri } : {}),
1296
+ });
1297
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Private file and directory creation for credential material.
3
+ *
4
+ * Every credential file under `~/.neurolink` used to be created through a
5
+ * different path with a different idea of permissions: a temp file at umask
6
+ * mode then chmod'd, a directory whose mode was asserted only on first
7
+ * creation, a truncate-in-place write that followed symlinks. These helpers
8
+ * are the one place that gets it right, and the token store, the CLI
9
+ * credentials file and the update-state file all go through them.
10
+ *
11
+ * @module auth/secureFiles
12
+ */
13
+ export declare const PRIVATE_FILE_MODE = 384;
14
+ export declare const PRIVATE_DIR_MODE = 448;
15
+ /** Everything a CLI entry point creates is owner-only unless it says otherwise. */
16
+ export declare const PRIVATE_UMASK = 63;
17
+ /**
18
+ * Tighten the process umask so no file this process creates is group- or
19
+ * world-readable, whatever the shell's default was. Idempotent; returns the
20
+ * mask that was in force.
21
+ */
22
+ export declare function tightenProcessUmask(): number;
23
+ /** Create `dir` owner-only, and re-assert the mode when it already existed. */
24
+ export declare function ensurePrivateDirectory(dir: string): Promise<void>;
25
+ /** Synchronous twin of {@link ensurePrivateDirectory}, for CLI start-up paths. */
26
+ export declare function ensurePrivateDirectorySync(dir: string): void;
27
+ /**
28
+ * Write `content` to `targetPath` atomically and owner-only.
29
+ *
30
+ * The temp file is created with `wx` at 0600 — never at umask mode and then
31
+ * chmod'd, which left a window in which another uid could open it — under an
32
+ * unpredictable name, and renamed into place. A rename replaces a symlink at
33
+ * the target rather than following it.
34
+ */
35
+ export declare function writePrivateFileAtomically(targetPath: string, content: string): Promise<void>;
36
+ /** Synchronous twin of {@link writePrivateFileAtomically}. */
37
+ export declare function writePrivateFileAtomicallySync(targetPath: string, content: string): void;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Private file and directory creation for credential material.
3
+ *
4
+ * Every credential file under `~/.neurolink` used to be created through a
5
+ * different path with a different idea of permissions: a temp file at umask
6
+ * mode then chmod'd, a directory whose mode was asserted only on first
7
+ * creation, a truncate-in-place write that followed symlinks. These helpers
8
+ * are the one place that gets it right, and the token store, the CLI
9
+ * credentials file and the update-state file all go through them.
10
+ *
11
+ * @module auth/secureFiles
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
15
+ import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
16
+ import { dirname } from "node:path";
17
+ export const PRIVATE_FILE_MODE = 0o600;
18
+ export const PRIVATE_DIR_MODE = 0o700;
19
+ /** Everything a CLI entry point creates is owner-only unless it says otherwise. */
20
+ export const PRIVATE_UMASK = 0o077;
21
+ /**
22
+ * Tighten the process umask so no file this process creates is group- or
23
+ * world-readable, whatever the shell's default was. Idempotent; returns the
24
+ * mask that was in force.
25
+ */
26
+ export function tightenProcessUmask() {
27
+ if (process.platform === "win32") {
28
+ return 0;
29
+ }
30
+ return process.umask(PRIVATE_UMASK);
31
+ }
32
+ /** Create `dir` owner-only, and re-assert the mode when it already existed. */
33
+ export async function ensurePrivateDirectory(dir) {
34
+ await mkdir(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
35
+ await chmod(dir, PRIVATE_DIR_MODE).catch(() => undefined);
36
+ }
37
+ /** Synchronous twin of {@link ensurePrivateDirectory}, for CLI start-up paths. */
38
+ export function ensurePrivateDirectorySync(dir) {
39
+ mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
40
+ try {
41
+ chmodSync(dir, PRIVATE_DIR_MODE);
42
+ }
43
+ catch {
44
+ // Not owned by us; the mkdir above is the best we can do.
45
+ }
46
+ }
47
+ function tempPathFor(targetPath) {
48
+ return `${targetPath}.tmp.${process.pid}.${randomUUID()}`;
49
+ }
50
+ /**
51
+ * Write `content` to `targetPath` atomically and owner-only.
52
+ *
53
+ * The temp file is created with `wx` at 0600 — never at umask mode and then
54
+ * chmod'd, which left a window in which another uid could open it — under an
55
+ * unpredictable name, and renamed into place. A rename replaces a symlink at
56
+ * the target rather than following it.
57
+ */
58
+ export async function writePrivateFileAtomically(targetPath, content) {
59
+ await ensurePrivateDirectory(dirname(targetPath));
60
+ const tempPath = tempPathFor(targetPath);
61
+ try {
62
+ await writeFile(tempPath, content, {
63
+ encoding: "utf8",
64
+ mode: PRIVATE_FILE_MODE,
65
+ flag: "wx",
66
+ });
67
+ await chmod(tempPath, PRIVATE_FILE_MODE).catch(() => undefined);
68
+ await rename(tempPath, targetPath);
69
+ }
70
+ finally {
71
+ await rm(tempPath, { force: true }).catch(() => undefined);
72
+ }
73
+ }
74
+ /** Synchronous twin of {@link writePrivateFileAtomically}. */
75
+ export function writePrivateFileAtomicallySync(targetPath, content) {
76
+ ensurePrivateDirectorySync(dirname(targetPath));
77
+ const tempPath = tempPathFor(targetPath);
78
+ try {
79
+ writeFileSync(tempPath, content, {
80
+ encoding: "utf8",
81
+ mode: PRIVATE_FILE_MODE,
82
+ flag: "wx",
83
+ });
84
+ try {
85
+ chmodSync(tempPath, PRIVATE_FILE_MODE);
86
+ }
87
+ catch {
88
+ // The create mode already bounded it.
89
+ }
90
+ renameSync(tempPath, targetPath);
91
+ }
92
+ finally {
93
+ rmSync(tempPath, { force: true });
94
+ }
95
+ }
@@ -50,12 +50,12 @@ export declare class TokenStore {
50
50
  private static readonly NEUROLINK_DIR;
51
51
  private static readonly TOKEN_FILE;
52
52
  private static readonly FILE_PERMISSIONS;
53
- private static readonly DIR_PERMISSIONS;
54
53
  /** Default expiration buffer: 1 hour (proactive refresh before actual expiry) */
55
54
  private static readonly DEFAULT_EXPIRY_BUFFER_MS;
56
55
  private readonly storagePath;
57
56
  private readonly encryptionEnabled;
58
57
  private readonly encryptionKey;
58
+ private readonly encryptionKeyBytes;
59
59
  private readonly tokenRefreshers;
60
60
  private readonly inFlightRefreshes;
61
61
  private readonly _mutex;
@@ -246,10 +246,44 @@ export declare class TokenStore {
246
246
  * Loads the storage data from file
247
247
  */
248
248
  private loadStorageData;
249
+ /** Parse plain-JSON storage; null when it is not a usable store. */
250
+ private parseStorageJson;
251
+ /**
252
+ * Decode a store written by either codec.
253
+ *
254
+ * The current codec is prefixed and XORs UTF-8 bytes. The older one XORed
255
+ * UTF-16 code units and then truncated each to a byte, so a store holding a
256
+ * single non-Latin-1 character (an emoji in a label, a smart quote in a
257
+ * disable reason) could never be read back. Older files carry no prefix and
258
+ * are read the old way; the next save rewrites them with the new codec.
259
+ */
260
+ private parseObfuscatedStorage;
261
+ private quarantineStorageFile;
249
262
  /**
250
263
  * Saves storage data to file
251
264
  */
252
265
  private saveStorageData;
266
+ /**
267
+ * Serialize and write the store: owner-only from creation, under an
268
+ * unpredictable temp name, renamed into place. See `secureFiles`.
269
+ */
270
+ private writeStorageFile;
271
+ /**
272
+ * Take the in-process mutex, then the cross-process file lock, around a
273
+ * read-modify-write. The CLI and a running proxy share one file; without
274
+ * the second lock a stale snapshot written late reverted the other's
275
+ * rotated refresh token or disable flag.
276
+ */
277
+ private runExclusiveLocked;
278
+ private get lockPath();
279
+ private readLockOwner;
280
+ private removeAbandonedLock;
281
+ /**
282
+ * Best-effort cross-process lock. When another process holds it past the
283
+ * timeout the write proceeds anyway with a warning: a stuck lock must not
284
+ * take the pool offline, which is worse than the race it guards against.
285
+ */
286
+ private acquireFileLock;
253
287
  /**
254
288
  * Deletes the storage file
255
289
  */
@@ -268,16 +302,19 @@ export declare class TokenStore {
268
302
  * using proper encryption with a user-provided key or system keychain
269
303
  */
270
304
  private deriveEncryptionKey;
305
+ private deriveEncryptionKeyBytes;
271
306
  /**
272
- * Simple XOR-based obfuscation
273
- * Note: This is NOT cryptographically secure, just basic obfuscation
274
- * For production use, consider using node:crypto with proper encryption
307
+ * XOR obfuscation over the UTF-8 bytes of the document, base64-encoded and
308
+ * prefixed so a reader can tell it from the older code-unit codec.
309
+ * Note: this is NOT cryptographically secure, just basic obfuscation.
275
310
  */
276
311
  private obfuscate;
312
+ private deobfuscateV2;
277
313
  /**
278
- * Reverses the XOR obfuscation
314
+ * The pre-UTF-8 codec, kept only to read stores written before it existed.
315
+ * It cannot round-trip any character above U+00FF.
279
316
  */
280
- private deobfuscate;
317
+ private deobfuscateLegacy;
281
318
  }
282
319
  /**
283
320
  * Default token store singleton instance