@juspay/neurolink 12.51.0 → 12.51.2

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.
@@ -290,6 +290,19 @@ function validateApiKey(input) {
290
290
  }
291
291
  return true;
292
292
  }
293
+ const AZURE_OPENAI_HOST_SUFFIX = ".openai.azure.com";
294
+ /**
295
+ * True only when the parsed hostname is a subdomain of openai.azure.com.
296
+ * A substring test also accepts `openai.azure.com.evil.example` and
297
+ * `evilopenai.azure.com`, so the leading dot of the suffix is what pins the
298
+ * label boundary. The URL class has already lower-cased the host; a single
299
+ * trailing dot (an absolute DNS name) is the same host and is ignored.
300
+ */
301
+ function isAzureOpenAIHostname(hostname) {
302
+ const host = hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
303
+ return (host.length > AZURE_OPENAI_HOST_SUFFIX.length &&
304
+ host.endsWith(AZURE_OPENAI_HOST_SUFFIX));
305
+ }
293
306
  /**
294
307
  * Validate Azure OpenAI endpoint URL
295
308
  */
@@ -300,7 +313,7 @@ function validateEndpoint(input) {
300
313
  const trimmed = input.trim();
301
314
  try {
302
315
  const url = new URL(trimmed);
303
- if (!url.hostname.includes("openai.azure.com")) {
316
+ if (!isAzureOpenAIHostname(url.hostname)) {
304
317
  return "Endpoint should be an Azure OpenAI URL (*.openai.azure.com)";
305
318
  }
306
319
  return true;
@@ -469,6 +469,26 @@ function validateServiceAccountJSON(input) {
469
469
  return "Invalid JSON format";
470
470
  }
471
471
  }
472
+ /**
473
+ * Quote a value for a double-quoted .env entry.
474
+ *
475
+ * Line breaks are folded first, whichever way they were typed (a real CR/LF,
476
+ * or the two characters `\n` a key copied out of a service-account JSON file
477
+ * carries), and written back as the `\n` escape the .env loaders expand. That
478
+ * has to come before the backslashes are escaped, or a pasted `\n` would be
479
+ * doubled into `\\n` and the key would stop being a key. Every other backslash
480
+ * is then escaped before the quote is: an unescaped one in front of a `"`
481
+ * cancels the quote's own escape, closes the value early and lets the rest of
482
+ * the input run as part of the .env line.
483
+ */
484
+ function quoteEnvValue(value) {
485
+ const escaped = value
486
+ .replace(/\\r\\n|\\n|\\r|\r\n|\r/g, "\n")
487
+ .replace(/\\/g, "\\\\")
488
+ .replace(/"/g, '\\"')
489
+ .replace(/\n/g, "\\n");
490
+ return `"${escaped}"`;
491
+ }
472
492
  /**
473
493
  * Update .env file with selected authentication method
474
494
  */
@@ -524,10 +544,7 @@ async function updateEnvFile(method, config) {
524
544
  existingVars.set("GOOGLE_AUTH_CLIENT_EMAIL", config.clientEmail);
525
545
  }
526
546
  if (config.privateKey) {
527
- const escaped = config.privateKey
528
- .replace(/\r?\n/g, "\\n")
529
- .replace(/"/g, '\\"');
530
- existingVars.set("GOOGLE_AUTH_PRIVATE_KEY", `"${escaped}"`);
547
+ existingVars.set("GOOGLE_AUTH_PRIVATE_KEY", quoteEnvValue(config.privateKey));
531
548
  }
532
549
  break;
533
550
  }
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import chalk from "chalk";
2
3
  import ora from "ora";
3
4
  import inquirer from "inquirer";
@@ -134,7 +135,7 @@ export class SageMakerCommandFactory {
134
135
  * Create secure configuration without exposing credentials to process.env
135
136
  */
136
137
  static createSecureConfiguration(config) {
137
- const sessionId = `sagemaker_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
138
+ const sessionId = `sagemaker_${Date.now()}_${randomBytes(16).toString("hex")}`;
138
139
  const secureConfig = {
139
140
  ...config,
140
141
  sessionId,
@@ -143,9 +144,12 @@ export class SageMakerCommandFactory {
143
144
  // Store temporarily in secure memory store
144
145
  this.secureCredentialStore.set(sessionId, secureConfig);
145
146
  // Auto-cleanup after 5 minutes for security
146
- setTimeout(() => {
147
+ const expiry = setTimeout(() => {
147
148
  this.secureCredentialStore.delete(sessionId);
148
149
  }, 5 * 60 * 1000);
150
+ // Setup also deletes the entry itself when it finishes; the timer is only
151
+ // the backstop and must not hold the CLI open for the rest of its window.
152
+ expiry.unref();
149
153
  return secureConfig;
150
154
  }
151
155
  /**
@@ -505,7 +509,7 @@ export class SageMakerCommandFactory {
505
509
  "šŸ“– Learn more: https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html\n"));
506
510
  spinner.start("Setting up SageMaker configuration...");
507
511
  // Secure credential management without process.env exposure
508
- const secureConfig = this.createSecureConfiguration({
512
+ const secureConfig = SageMakerCommandFactory.createSecureConfiguration({
509
513
  accessKeyId: answers.accessKeyId,
510
514
  secretAccessKey: answers.secretAccessKey,
511
515
  region: answers.region,
@@ -516,7 +520,7 @@ export class SageMakerCommandFactory {
516
520
  // Clear cache and test configuration with secure config
517
521
  clearConfigurationCache();
518
522
  try {
519
- await this.validateSecureConfiguration(secureConfig); // Validate configuration is loadable
523
+ await SageMakerCommandFactory.validateSecureConfiguration(secureConfig); // Validate configuration is loadable
520
524
  spinner.succeed("āœ… Configuration validated successfully");
521
525
  logger.always(chalk.green("\nšŸŽ‰ SageMaker setup complete!"));
522
526
  logger.always(chalk.yellow("\nšŸ’” Next steps:"));
@@ -544,13 +548,13 @@ export class SageMakerCommandFactory {
544
548
  " • Consider using AWS credential rotation policies\n" +
545
549
  " • Monitor AWS CloudTrail for unauthorized access attempts"));
546
550
  // Clear secure credentials from memory after successful setup
547
- this.clearSecureCredentials(secureConfig.sessionId);
551
+ SageMakerCommandFactory.clearSecureCredentials(secureConfig.sessionId);
548
552
  }
549
553
  catch (configError) {
550
554
  spinner.fail("āŒ Configuration validation failed");
551
555
  logger.error(chalk.red(`Error: ${configError instanceof Error ? configError.message : String(configError)}`));
552
556
  // Clear secure credentials from memory on error
553
- this.clearSecureCredentials(secureConfig.sessionId);
557
+ SageMakerCommandFactory.clearSecureCredentials(secureConfig.sessionId);
554
558
  process.exit(1);
555
559
  }
556
560
  }
@@ -8,6 +8,12 @@
8
8
  * @author NeuroLink Team
9
9
  * @version 1.0.0
10
10
  */
11
+ /**
12
+ * Largest delay `setTimeout` honours (2^31 - 1 ms, about 24.8 days). A longer
13
+ * delay is not "later": Node warns and fires after 1 ms, so a caller-supplied
14
+ * deadline above this must be held at it rather than handed to the timer.
15
+ */
16
+ export declare const MAX_TIMER_MS = 2147483647;
11
17
  /**
12
18
  * Tool execution timeout constants
13
19
  * These values balance reliability vs performance for AI tool operations
@@ -8,6 +8,12 @@
8
8
  * @author NeuroLink Team
9
9
  * @version 1.0.0
10
10
  */
11
+ /**
12
+ * Largest delay `setTimeout` honours (2^31 - 1 ms, about 24.8 days). A longer
13
+ * delay is not "later": Node warns and fires after 1 ms, so a caller-supplied
14
+ * deadline above this must be held at it rather than handed to the timer.
15
+ */
16
+ export const MAX_TIMER_MS = 2_147_483_647;
11
17
  /**
12
18
  * Tool execution timeout constants
13
19
  * These values balance reliability vs performance for AI tool operations
@@ -361,7 +361,17 @@ export declare class ErrorFactory {
361
361
  static csvProcessingFailed(filename: string, originalError: Error): NeuroLinkError;
362
362
  }
363
363
  /**
364
- * Timeout wrapper for async operations
364
+ * Timeout wrapper for async operations.
365
+ *
366
+ * Races an already-created promise against a deadline. It cannot cancel that
367
+ * promise: when the deadline wins the operation keeps running as an abandoned
368
+ * ghost. A caller that owns the abortable operation (an HTTP request, a
369
+ * provider call) must hand it an `AbortSignal` itself and abort it when this
370
+ * rejects.
371
+ *
372
+ * `timeoutMs` is held at `MAX_TIMER_MS`: a longer deadline would otherwise be
373
+ * handed to `setTimeout`, which fires after 1 ms and so rejected an operation
374
+ * that had asked for the longest wait available.
365
375
  */
366
376
  export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutError?: Error): Promise<T>;
367
377
  /**
@@ -3,6 +3,7 @@
3
3
  * Provides structured error management for tool execution and system operations
4
4
  */
5
5
  import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
6
+ import { MAX_TIMER_MS } from "../constants/timeouts.js";
6
7
  import { logger } from "./logger.js";
7
8
  import { CircuitBreakerOpenError } from "../types/index.js";
8
9
  import { HITLTimeoutError } from "../hitl/hitlErrors.js";
@@ -1081,14 +1082,28 @@ export class ErrorFactory {
1081
1082
  }
1082
1083
  }
1083
1084
  /**
1084
- * Timeout wrapper for async operations
1085
+ * Timeout wrapper for async operations.
1086
+ *
1087
+ * Races an already-created promise against a deadline. It cannot cancel that
1088
+ * promise: when the deadline wins the operation keeps running as an abandoned
1089
+ * ghost. A caller that owns the abortable operation (an HTTP request, a
1090
+ * provider call) must hand it an `AbortSignal` itself and abort it when this
1091
+ * rejects.
1092
+ *
1093
+ * `timeoutMs` is held at `MAX_TIMER_MS`: a longer deadline would otherwise be
1094
+ * handed to `setTimeout`, which fires after 1 ms and so rejected an operation
1095
+ * that had asked for the longest wait available.
1085
1096
  */
1086
1097
  export async function withTimeout(promise, timeoutMs, timeoutError) {
1087
1098
  let timer;
1099
+ // An explicit upper-bound comparison, and `>` rather than `<=`: NaN fails
1100
+ // `>`, so NaN, zero and negative deadlines reach `setTimeout` as they always
1101
+ // did, and only a value above the limit is replaced by it.
1102
+ const delayMs = timeoutMs > MAX_TIMER_MS ? MAX_TIMER_MS : timeoutMs;
1088
1103
  const timeoutPromise = new Promise((_, reject) => {
1089
1104
  timer = setTimeout(() => {
1090
1105
  reject(timeoutError || new Error(`Operation timed out after ${timeoutMs}ms`));
1091
- }, timeoutMs);
1106
+ }, delayMs);
1092
1107
  });
1093
1108
  try {
1094
1109
  return await Promise.race([promise, timeoutPromise]);
@@ -3,6 +3,7 @@
3
3
  * Parallel execution engine for ensemble workflows
4
4
  */
5
5
  import pLimit from "p-limit";
6
+ import { MAX_TIMER_MS } from "../../constants/timeouts.js";
6
7
  import { AIProviderFactory } from "../../core/factory.js";
7
8
  import { logger } from "../../utils/logger.js";
8
9
  import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../../observability/index.js";
@@ -130,12 +131,13 @@ async function executeModel(options, workflowDefaultSystemPrompt) {
130
131
  const MAX_ATTEMPTS = 2;
131
132
  while (true) {
132
133
  attempts++;
133
- result = await executeWithTimeout(async () => {
134
+ result = await executeWithTimeout(async (abortSignal) => {
134
135
  return await provider.generate({
135
136
  prompt,
136
137
  systemPrompt: resolvedSystemPrompt,
137
138
  temperature: model.temperature,
138
139
  maxTokens: model.maxTokens,
140
+ abortSignal,
139
141
  });
140
142
  }, timeout, `Model ${model.provider}/${model.model} timed out after ${timeout}ms`);
141
143
  const contentLen = result?.content?.length ?? 0;
@@ -199,14 +201,29 @@ async function createProvider(model) {
199
201
  return await AIProviderFactory.createProvider(model.provider, model.model);
200
202
  }
201
203
  /**
202
- * Execute function with timeout
203
- * @param fn - Async function to execute
204
+ * Run an abortable operation under a deadline.
205
+ *
206
+ * The operation receives the signal of an `AbortController` that this function
207
+ * aborts the moment the deadline lapses, so the owner of the work (the provider
208
+ * call and its HTTP request) is cancelled rather than abandoned. `Promise.race`
209
+ * alone only stops this caller waiting; it cannot stop a promise that already
210
+ * exists, so cancelling is the operation's job once it is handed the signal.
211
+ *
212
+ * `timeoutMs` is held at `MAX_TIMER_MS`: `setTimeout` fires after 1 ms for a
213
+ * longer delay, which turned the longest deadline a workflow could ask for into
214
+ * an immediate failure.
215
+ * @param run - Operation to execute; observes the signal to stop early
204
216
  * @param timeoutMs - Timeout in milliseconds
205
217
  * @param timeoutMessage - Error message for timeout
206
- * @returns Result of function execution
218
+ * @returns Result of the operation
207
219
  */
208
- async function executeWithTimeout(fn, timeoutMs, timeoutMessage) {
220
+ async function executeWithTimeout(run, timeoutMs, timeoutMessage) {
221
+ const controller = new AbortController();
209
222
  let timeoutHandle;
223
+ // An explicit upper-bound comparison, and `>` rather than `<=`: NaN fails
224
+ // `>`, so NaN, zero and negative deadlines reach `setTimeout` as they always
225
+ // did, and only a value above the limit is replaced by it.
226
+ const delayMs = timeoutMs > MAX_TIMER_MS ? MAX_TIMER_MS : timeoutMs;
210
227
  const timeoutPromise = new Promise((_, reject) => {
211
228
  timeoutHandle = setTimeout(() => {
212
229
  const error = new WorkflowError(timeoutMessage, {
@@ -215,21 +232,17 @@ async function executeWithTimeout(fn, timeoutMs, timeoutMessage) {
215
232
  phase: "ensemble",
216
233
  retryable: true,
217
234
  });
235
+ // Reject before aborting: the abort can make the operation's own
236
+ // promise fail, and the race must already hold the deadline error.
218
237
  reject(error);
219
- }, timeoutMs);
238
+ controller.abort(error);
239
+ }, delayMs);
220
240
  });
221
241
  try {
222
- const result = await Promise.race([fn(), timeoutPromise]);
223
- if (timeoutHandle) {
224
- clearTimeout(timeoutHandle);
225
- }
226
- return result;
242
+ return await Promise.race([run(controller.signal), timeoutPromise]);
227
243
  }
228
- catch (error) {
229
- if (timeoutHandle) {
230
- clearTimeout(timeoutHandle);
231
- }
232
- throw error;
244
+ finally {
245
+ clearTimeout(timeoutHandle);
233
246
  }
234
247
  }
235
248
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.51.0",
3
+ "version": "12.51.2",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI Ā· Anthropic Ā· Gemini Ā· Bedrock Ā· Azure Ā· Ollama Ā· TypeSafe Jev and more.",
6
6
  "author": {
@@ -199,6 +199,7 @@
199
199
  "test:bugfixes": "pnpm exec tsx test/continuous-test-suite-bugfixes.ts",
200
200
  "test:json-e2e": "pnpm exec tsx test/continuous-test-suite-json-e2e.ts",
201
201
  "test:workflow": "pnpm exec tsx test/continuous-test-suite-workflow.ts",
202
+ "test:workflow-deadlines": "pnpm exec tsx test/continuous-test-suite-workflow-deadlines.ts",
202
203
  "test:hitl": "pnpm exec tsx test/continuous-test-suite-hitl.ts",
203
204
  "test:hitl:timeout-exclusions": "pnpm exec tsx test/continuous-test-suite-hitl-timeout-exclusions.ts",
204
205
  "test:tasks": "pnpm exec tsx test/continuous-test-suite-tasks.ts",