@juspay/neurolink 11.1.0 → 11.1.1

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 (35) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/browser/neurolink.min.js +390 -390
  3. package/dist/cli/commands/setup.d.ts +4 -1
  4. package/dist/cli/commands/setup.js +44 -14
  5. package/dist/constants/networkErrorCodes.d.ts +14 -0
  6. package/dist/constants/networkErrorCodes.js +21 -0
  7. package/dist/factories/providerDescriptors.js +13 -3
  8. package/dist/lib/constants/networkErrorCodes.d.ts +14 -0
  9. package/dist/lib/constants/networkErrorCodes.js +22 -0
  10. package/dist/lib/factories/providerDescriptors.js +13 -3
  11. package/dist/lib/processors/base/BaseFileProcessor.d.ts +17 -0
  12. package/dist/lib/processors/base/BaseFileProcessor.js +40 -0
  13. package/dist/lib/processors/document/OpenDocumentProcessor.js +12 -2
  14. package/dist/lib/proxy/proxyFetch.js +1 -9
  15. package/dist/lib/types/cli.d.ts +2 -0
  16. package/dist/lib/types/providers.d.ts +17 -2
  17. package/dist/lib/utils/errorClassifier.js +100 -11
  18. package/dist/lib/utils/providerConfig.d.ts +16 -0
  19. package/dist/lib/utils/providerConfig.js +23 -0
  20. package/dist/lib/utils/providerHealth.d.ts +30 -24
  21. package/dist/lib/utils/providerHealth.js +42 -41
  22. package/dist/lib/utils/providerUtils.js +2 -2
  23. package/dist/processors/base/BaseFileProcessor.d.ts +17 -0
  24. package/dist/processors/base/BaseFileProcessor.js +40 -0
  25. package/dist/processors/document/OpenDocumentProcessor.js +12 -2
  26. package/dist/proxy/proxyFetch.js +1 -9
  27. package/dist/types/cli.d.ts +2 -0
  28. package/dist/types/providers.d.ts +17 -2
  29. package/dist/utils/errorClassifier.js +100 -11
  30. package/dist/utils/providerConfig.d.ts +16 -0
  31. package/dist/utils/providerConfig.js +23 -0
  32. package/dist/utils/providerHealth.d.ts +30 -24
  33. package/dist/utils/providerHealth.js +42 -41
  34. package/dist/utils/providerUtils.js +2 -2
  35. package/package.json +1 -1
@@ -185,6 +185,29 @@ export function getProviderModel(envVar, defaultModel) {
185
185
  export function hasProviderCredentials(envVars) {
186
186
  return envVars.some((envVar) => !!process.env[envVar]);
187
187
  }
188
+ /**
189
+ * Evaluates a `ProviderDescriptor.envVars.extraRequiredFallbacks`-shaped
190
+ * list against an env-var source. Each entry is either a single env var
191
+ * name (satisfied on its own) or a nested array of names that must ALL be
192
+ * present together (e.g. Vertex's GOOGLE_AUTH_CLIENT_EMAIL +
193
+ * GOOGLE_AUTH_PRIVATE_KEY pair, which is only valid auth as a pair).
194
+ * Returns true when at least one entry is satisfied. The single evaluation
195
+ * site for this shape — every consumer (providerUtils.ts, providerHealth.ts,
196
+ * setup.ts, environmentManager.ts) must call this instead of re-deriving the
197
+ * same `.some()`/`.every()` logic, so they can't drift out of sync with each
198
+ * other or with the real auth gate (hasGoogleCredentials()).
199
+ * @param env Explicit env-var source (`process.env`, or a parsed .env file) —
200
+ * never hardcoded, so callers checking a file's contents (not the live
201
+ * process env) can reuse this too.
202
+ */
203
+ export function satisfiesFallbacks(fallbacks, env) {
204
+ if (!fallbacks) {
205
+ return false;
206
+ }
207
+ return fallbacks.some((entry) => typeof entry === "string"
208
+ ? !!env[entry]
209
+ : entry.every((name) => !!env[name]));
210
+ }
188
211
  // =============================================================================
189
212
  // PROVIDER-SPECIFIC CONFIGURATION CREATORS
190
213
  // =============================================================================
@@ -36,27 +36,25 @@ export declare class ProviderHealthChecker {
36
36
  */
37
37
  private static checkModelAvailability;
38
38
  /**
39
- * Providers whose credential model can't be reduced to "every one of
40
- * these exact env vars must be literally set" — the check in
41
- * checkEnvironmentConfiguration() below ANDs every entry in the
42
- * returned list together, which can't express Vertex's file-OR-
43
- * individual-creds-OR-service-account auth, Bedrock's AWS SDK default
44
- * provider chain (profile / IAM role, no env vars at all), or
45
- * LiteLLM's documented zero-config local proxy. Their real requirement
46
- * is validated by checkProviderSpecificConfig()'s dedicated per-provider
47
- * checks instead matches the original hand-written switch's
48
- * deliberate `return []` for exactly these three. Naively deriving
49
- * [apiKey, ...extraRequired] from the descriptor here (as for the other
50
- * 27 providers) would make checkEnvironmentConfiguration() push a false
51
- * "missing environment variables" issue and therefore isHealthy=false
52
- * — for legitimate fallback-based Vertex auth, AWS_PROFILE/IAM-role
53
- * Bedrock auth, and unauthenticated local LiteLLM proxies. See
54
- * hasProviderEnvVars() in providerUtils.ts for the equivalent OR-aware
55
- * check used for auto-select gating.
56
- */
57
- private static readonly ENV_CHECK_DELEGATED_TO_SPECIFIC_CONFIG;
58
- /**
59
- * Get required environment variables for a provider
39
+ * Get required environment variables for a provider.
40
+ *
41
+ * Returns `[]` for providers with `descriptor.credentialsResolvedExternally
42
+ * === true` (Vertex, Bedrock, LiteLLM) the check in
43
+ * checkEnvironmentConfiguration() below ANDs every entry in the returned
44
+ * list together, which can't express Vertex's file-OR-individual-creds-
45
+ * OR-service-account auth, Bedrock's AWS SDK default provider chain
46
+ * (profile / IAM role, no env vars at all), or LiteLLM's documented
47
+ * zero-config local proxy. Their real requirement is validated by
48
+ * checkProviderSpecificConfig()'s dedicated per-provider checks instead.
49
+ * Naively deriving [apiKey, ...extraRequired] from the descriptor for
50
+ * these three (as for the other 27 providers) would make
51
+ * checkEnvironmentConfiguration() push a false "missing environment
52
+ * variables" issue and therefore isHealthy=false for legitimate
53
+ * fallback-based Vertex auth, AWS_PROFILE/IAM-role Bedrock auth, and
54
+ * unauthenticated local LiteLLM proxies. See hasProviderEnvVars() in
55
+ * providerUtils.ts for the equivalent OR-aware check used for
56
+ * auto-select gating. See `ProviderDescriptor.credentialsResolvedExternally`
57
+ * (types/providers.ts) for the field's full documentation.
60
58
  */
61
59
  static getRequiredEnvironmentVariables(providerName: string): string[];
62
60
  /**
@@ -107,9 +105,17 @@ export declare class ProviderHealthChecker {
107
105
  */
108
106
  private static checkGoogleApplicationCredentials;
109
107
  /**
110
- * Check individual Google credentials
111
- */
112
- private static checkIndividualGoogleCredentials;
108
+ * Check Vertex's non-file auth fallbacks (GOOGLE_APPLICATION_CREDENTIALS_
109
+ * NEUROLINK, GOOGLE_SERVICE_ACCOUNT_KEY, or the GOOGLE_AUTH_CLIENT_EMAIL +
110
+ * GOOGLE_AUTH_PRIVATE_KEY pair) via the descriptor's extraRequiredFallbacks
111
+ * instead of a hand-maintained env-var list, so this stays in sync with
112
+ * the real gating logic (hasGoogleCredentials()) the same way
113
+ * checkApiKeyValidity()'s vertex branch does. The previous hand-rolled
114
+ * version only recognized GOOGLE_SERVICE_ACCOUNT_KEY or the email+key
115
+ * pair — missing GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK, the
116
+ * descriptor's documented first-priority fallback.
117
+ */
118
+ private static checkExtraRequiredFallbackCredentials;
113
119
  /**
114
120
  * Check AWS Bedrock configuration
115
121
  */
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { logger } from "./logger.js";
6
6
  import { AIProviderName, OpenAIModels, GoogleAIModels, AnthropicModels, BedrockModels, } from "../constants/enums.js";
7
- import { PROJECT_ID_FORMAT } from "./providerConfig.js";
7
+ import { PROJECT_ID_FORMAT, satisfiesFallbacks } from "./providerConfig.js";
8
8
  import { basename } from "path";
9
9
  import { createProxyFetch } from "../proxy/proxyFetch.js";
10
10
  import { DEFAULT_OLLAMA_MODEL } from "../providers/ollama/constants.js";
@@ -194,7 +194,7 @@ export class ProviderHealthChecker {
194
194
  const descriptor = ProviderFactory.getDescriptor(AIProviderName.VERTEX);
195
195
  const { extraRequired, extraRequiredFallbacks } = descriptor?.envVars ?? {};
196
196
  const hasValidAuth = (extraRequired ?? []).every((v) => !!process.env[v]) ||
197
- (extraRequiredFallbacks ?? []).some((v) => !!process.env[v]);
197
+ satisfiesFallbacks(extraRequiredFallbacks, process.env);
198
198
  logger.debug("Vertex auth final result", { hasValidAuth });
199
199
  if (hasValidAuth) {
200
200
  healthStatus.hasApiKey = true;
@@ -342,46 +342,40 @@ export class ProviderHealthChecker {
342
342
  }
343
343
  }
344
344
  /**
345
- * Providers whose credential model can't be reduced to "every one of
346
- * these exact env vars must be literally set" — the check in
347
- * checkEnvironmentConfiguration() below ANDs every entry in the
348
- * returned list together, which can't express Vertex's file-OR-
349
- * individual-creds-OR-service-account auth, Bedrock's AWS SDK default
350
- * provider chain (profile / IAM role, no env vars at all), or
351
- * LiteLLM's documented zero-config local proxy. Their real requirement
352
- * is validated by checkProviderSpecificConfig()'s dedicated per-provider
353
- * checks instead matches the original hand-written switch's
354
- * deliberate `return []` for exactly these three. Naively deriving
355
- * [apiKey, ...extraRequired] from the descriptor here (as for the other
356
- * 27 providers) would make checkEnvironmentConfiguration() push a false
357
- * "missing environment variables" issue and therefore isHealthy=false
358
- * — for legitimate fallback-based Vertex auth, AWS_PROFILE/IAM-role
359
- * Bedrock auth, and unauthenticated local LiteLLM proxies. See
360
- * hasProviderEnvVars() in providerUtils.ts for the equivalent OR-aware
361
- * check used for auto-select gating.
362
- */
363
- static ENV_CHECK_DELEGATED_TO_SPECIFIC_CONFIG = new Set([
364
- AIProviderName.VERTEX,
365
- AIProviderName.BEDROCK,
366
- AIProviderName.LITELLM,
367
- ]);
368
- /**
369
- * Get required environment variables for a provider
345
+ * Get required environment variables for a provider.
346
+ *
347
+ * Returns `[]` for providers with `descriptor.credentialsResolvedExternally
348
+ * === true` (Vertex, Bedrock, LiteLLM) the check in
349
+ * checkEnvironmentConfiguration() below ANDs every entry in the returned
350
+ * list together, which can't express Vertex's file-OR-individual-creds-
351
+ * OR-service-account auth, Bedrock's AWS SDK default provider chain
352
+ * (profile / IAM role, no env vars at all), or LiteLLM's documented
353
+ * zero-config local proxy. Their real requirement is validated by
354
+ * checkProviderSpecificConfig()'s dedicated per-provider checks instead.
355
+ * Naively deriving [apiKey, ...extraRequired] from the descriptor for
356
+ * these three (as for the other 27 providers) would make
357
+ * checkEnvironmentConfiguration() push a false "missing environment
358
+ * variables" issue and therefore isHealthy=false for legitimate
359
+ * fallback-based Vertex auth, AWS_PROFILE/IAM-role Bedrock auth, and
360
+ * unauthenticated local LiteLLM proxies. See hasProviderEnvVars() in
361
+ * providerUtils.ts for the equivalent OR-aware check used for
362
+ * auto-select gating. See `ProviderDescriptor.credentialsResolvedExternally`
363
+ * (types/providers.ts) for the field's full documentation.
370
364
  */
371
365
  static getRequiredEnvironmentVariables(providerName) {
372
- // Resolve the descriptor FIRST so the delegation-Set check below is
373
- // keyed on the canonical, alias-resolved `descriptor.name` — not the
374
- // raw, possibly-aliased `providerName` argument. Checking the raw
375
- // input here would let documented aliases (e.g. "googleVertex" for
366
+ // Resolve the descriptor FIRST so the field check below is keyed on
367
+ // the canonical, alias-resolved `descriptor.credentialsResolvedExternally`
368
+ // — not the raw, possibly-aliased `providerName` argument. Checking the
369
+ // raw input here would let documented aliases (e.g. "googleVertex" for
376
370
  // vertex, "aws" for bedrock) skip the delegation and fall through to
377
371
  // the naive [apiKey, ...extraRequired] derivation below, reproducing
378
372
  // the exact false "missing environment variables" regression this
379
- // Set exists to prevent.
373
+ // field exists to prevent.
380
374
  const descriptor = ProviderFactory.getDescriptor(providerName);
381
375
  if (!descriptor) {
382
376
  return [];
383
377
  }
384
- if (this.ENV_CHECK_DELEGATED_TO_SPECIFIC_CONFIG.has(descriptor.name)) {
378
+ if (descriptor.credentialsResolvedExternally) {
385
379
  return [];
386
380
  }
387
381
  const { apiKey, extraRequired } = descriptor.envVars;
@@ -518,11 +512,11 @@ export class ProviderHealthChecker {
518
512
  hasValidAuth = await this.checkGoogleApplicationCredentials(healthStatus);
519
513
  }
520
514
  if (!hasValidAuth) {
521
- hasValidAuth = this.checkIndividualGoogleCredentials(healthStatus);
515
+ hasValidAuth = this.checkExtraRequiredFallbackCredentials(healthStatus);
522
516
  }
523
517
  if (!hasValidAuth) {
524
518
  healthStatus.configurationIssues.push("Google Cloud authentication not configured or credentials file missing");
525
- healthStatus.recommendations.push("Set either GOOGLE_APPLICATION_CREDENTIALS (valid file path), GOOGLE_SERVICE_ACCOUNT_KEY (base64), or both GOOGLE_AUTH_CLIENT_EMAIL and GOOGLE_AUTH_PRIVATE_KEY");
519
+ healthStatus.recommendations.push("Set either GOOGLE_APPLICATION_CREDENTIALS (valid file path), GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK, GOOGLE_SERVICE_ACCOUNT_KEY (base64), or both GOOGLE_AUTH_CLIENT_EMAIL and GOOGLE_AUTH_PRIVATE_KEY");
526
520
  }
527
521
  return hasValidAuth;
528
522
  }
@@ -554,13 +548,20 @@ export class ProviderHealthChecker {
554
548
  }
555
549
  }
556
550
  /**
557
- * Check individual Google credentials
551
+ * Check Vertex's non-file auth fallbacks (GOOGLE_APPLICATION_CREDENTIALS_
552
+ * NEUROLINK, GOOGLE_SERVICE_ACCOUNT_KEY, or the GOOGLE_AUTH_CLIENT_EMAIL +
553
+ * GOOGLE_AUTH_PRIVATE_KEY pair) via the descriptor's extraRequiredFallbacks
554
+ * instead of a hand-maintained env-var list, so this stays in sync with
555
+ * the real gating logic (hasGoogleCredentials()) the same way
556
+ * checkApiKeyValidity()'s vertex branch does. The previous hand-rolled
557
+ * version only recognized GOOGLE_SERVICE_ACCOUNT_KEY or the email+key
558
+ * pair — missing GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK, the
559
+ * descriptor's documented first-priority fallback.
558
560
  */
559
- static checkIndividualGoogleCredentials(healthStatus) {
560
- const hasServiceAccountKey = !!process.env.GOOGLE_SERVICE_ACCOUNT_KEY;
561
- const hasIndividualCredentials = !!(process.env.GOOGLE_AUTH_CLIENT_EMAIL &&
562
- process.env.GOOGLE_AUTH_PRIVATE_KEY);
563
- if (hasServiceAccountKey || hasIndividualCredentials) {
561
+ static checkExtraRequiredFallbackCredentials(healthStatus) {
562
+ const descriptor = ProviderFactory.getDescriptor(AIProviderName.VERTEX);
563
+ const hasValidAuth = satisfiesFallbacks(descriptor?.envVars.extraRequiredFallbacks, process.env);
564
+ if (hasValidAuth) {
564
565
  healthStatus.hasApiKey = true;
565
566
  return true;
566
567
  }
@@ -6,7 +6,7 @@ import { AIProviderFactory } from "../core/factory.js";
6
6
  import { logger } from "./logger.js";
7
7
  import { AIProviderName } from "../constants/enums.js";
8
8
  import { ProviderHealthChecker } from "./providerHealth.js";
9
- import { API_KEY_FORMATS, API_KEY_LENGTHS, PROJECT_ID_FORMAT, } from "./providerConfig.js";
9
+ import { API_KEY_FORMATS, API_KEY_LENGTHS, PROJECT_ID_FORMAT, satisfiesFallbacks, } from "./providerConfig.js";
10
10
  import { DEFAULT_OLLAMA_MODEL } from "../providers/ollama/constants.js";
11
11
  import { ProviderFactory } from "../factories/providerFactory.js";
12
12
  import { PROVIDER_DESCRIPTORS } from "../factories/providerDescriptors.js";
@@ -340,7 +340,7 @@ export function hasProviderEnvVars(provider) {
340
340
  return false;
341
341
  }
342
342
  return ((extraRequired ?? []).every((v) => !!process.env[v]) ||
343
- (extraRequiredFallbacks ?? []).some((v) => !!process.env[v]));
343
+ satisfiesFallbacks(extraRequiredFallbacks, process.env));
344
344
  }
345
345
  /**
346
346
  * Get available provider names
@@ -45,6 +45,23 @@
45
45
  */
46
46
  import { FileErrorCode } from "../errors/index.js";
47
47
  import type { BatchProcessingSummary, FileInfo, FileProcessingError, ProcessorFileProcessingResult, FileProcessorConfig, ProcessorOperationResult, ProcessedFileBase, ProcessOptions } from "../../types/index.js";
48
+ /**
49
+ * Marker on the error raised when a processor refuses content for exceeding
50
+ * the size limit *after* the download — a ZIP entry that expands past the
51
+ * bound, say.
52
+ *
53
+ * Separate from DOWNLOAD_TOO_LARGE because the two fire at different stages,
54
+ * but it exists for the same reason. Without it a bound that fires reaches
55
+ * `buildProcessedResultWithResult` as a plain Error and becomes
56
+ * PROCESSING_FAILED, which is `retryable: true` — so a caller retries a
57
+ * deterministic refusal three times and gets the identical answer each time.
58
+ * FILE_TOO_LARGE is `retryable: false`, which is the truth about this failure.
59
+ */
60
+ export declare const CONTENT_TOO_LARGE_CODE = "CONTENT_TOO_LARGE";
61
+ /** An error that a processor's own size bound was exceeded. */
62
+ export declare function contentTooLargeError(message: string): Error;
63
+ /** Whether `error` is a processor size bound firing rather than a fault. */
64
+ export declare function isContentTooLarge(error: unknown): boolean;
48
65
  /**
49
66
  * Abstract base class for file processors.
50
67
  * Provides common download, validation, and error handling functionality.
@@ -72,6 +72,29 @@ function downloadTooLargeError(maxSizeMB, typeName) {
72
72
  function isDownloadTooLarge(error) {
73
73
  return (error?.code === DOWNLOAD_TOO_LARGE_CODE);
74
74
  }
75
+ /**
76
+ * Marker on the error raised when a processor refuses content for exceeding
77
+ * the size limit *after* the download — a ZIP entry that expands past the
78
+ * bound, say.
79
+ *
80
+ * Separate from DOWNLOAD_TOO_LARGE because the two fire at different stages,
81
+ * but it exists for the same reason. Without it a bound that fires reaches
82
+ * `buildProcessedResultWithResult` as a plain Error and becomes
83
+ * PROCESSING_FAILED, which is `retryable: true` — so a caller retries a
84
+ * deterministic refusal three times and gets the identical answer each time.
85
+ * FILE_TOO_LARGE is `retryable: false`, which is the truth about this failure.
86
+ */
87
+ export const CONTENT_TOO_LARGE_CODE = "CONTENT_TOO_LARGE";
88
+ /** An error that a processor's own size bound was exceeded. */
89
+ export function contentTooLargeError(message) {
90
+ const error = new Error(message);
91
+ error.code = CONTENT_TOO_LARGE_CODE;
92
+ return error;
93
+ }
94
+ /** Whether `error` is a processor size bound firing rather than a fault. */
95
+ export function isContentTooLarge(error) {
96
+ return (error?.code === CONTENT_TOO_LARGE_CODE);
97
+ }
75
98
  /** Node's signal that a zlib output bound was reached. */
76
99
  function isBufferTooLargeError(error) {
77
100
  return (error?.code === "ERR_BUFFER_TOO_LARGE");
@@ -360,6 +383,23 @@ export class BaseFileProcessor {
360
383
  return { success: true, data: result };
361
384
  }
362
385
  catch (error) {
386
+ // A size bound firing is a verdict, not a fault: PROCESSING_FAILED is
387
+ // retryable, and re-running a decompression that will hit the same
388
+ // ceiling is the one thing worth not doing three times.
389
+ if (isContentTooLarge(error)) {
390
+ return {
391
+ success: false,
392
+ // Same detail keys as the other FILE_TOO_LARGE sites, so anything
393
+ // reading them does not have to special-case where the verdict came
394
+ // from. `sizeMB` is absent on purpose: the decompressed size is the
395
+ // number this bound exists to never find out.
396
+ error: this.createError(FileErrorCode.FILE_TOO_LARGE, {
397
+ maxMB: this.config.maxSizeMB,
398
+ type: this.config.fileTypeName,
399
+ reason: error instanceof Error ? error.message : undefined,
400
+ }),
401
+ };
402
+ }
363
403
  return {
364
404
  success: false,
365
405
  error: this.createError(FileErrorCode.PROCESSING_FAILED, { fileType: this.config.fileTypeName }, error instanceof Error ? error : undefined),
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { createRequire } from "node:module";
10
10
  import * as zlib from "node:zlib";
11
- import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
11
+ import { BaseFileProcessor, contentTooLargeError, isContentTooLarge, } from "../base/BaseFileProcessor.js";
12
12
  import { readZipEntryWithinLimit } from "../archive/zipEntryReader.js";
13
13
  import { SIZE_LIMITS } from "../config/index.js";
14
14
  const require = createRequire(import.meta.url);
@@ -71,7 +71,10 @@ export class OpenDocumentProcessor extends BaseFileProcessor {
71
71
  // `maxSizeMB` only ever saw the compressed archive on the way in.
72
72
  const read = readZipEntryWithinLimit(contentEntry, SIZE_LIMITS.DOCUMENT_MAX_MB * 1024 * 1024, zlib);
73
73
  if (read.status === "too-large") {
74
- throw new Error(`content.xml exceeds the ${SIZE_LIMITS.DOCUMENT_MAX_MB}MB limit for OpenDocument content`);
74
+ // Coded, so the bound surfaces as FILE_TOO_LARGE (not retryable)
75
+ // rather than PROCESSING_FAILED (retryable). The file will be
76
+ // exactly as oversized on the next attempt.
77
+ throw contentTooLargeError(`content.xml in "${this.getFilename(fileInfo)}" exceeds the ${SIZE_LIMITS.DOCUMENT_MAX_MB}MB limit for OpenDocument content`);
75
78
  }
76
79
  if (read.status !== "ok") {
77
80
  throw new Error("content.xml could not be read from the archive");
@@ -93,6 +96,13 @@ export class OpenDocumentProcessor extends BaseFileProcessor {
93
96
  }
94
97
  }
95
98
  catch (error) {
99
+ // A size verdict passes through intact. Re-wrapping it as a plain Error
100
+ // drops the code, and the base class then reports PROCESSING_FAILED —
101
+ // which is retryable, so the caller would refetch and decompress a file
102
+ // guaranteed to be exactly as oversized.
103
+ if (isContentTooLarge(error)) {
104
+ throw error;
105
+ }
96
106
  const message = error instanceof Error ? error.message : "Unknown error";
97
107
  throw new Error(`Failed to extract OpenDocument content: ${message}`, {
98
108
  cause: error,
@@ -8,6 +8,7 @@ import { SpanStatusCode, propagation, context } from "@opentelemetry/api";
8
8
  import { tracers } from "../telemetry/tracers.js";
9
9
  import { shouldBypassProxy } from "./utils/noProxyUtils.js";
10
10
  import { createHash } from "node:crypto";
11
+ import { TRANSIENT_NETWORK_CODES } from "../constants/networkErrorCodes.js";
11
12
  async function getLangfuseContext() {
12
13
  try {
13
14
  // Dynamic import to avoid hard dependency — getLangfuseContext is only
@@ -79,15 +80,6 @@ function extractHostname(url) {
79
80
  return "[unknown]";
80
81
  }
81
82
  }
82
- /** Error codes classified as transient (module-scope: the retry path is hot). */
83
- const TRANSIENT_NETWORK_CODES = new Set([
84
- "ECONNRESET",
85
- "ETIMEDOUT",
86
- "ECONNREFUSED",
87
- "EPIPE",
88
- "UND_ERR_SOCKET",
89
- "UND_ERR_CONNECT_TIMEOUT",
90
- ]);
91
83
  /**
92
84
  * Classify a fetch failure as a transient network error worth retrying.
93
85
  *
@@ -583,6 +583,8 @@ export type SetupArgs = {
583
583
  status?: boolean;
584
584
  interactive?: boolean;
585
585
  help?: boolean;
586
+ check?: boolean;
587
+ nonInteractive?: boolean;
586
588
  };
587
589
  /**
588
590
  * Narrowed ProviderInfo used by the main `neurolink setup` command,
@@ -1714,8 +1714,8 @@ export type ProviderDescriptor = {
1714
1714
  modelFallbacks?: readonly string[];
1715
1715
  /** Additional env vars required alongside apiKey (e.g. AWS secret key, Azure endpoint). */
1716
1716
  extraRequired?: readonly string[];
1717
- /** Alternate ways to satisfy extraRequired when it isn't a plain env-var list (e.g. Vertex's file-path-OR-individual-fields auth). */
1718
- extraRequiredFallbacks?: readonly string[];
1717
+ /** Alternate ways to satisfy extraRequired when it isn't a plain env-var list (e.g. Vertex's file-path-OR-individual-fields auth). Each entry is either a single env var name (satisfied alone) or a nested array of names that must ALL be present together (e.g. Vertex's GOOGLE_AUTH_CLIENT_EMAIL + GOOGLE_AUTH_PRIVATE_KEY pair, which is only valid as a pair). Evaluate with `satisfiesFallbacks()` (providerConfig.ts) rather than re-deriving this logic at each call site. */
1718
+ extraRequiredFallbacks?: readonly (string | readonly string[])[];
1719
1719
  /** True when the provider is usable with zero configuration (local runtime with a documented default URL, or a documented non-secret default like LiteLLM's "sk-anything"). */
1720
1720
  optional?: boolean;
1721
1721
  };
@@ -1741,6 +1741,21 @@ export type ProviderDescriptor = {
1741
1741
  autoSelectPriority?: number;
1742
1742
  /** Format-validation regex sourced from providerConfig.ts's API_KEY_FORMATS, when one exists for this provider. */
1743
1743
  apiKeyFormatPattern?: RegExp;
1744
+ /**
1745
+ * True when this provider's credentials are resolved by an external chain
1746
+ * or its own config validator rather than by plain env-var presence, so
1747
+ * its required-env-vars can't be expressed as "every one of these exact
1748
+ * names must be literally set". Examples: Vertex accepts a service-account
1749
+ * file OR individual client-email/private-key fields OR a base64 key
1750
+ * (an OR, not an AND, of auth paths); Bedrock falls back to the AWS SDK's
1751
+ * own default credential chain (shared profile, IAM role) with no env
1752
+ * vars required at all; LiteLLM is a documented zero-config local proxy.
1753
+ * `ProviderHealthChecker.getRequiredEnvironmentVariables()` returns `[]`
1754
+ * for these providers and defers to `checkProviderSpecificConfig()`'s
1755
+ * dedicated per-provider check instead of deriving a flat AND-list from
1756
+ * `envVars`.
1757
+ */
1758
+ credentialsResolvedExternally?: boolean;
1744
1759
  };
1745
1760
  /** Minimal NeuroLink-like instance accepted by the image generation service. */
1746
1761
  export type NeuroLinkInstance = {
@@ -13,21 +13,84 @@
13
13
  import { ProviderError, AuthenticationError, RateLimitError, InvalidModelError, NetworkError, } from "../types/index.js";
14
14
  import { TimeoutError } from "./timeout.js";
15
15
  import { duckTypedStatusCode } from "./providerRetry.js";
16
+ import { TRANSIENT_NETWORK_CODES } from "../constants/networkErrorCodes.js";
17
+ import { redactUrlsInText } from "./logSanitize.js";
18
+ /** Bounded walk depth for `.cause` chains — matches the precedent in
19
+ * `proxy/proxyFetch.ts`'s `isTransientNetworkError`. Guards against
20
+ * pathological/cyclic `.cause` chains hanging classification. */
21
+ const MAX_CAUSE_DEPTH = 5;
22
+ /**
23
+ * Walk `error.cause` up to `MAX_CAUSE_DEPTH` links, guarded by a seen-set so
24
+ * a cyclic chain (`a.cause === a`, or a longer cycle) terminates instead of
25
+ * looping. Node's native `fetch` (undici) throws `TypeError: fetch failed`
26
+ * with the real transport error nested under `.cause` — sometimes another
27
+ * level deep (e.g. a SocketError inside a ConnectTimeoutError) — so a
28
+ * classifier that only reads the outer error's `.message`/`.code` never
29
+ * sees it.
30
+ */
31
+ function collectCauseChain(error) {
32
+ const chain = [];
33
+ const seen = new Set();
34
+ let current = error;
35
+ while (current &&
36
+ typeof current === "object" &&
37
+ !seen.has(current) &&
38
+ chain.length < MAX_CAUSE_DEPTH) {
39
+ seen.add(current);
40
+ const record = current;
41
+ chain.push(record);
42
+ current = record.cause;
43
+ }
44
+ return chain;
45
+ }
46
+ function firstString(chain, key) {
47
+ for (const record of chain) {
48
+ if (typeof record[key] === "string") {
49
+ return record[key];
50
+ }
51
+ }
52
+ return undefined;
53
+ }
16
54
  function buildErrorContext(error, provider, modelName) {
17
- const record = error && typeof error === "object"
18
- ? error
19
- : undefined;
20
- const message = typeof record?.message === "string"
21
- ? record.message
55
+ const chain = collectCauseChain(error);
56
+ const top = chain[0];
57
+ const topMessage = typeof top?.message === "string"
58
+ ? top.message
22
59
  : error instanceof Error
23
60
  ? error.message
24
61
  : "Unknown error";
62
+ // Compose (never replace) the message: append the deepest cause's message
63
+ // when it differs from the top, so existing rules matching the outer text
64
+ // (e.g. "rate limit", "model not found") keep matching, while the real
65
+ // transport failure buried in .cause becomes visible to rules that need
66
+ // it (e.g. a nested "ECONNREFUSED").
67
+ // The nested message is redacted before it is composed in: an undici cause
68
+ // carries the full request URL, so a presigned token would otherwise reach
69
+ // a client-facing error message through this path. Only the nested text is
70
+ // scrubbed — the provider's own top-level message is left alone, since
71
+ // several providers deliberately name their base URL in it.
72
+ const deepest = chain[chain.length - 1];
73
+ const deepestMessage = typeof deepest?.message === "string"
74
+ ? redactUrlsInText(deepest.message)
75
+ : undefined;
76
+ const message = deepestMessage && deepestMessage !== topMessage
77
+ ? `${topMessage}: ${deepestMessage}`
78
+ : topMessage;
79
+ // errorCode/errorName/statusCode: prefer the outer error's own value,
80
+ // falling back to the first cause in the chain that has one.
81
+ let statusCode;
82
+ for (const record of chain) {
83
+ statusCode = duckTypedStatusCode(record);
84
+ if (statusCode !== undefined) {
85
+ break;
86
+ }
87
+ }
25
88
  return {
26
89
  error,
27
90
  message,
28
- statusCode: duckTypedStatusCode(error),
29
- errorName: typeof record?.name === "string" ? record.name : undefined,
30
- errorCode: typeof record?.code === "string" ? record.code : undefined,
91
+ statusCode,
92
+ errorName: firstString(chain, "name"),
93
+ errorCode: firstString(chain, "code"),
31
94
  provider,
32
95
  modelName,
33
96
  };
@@ -80,13 +143,39 @@ export const DEFAULT_ERROR_RULES = [
80
143
  : `${ctx.provider} model not found.`,
81
144
  },
82
145
  {
83
- match: (ctx) => /ECONNRESET|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network|connection/i.test(ctx.message),
146
+ // Message regex covers providers/SDKs that surface a code as text
147
+ // (e.g. AWS SDK wrapping "ECONNRESET" into its own message). errorCode
148
+ // covers undici's native fetch(), which wraps transport failures as
149
+ // `TypeError: fetch failed` and puts the *structured* code
150
+ // (ECONNREFUSED, UND_ERR_SOCKET, ...) on a nested `.cause` rather than
151
+ // in any message text — buildErrorContext's cause walk surfaces it here.
152
+ match: (ctx) => /ECONNRESET|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network|connection/i.test(ctx.message) ||
153
+ (ctx.errorCode !== undefined &&
154
+ TRANSIENT_NETWORK_CODES.has(ctx.errorCode)),
84
155
  errorClass: NetworkError,
85
156
  message: (ctx) => `Connection error: ${ctx.message}`,
86
157
  },
87
158
  {
88
- match: (ctx) => (ctx.statusCode !== undefined && ctx.statusCode >= 500) ||
89
- /\b5\d\d\b|server error/i.test(ctx.message),
159
+ // Batch J Task 3: the old `/\b5\d\d\b/` matched ANY bare 3-digit number
160
+ // in [500,599) anywhere in the message — e.g. "max_tokens (500) exceeds
161
+ // model limit" — with no relation to an actual HTTP status. Tightened to
162
+ // require the number sit in a status-shaped context: immediately next
163
+ // to "error" (either order) or "status"/"status code" (a common HTTP
164
+ // client wrapper phrase, e.g. axios's "Request failed with status code
165
+ // 500"), with a bounded gap so unrelated digits nearby can't bridge the
166
+ // match — or a named 5xx phrase that needs no digit at all ("bad
167
+ // gateway", "service unavailable", "gateway timeout", "server error",
168
+ // which already covers "... Internal Server Error"). This changes the
169
+ // MATCHED MESSAGE TEXT only, never the classified class: when no rule
170
+ // matches, `classifyProviderError`'s fallback also returns
171
+ // `ProviderError` (see above) — the same class this rule assigns — so
172
+ // narrowing this regex can only move a message between "${provider}
173
+ // server error: ..." and "${provider} error: ...", never between error
174
+ // classes.
175
+ match: (ctx) => (ctx.statusCode !== undefined &&
176
+ ctx.statusCode >= 500 &&
177
+ ctx.statusCode <= 599) ||
178
+ /server error|bad gateway|service unavailable|gateway timeout|\berror\b\D{0,12}\b5\d\d\b|\b5\d\d\b\D{0,12}\berror\b|\bstatus(?:\s*code)?\b\D{0,12}\b5\d\d\b/i.test(ctx.message),
90
179
  errorClass: ProviderError,
91
180
  message: (ctx) => `${ctx.provider} server error: ${ctx.message}`,
92
181
  },
@@ -70,6 +70,22 @@ export declare function getProviderModel(envVar: string, defaultModel: string):
70
70
  * @returns True if one of the credentials is available
71
71
  */
72
72
  export declare function hasProviderCredentials(envVars: string[]): boolean;
73
+ /**
74
+ * Evaluates a `ProviderDescriptor.envVars.extraRequiredFallbacks`-shaped
75
+ * list against an env-var source. Each entry is either a single env var
76
+ * name (satisfied on its own) or a nested array of names that must ALL be
77
+ * present together (e.g. Vertex's GOOGLE_AUTH_CLIENT_EMAIL +
78
+ * GOOGLE_AUTH_PRIVATE_KEY pair, which is only valid auth as a pair).
79
+ * Returns true when at least one entry is satisfied. The single evaluation
80
+ * site for this shape — every consumer (providerUtils.ts, providerHealth.ts,
81
+ * setup.ts, environmentManager.ts) must call this instead of re-deriving the
82
+ * same `.some()`/`.every()` logic, so they can't drift out of sync with each
83
+ * other or with the real auth gate (hasGoogleCredentials()).
84
+ * @param env Explicit env-var source (`process.env`, or a parsed .env file) —
85
+ * never hardcoded, so callers checking a file's contents (not the live
86
+ * process env) can reuse this too.
87
+ */
88
+ export declare function satisfiesFallbacks(fallbacks: readonly (string | readonly string[])[] | undefined, env: Record<string, string | undefined>): boolean;
73
89
  /**
74
90
  * Creates Anthropic provider configuration
75
91
  * Supports both API key and OAuth authentication methods
@@ -185,6 +185,29 @@ export function getProviderModel(envVar, defaultModel) {
185
185
  export function hasProviderCredentials(envVars) {
186
186
  return envVars.some((envVar) => !!process.env[envVar]);
187
187
  }
188
+ /**
189
+ * Evaluates a `ProviderDescriptor.envVars.extraRequiredFallbacks`-shaped
190
+ * list against an env-var source. Each entry is either a single env var
191
+ * name (satisfied on its own) or a nested array of names that must ALL be
192
+ * present together (e.g. Vertex's GOOGLE_AUTH_CLIENT_EMAIL +
193
+ * GOOGLE_AUTH_PRIVATE_KEY pair, which is only valid auth as a pair).
194
+ * Returns true when at least one entry is satisfied. The single evaluation
195
+ * site for this shape — every consumer (providerUtils.ts, providerHealth.ts,
196
+ * setup.ts, environmentManager.ts) must call this instead of re-deriving the
197
+ * same `.some()`/`.every()` logic, so they can't drift out of sync with each
198
+ * other or with the real auth gate (hasGoogleCredentials()).
199
+ * @param env Explicit env-var source (`process.env`, or a parsed .env file) —
200
+ * never hardcoded, so callers checking a file's contents (not the live
201
+ * process env) can reuse this too.
202
+ */
203
+ export function satisfiesFallbacks(fallbacks, env) {
204
+ if (!fallbacks) {
205
+ return false;
206
+ }
207
+ return fallbacks.some((entry) => typeof entry === "string"
208
+ ? !!env[entry]
209
+ : entry.every((name) => !!env[name]));
210
+ }
188
211
  // =============================================================================
189
212
  // PROVIDER-SPECIFIC CONFIGURATION CREATORS
190
213
  // =============================================================================