@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.
- package/CHANGELOG.md +2 -2
- package/README.md +1 -1
- package/dist/action/actionExecutor.d.ts +33 -3
- package/dist/action/actionExecutor.js +71 -15
- package/dist/action/actionInputs.js +4 -1
- package/dist/action/cliVersion.d.ts +25 -0
- package/dist/action/cliVersion.js +26 -0
- package/dist/action/index.d.ts +2 -1
- package/dist/action/index.js +2 -1
- package/dist/adapters/video/directorPipeline.js +46 -36
- package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
- package/dist/adapters/video/ffmpegAdapter.js +32 -5
- package/dist/adapters/video/frameExtractor.js +1 -1
- package/dist/adapters/video/videoMerger.js +5 -2
- package/dist/agent/arithmeticEvaluator.d.ts +32 -0
- package/dist/agent/arithmeticEvaluator.js +349 -0
- package/dist/agent/directTools.d.ts +6 -18
- package/dist/agent/directTools.js +17 -56
- package/dist/artifacts/artifactStore.d.ts +22 -1
- package/dist/artifacts/artifactStore.js +66 -6
- package/dist/auth/anthropicOAuth.d.ts +26 -1
- package/dist/auth/anthropicOAuth.js +81 -5
- package/dist/auth/secureFiles.d.ts +37 -0
- package/dist/auth/secureFiles.js +95 -0
- package/dist/auth/tokenStore.d.ts +43 -6
- package/dist/auth/tokenStore.js +260 -79
- package/dist/avatar/providers/DIDAvatar.js +49 -45
- package/dist/avatar/providers/ReplicateAvatar.js +33 -40
- package/dist/browser/neurolink.min.js +485 -558
- package/dist/cli/commands/auth.js +112 -70
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +128 -13
- package/dist/cli/commands/serve.d.ts +10 -1
- package/dist/cli/commands/serve.js +98 -17
- package/dist/cli/commands/server.js +17 -5
- package/dist/cli/index.js +146 -5
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/envManager.js +17 -3
- package/dist/cli/utils/serverUtils.d.ts +39 -3
- package/dist/cli/utils/serverUtils.js +63 -6
- package/dist/core/baseProvider.js +7 -1
- package/dist/core/modules/MessageBuilder.js +5 -0
- package/dist/core/modules/ToolsManager.d.ts +46 -1
- package/dist/core/modules/ToolsManager.js +190 -43
- package/dist/files/fileReferenceRegistry.d.ts +2 -23
- package/dist/files/fileReferenceRegistry.js +55 -5
- package/dist/hitl/hitlManager.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/mcp/auth/tokenStorage.d.ts +7 -0
- package/dist/mcp/auth/tokenStorage.js +23 -4
- package/dist/mcp/externalServerManager.d.ts +13 -3
- package/dist/mcp/externalServerManager.js +154 -136
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/mcpClientFactory.d.ts +14 -0
- package/dist/mcp/mcpClientFactory.js +70 -7
- package/dist/mcp/projectConfigTrust.d.ts +72 -0
- package/dist/mcp/projectConfigTrust.js +237 -0
- package/dist/mcp/transportUrlPolicy.d.ts +39 -0
- package/dist/mcp/transportUrlPolicy.js +172 -0
- package/dist/music/providers/BeatovenMusic.js +35 -46
- package/dist/music/providers/ReplicateMusic.js +33 -40
- package/dist/neurolink.d.ts +16 -1
- package/dist/neurolink.js +64 -11
- package/dist/processors/archive/ArchiveProcessor.js +15 -1
- package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
- package/dist/processors/archive/zipEntryIndex.js +33 -0
- package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
- package/dist/processors/base/BaseFileProcessor.js +40 -111
- package/dist/processors/data/YamlProcessor.d.ts +13 -1
- package/dist/processors/data/YamlProcessor.js +93 -2
- package/dist/processors/document/OpenDocumentProcessor.js +7 -2
- package/dist/processors/document/PptxProcessor.js +12 -7
- package/dist/processors/media/VideoProcessor.js +93 -15
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +13 -1
- package/dist/proxy/updateState.js +4 -4
- package/dist/rag/document/loaders.d.ts +0 -6
- package/dist/rag/document/loaders.js +16 -10
- package/dist/rag/ragIntegration.d.ts +2 -2
- package/dist/rag/ragIntegration.js +35 -10
- package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
- package/dist/rag/retrieval/vectorQueryTool.js +54 -17
- package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
- package/dist/server/abstract/baseServerAdapter.js +119 -6
- package/dist/server/adapters/expressAdapter.d.ts +3 -0
- package/dist/server/adapters/expressAdapter.js +83 -8
- package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
- package/dist/server/adapters/fastifyAdapter.js +165 -70
- package/dist/server/adapters/honoAdapter.d.ts +9 -1
- package/dist/server/adapters/honoAdapter.js +99 -21
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +153 -74
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/middleware/auth.js +6 -2
- package/dist/server/middleware/cache.js +6 -1
- package/dist/server/middleware/rateLimit.js +29 -4
- package/dist/server/routes/agentRoutes.js +24 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +273 -84
- package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
- package/dist/server/routes/codexProxyRoutes.js +125 -40
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +35 -4
- package/dist/server/routes/mcpRoutes.d.ts +6 -2
- package/dist/server/routes/mcpRoutes.js +21 -7
- package/dist/server/routes/memoryRoutes.js +7 -6
- package/dist/server/routes/toolRoutes.d.ts +12 -2
- package/dist/server/routes/toolRoutes.js +33 -3
- package/dist/server/routes/transcribeRoutes.js +60 -13
- package/dist/server/utils/clientAddress.d.ts +23 -0
- package/dist/server/utils/clientAddress.js +65 -0
- package/dist/server/utils/clientError.d.ts +35 -0
- package/dist/server/utils/clientError.js +129 -0
- package/dist/server/utils/expressBody.d.ts +33 -0
- package/dist/server/utils/expressBody.js +353 -0
- package/dist/server/utils/pathMatch.d.ts +21 -0
- package/dist/server/utils/pathMatch.js +41 -0
- package/dist/server/utils/validation.d.ts +9 -0
- package/dist/server/utils/validation.js +9 -0
- package/dist/server/websocket/WebSocketHandler.js +22 -3
- package/dist/server/websocket/frameQueue.d.ts +19 -0
- package/dist/server/websocket/frameQueue.js +41 -3
- package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
- package/dist/server/websocket/transcribeWebSocket.js +223 -34
- package/dist/skills/skillStores.js +5 -0
- package/dist/tasks/store/fileTaskStore.d.ts +2 -0
- package/dist/tasks/store/fileTaskStore.js +11 -4
- package/dist/types/attachments.d.ts +117 -0
- package/dist/types/attachments.js +15 -0
- package/dist/types/auth.d.ts +6 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/file.d.ts +8 -0
- package/dist/types/hitl.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/mcp.d.ts +98 -0
- package/dist/types/multimodal.js +0 -41
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +62 -3
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +255 -10
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/transcribe.d.ts +6 -0
- package/dist/utils/attachmentPolicy.d.ts +89 -0
- package/dist/utils/attachmentPolicy.js +327 -0
- package/dist/utils/dotenvBootstrap.d.ts +48 -19
- package/dist/utils/dotenvBootstrap.js +172 -31
- package/dist/utils/fileDetector.js +115 -65
- package/dist/utils/json/coerce.js +11 -1
- package/dist/utils/logSanitize.d.ts +5 -0
- package/dist/utils/logSanitize.js +8 -0
- package/dist/utils/logger.js +73 -1
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +118 -105
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +2 -0
- package/dist/utils/redis.d.ts +12 -0
- package/dist/utils/redis.js +72 -0
- package/dist/utils/safeFetch.d.ts +22 -1
- package/dist/utils/safeFetch.js +132 -38
- package/dist/utils/safeStoreId.d.ts +19 -0
- package/dist/utils/safeStoreId.js +29 -0
- package/dist/utils/schemaConversion.d.ts +30 -3
- package/dist/utils/schemaConversion.js +457 -84
- package/dist/utils/sizeGuard.js +2 -2
- package/dist/utils/ssrfGuard.d.ts +3 -3
- package/dist/utils/ssrfGuard.js +71 -39
- package/dist/voice/eval/sttEvalStore.d.ts +26 -3
- package/dist/voice/eval/sttEvalStore.js +77 -12
- package/dist/voice/livekit/agentDefinition.d.ts +18 -0
- package/dist/voice/livekit/agentDefinition.js +22 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
- package/dist/voice/livekit/realtimeMcpTools.js +8 -7
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
- package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
- package/dist/voice/livekit/voiceAgent.d.ts +2 -2
- package/dist/voice/livekit/voiceAgent.js +2 -1
- package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
- package/dist/voice/livekit/xaiSessionWatch.js +30 -0
- package/dist/voice/providers/GeminiLive.js +21 -6
- package/dist/voice/providers/GoogleSTT.js +8 -5
- package/dist/voice/transcribe.d.ts +2 -2
- package/dist/voice/transcribe.js +34 -18
- package/dist/voice/whistle/audio.js +11 -5
- package/docs-site/static/search-index.json +75 -52
- package/package.json +171 -156
- 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
|
|
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
|
-
|
|
1014
|
-
|
|
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
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
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
|
-
*
|
|
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
|
|
317
|
+
private deobfuscateLegacy;
|
|
281
318
|
}
|
|
282
319
|
/**
|
|
283
320
|
* Default token store singleton instance
|