@juspay/neurolink 13.0.2 → 13.1.0

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.
@@ -3,6 +3,7 @@ import { sniffImageMimeType } from "../utils/imageDetection.js";
3
3
  import { logger } from "../utils/logger.js";
4
4
  import { redactUrlForError } from "../utils/logSanitize.js";
5
5
  import { getProviderModel } from "../utils/providerConfig.js";
6
+ import { cleartextBaseURLProblem } from "./decide/baseUrl.js";
6
7
  import { isRecord, redactCredentials, SystemOneDecisionProvider, } from "./systemOneDecision.js";
7
8
  const PERPLEXITY_DEFAULT_BASE_URL = "https://api.perplexity.ai";
8
9
  const PERPLEXITY_ROUTE = "/v1/decisions";
@@ -50,6 +51,10 @@ function baseURLProblem(baseURL) {
50
51
  if (url.protocol !== "https:" && url.protocol !== "http:") {
51
52
  return `The Perplexity base URL must start with https:// or http://. ${fix}`;
52
53
  }
54
+ const cleartext = cleartextBaseURLProblem(url, "Perplexity", fix);
55
+ if (cleartext) {
56
+ return cleartext;
57
+ }
53
58
  return url.username || url.password || url.search || url.hash
54
59
  ? `The Perplexity base URL must not carry credentials, a query string or a fragment. ${fix}`
55
60
  : undefined;
@@ -56,6 +56,21 @@ export function redactCredentials(message, apiKey) {
56
56
  .replace(/data:[a-z]+\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/gi, "[media]")
57
57
  .trim();
58
58
  }
59
+ /**
60
+ * Whether a failed `fetch` was one that refused a redirect. undici words every
61
+ * transport failure as `TypeError: fetch failed` and puts the reason on
62
+ * `cause`, so the chain is walked for its "unexpected redirect" text.
63
+ */
64
+ function isRedirectRefusal(error) {
65
+ let current = error;
66
+ for (let depth = 0; depth < 5 && current instanceof Error; depth++) {
67
+ if (current.message === "unexpected redirect") {
68
+ return true;
69
+ }
70
+ current = current.cause;
71
+ }
72
+ return false;
73
+ }
59
74
  /**
60
75
  * Rejects with the signal's reason when it aborts, so a caller tearing a turn
61
76
  * down is not held, nor its process kept alive, for the length of a vendor's
@@ -261,6 +276,9 @@ export class SystemOneDecisionProvider extends BaseProvider {
261
276
  headers: this.decisionHeaders(),
262
277
  body,
263
278
  signal,
279
+ // A 307 or 308 would replay the decision body at whatever address
280
+ // the server names.
281
+ redirect: "error",
264
282
  });
265
283
  const latencyMs = Date.now() - startedAt;
266
284
  const requestId = this.readRequestId(response.headers);
@@ -351,12 +369,18 @@ export class SystemOneDecisionProvider extends BaseProvider {
351
369
  // turn is already being torn down.
352
370
  const aborted = request.signal?.aborted === true;
353
371
  const isTimeout = error instanceof Error && error.name === "TimeoutError";
372
+ const redirected = isRedirectRefusal(error);
354
373
  lastError = {
355
374
  kind: aborted ? "network" : isTimeout ? "timeout" : "network",
356
- message: redactCredentials(redactUrlsInText(error instanceof Error ? error.message : String(error)), this.decisionApiKey()),
357
- retryable: !aborted,
375
+ // `fetch` words a refused redirect as "fetch failed", which says
376
+ // nothing about why, and asking again meets the same answer. The
377
+ // address it points at is the server's to choose, so it is not quoted.
378
+ message: redirected
379
+ ? `${label} answered with a redirect, which is refused so the decision is not sent anywhere else. Point the base URL at the final address.`
380
+ : redactCredentials(redactUrlsInText(error instanceof Error ? error.message : String(error)), this.decisionApiKey()),
381
+ retryable: !aborted && !redirected,
358
382
  };
359
- if (aborted || attempt === this.maxRetries) {
383
+ if (!lastError.retryable || attempt === this.maxRetries) {
360
384
  throw this.decisionError(lastError);
361
385
  }
362
386
  await sleep(2 ** attempt * 250 + Math.random() * 250);
@@ -443,16 +443,51 @@ export type ClientMiddlewareContext = {
443
443
  /** Additional context data */
444
444
  [key: string]: unknown;
445
445
  };
446
+ /**
447
+ * What `type` holds on the element `NeuroLinkProvider` returns: React's context
448
+ * provider descriptor, an exotic component that carries a `$$typeof` tag and is
449
+ * not meant to be called.
450
+ *
451
+ * Spelled without importing React, for the reason given on `ClientReactElement`.
452
+ * The call signature never returns, so the descriptor cannot be called for a
453
+ * value, and `props` is `unknown` so it still fits React's `JSXElementConstructor`.
454
+ */
455
+ export type ClientReactProviderDescriptor = ((props: unknown) => never) & {
456
+ readonly $$typeof: symbol;
457
+ };
458
+ /**
459
+ * What `NeuroLinkProvider` returns, spelled without importing React.
460
+ *
461
+ * The published declaration of the hooks module cannot name a React type: `react`
462
+ * is an optional peer, and a consumer without it fails to compile the whole
463
+ * `@juspay/neurolink/client` entry point (TS2307). This is the shape of React's
464
+ * element, so a value of this type is still a valid JSX component return and
465
+ * assignable to `ReactNode` and `ReactElement`.
466
+ *
467
+ * `type` is either a host-element tag (a string) or the provider's descriptor,
468
+ * which is what the element returned by `NeuroLinkProvider` carries at runtime.
469
+ * Narrow it before use (`typeof element.type === "string"`); a string method
470
+ * called on it directly is a compile error. Nothing should rely on it: it names
471
+ * React's internal representation, which React may change.
472
+ */
473
+ export type ClientReactElement = {
474
+ type: string | ClientReactProviderDescriptor;
475
+ props: unknown;
476
+ key: string | null;
477
+ };
446
478
  /**
447
479
  * Props for the NeuroLinkProvider React component.
448
480
  *
449
- * `children` is typed as `unknown` so this module stays React-agnostic;
450
- * the provider component in reactHooks.tsx narrows it to `ReactNode`.
481
+ * `children` is typed as `unknown` so the declaration needs no React types.
482
+ * Anything React accepts as a child is accepted, and so is anything else: the
483
+ * type does not check it. The component renders it as a React node, so a value
484
+ * React cannot render (a function, a plain object) compiles and then fails in
485
+ * React at render time.
451
486
  */
452
487
  export type NeuroLinkProviderProps = {
453
488
  /** Client configuration */
454
489
  config: ClientConfig;
455
- /** Child components (ReactNode at runtime) */
490
+ /** Child components. Typed `unknown` and rendered as a React node without a type check. */
456
491
  children: unknown;
457
492
  };
458
493
  /**
@@ -50,6 +50,13 @@ export type PrecallEvaluationConfig = {
50
50
  * - Phone: '\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b'
51
51
  * - SSN: '\\b\\d{3}-\\d{2}-\\d{4}\\b'
52
52
  * - Custom words: '\\b(word1|word2|word3)\\b'
53
+ *
54
+ * A pattern is refused, skipped and logged as an error (never thrown at the
55
+ * caller) when it is longer than 1000 characters or has a shape that can
56
+ * backtrack catastrophically: a repeated group with a variable-length part
57
+ * that can also match what follows it, a repetition bound above 500, or
58
+ * overlapping alternatives under a repetition. See
59
+ * `BadWordsConfig.regexPatterns`.
53
60
  */
54
61
  sanitizationPatterns?: string[];
55
62
  /**
@@ -67,6 +74,37 @@ export type PrecallEvaluationConfig = {
67
74
  export type BadWordsConfig = {
68
75
  enabled?: boolean;
69
76
  list?: string[];
77
+ /**
78
+ * Regular expressions to redact, each applied with the `gi` flags. When set
79
+ * they take priority over `list`.
80
+ *
81
+ * Patterns run synchronously on the whole response, so a pattern that can
82
+ * backtrack catastrophically would stall the process. A pattern is refused,
83
+ * skipped and logged as an error (the same handling as an invalid regex,
84
+ * never thrown at the caller) when it:
85
+ *
86
+ * - is longer than 1000 characters;
87
+ * - repeats a group in which a variable-length part (`+`, `*`, `?`, `{n,m}`)
88
+ * can match the same text as whatever follows it, so the input can be
89
+ * split many ways: `(a+)+`, `(\w+\s*)*`, `(.*a){10}`, `(a?a?)+`,
90
+ * `(a{1,30}){1,30}`;
91
+ * - repeats a group whose alternatives may overlap, such as `(a|aa)+`;
92
+ * - chains more than a handful of overlapping optional or open-ended parts,
93
+ * such as `a?a?a?a?a?a?a?a?a?` or `.*a.*b.*c.*d.*e.*f`;
94
+ * - has a repetition bound above 500, such as `a{1,1000}`.
95
+ *
96
+ * A repeated group is fine when each variable-length part is followed by
97
+ * something it cannot match: `(\d{1,3}\.){3}`, `([a-z0-9-]+\.)+` and
98
+ * `(?:\d[ -]?){13,16}` all run.
99
+ *
100
+ * A skipped pattern filters nothing. Use `list` for plain terms, or flatten
101
+ * the pattern (`[\w-]+` instead of `([\w-]+)+`).
102
+ *
103
+ * Example patterns:
104
+ * - SSN: '\\d{3}-\\d{2}-\\d{4}'
105
+ * - Card number: '\\d{16}'
106
+ * - Words: '\\b(password|secret)\\b'
107
+ */
70
108
  regexPatterns?: string[];
71
109
  /**
72
110
  * Text to use when replacing filtered content.
@@ -109,8 +147,24 @@ export type ContentFilteringResult = {
109
147
  hasChanges: boolean;
110
148
  appliedFilters: string[];
111
149
  filteringStats: {
150
+ /** Regex patterns that were compiled and run. */
112
151
  regexPatternsApplied: number;
152
+ /**
153
+ * Regex patterns refused before they ran because they were too long or
154
+ * could backtrack catastrophically. Invalid patterns are not counted.
155
+ */
156
+ regexPatternsRejected?: number;
113
157
  stringFiltersApplied: number;
114
158
  totalMatches: number;
115
159
  };
116
160
  };
161
+ /**
162
+ * Outcome of vetting a caller-supplied regular expression before it is
163
+ * compiled and run against model output.
164
+ */
165
+ export type RegexPatternVerdict = {
166
+ safe: true;
167
+ } | {
168
+ safe: false;
169
+ reason: string;
170
+ };
@@ -1319,25 +1319,42 @@ export type RegistryConfig = {
1319
1319
  */
1320
1320
  type: RegistrySourceType;
1321
1321
  /**
1322
- * Registry URL or identifier
1322
+ * Base URL of the registry. `official` defaults to
1323
+ * `https://registry.modelcontextprotocol.io`; `custom` requires it. The
1324
+ * client appends `/v0.1/servers`, so pass the origin (a trailing `/v0.1`
1325
+ * is accepted too). Only `http:` and `https:` URLs without embedded
1326
+ * credentials are used.
1323
1327
  */
1324
1328
  url?: string;
1325
1329
  /**
1326
- * Authentication token
1330
+ * Bearer token sent as `Authorization: Bearer <token>` to this registry
1331
+ * only. It is never emitted in events or logs. A registry with a token is
1332
+ * queried only over `https:` or on a loopback host (`localhost`,
1333
+ * `127.0.0.0/8`, `[::1]`); a plain `http:` URL to any other host is skipped
1334
+ * so the token never crosses the network in clear.
1327
1335
  */
1328
1336
  authToken?: string;
1329
1337
  /**
1330
- * Request timeout in milliseconds
1338
+ * Request timeout in milliseconds. Overrides the client-wide `timeout`
1339
+ * for this registry.
1331
1340
  */
1332
1341
  timeout?: number;
1333
1342
  /**
1334
- * Enable caching
1343
+ * Set to `false` to turn off caching of this registry's responses. It
1344
+ * cannot override a client-wide `enableCache: false`, which turns caching
1345
+ * off for every registry.
1335
1346
  */
1336
1347
  enableCache?: boolean;
1337
1348
  /**
1338
- * Cache TTL in milliseconds
1349
+ * Cache TTL in milliseconds for this registry's responses. Falls back to
1350
+ * the client-wide `defaultCacheTTL`.
1339
1351
  */
1340
1352
  cacheTTL?: number;
1353
+ /**
1354
+ * Maximum number of list pages (100 servers each) fetched when this
1355
+ * registry is merged into `search()` / `getAllEntries()`. Defaults to 5.
1356
+ */
1357
+ maxPages?: number;
1341
1358
  };
1342
1359
  /**
1343
1360
  * Search options for registry queries
@@ -1395,11 +1412,24 @@ export type RegistrySearchResult = {
1395
1412
  */
1396
1413
  export type MCPRegistryClientConfig = {
1397
1414
  /**
1398
- * Default registries to use
1415
+ * Registries to consult when `remoteLookup` is on. Only `official` and
1416
+ * `custom` registries are queried; `npm` and `github` entries are accepted
1417
+ * for compatibility but skipped. Defaults to the official MCP Registry.
1399
1418
  */
1400
1419
  registries?: RegistryConfig[];
1401
1420
  /**
1402
- * Enable automatic caching
1421
+ * Opt in to remote lookups. Default `false`: the client then reads only
1422
+ * its built-in catalog and custom entries and never touches the network,
1423
+ * whatever `registries` says. When `true`, `getEntry()` falls back to the
1424
+ * configured registries on a local miss and `search()` / `getAllEntries()`
1425
+ * merge their servers after the local ones. Remote entries are always
1426
+ * `verified: false`, and any network or parse failure falls back to the
1427
+ * local result instead of throwing.
1428
+ */
1429
+ remoteLookup?: boolean;
1430
+ /**
1431
+ * Enable automatic caching. `false` turns it off for the merged list and
1432
+ * for every registry, including one that sets its own `enableCache: true`.
1403
1433
  */
1404
1434
  enableCache?: boolean;
1405
1435
  /**
@@ -1407,14 +1437,80 @@ export type MCPRegistryClientConfig = {
1407
1437
  */
1408
1438
  defaultCacheTTL?: number;
1409
1439
  /**
1410
- * Request timeout
1440
+ * Request timeout in milliseconds for each remote registry request
1441
+ * (default 10000). A registry's own `timeout` takes precedence.
1411
1442
  */
1412
1443
  timeout?: number;
1413
1444
  /**
1414
- * User agent string
1445
+ * User-Agent header sent on every remote registry request
1415
1446
  */
1416
1447
  userAgent?: string;
1417
1448
  };
1449
+ /**
1450
+ * An input (environment variable or header) declared by a registry record.
1451
+ * Mirrors the `KeyValueInput` schema of the official MCP Registry API.
1452
+ */
1453
+ export type RemoteRegistryInput = {
1454
+ name: string;
1455
+ isRequired: boolean;
1456
+ isSecret: boolean;
1457
+ };
1458
+ /**
1459
+ * Transport declared by a registry record (`stdio`, `streamable-http` or
1460
+ * `sse`). `url` is present for the remote transports.
1461
+ */
1462
+ export type RemoteRegistryTransport = {
1463
+ type: string;
1464
+ url?: string;
1465
+ };
1466
+ /**
1467
+ * A package a registry record can be installed from (`npm`, `pypi`, `oci`, ...)
1468
+ */
1469
+ export type RemoteRegistryPackage = {
1470
+ registryType: string;
1471
+ identifier: string;
1472
+ version?: string;
1473
+ transport?: RemoteRegistryTransport;
1474
+ environmentVariables: RemoteRegistryInput[];
1475
+ };
1476
+ /**
1477
+ * Server description inside a registry record. Mirrors the `ServerDetail`
1478
+ * schema of the official MCP Registry API, reduced to the fields the client
1479
+ * reads; everything else in the response is ignored.
1480
+ */
1481
+ export type RemoteRegistryServer = {
1482
+ name: string;
1483
+ description: string;
1484
+ version: string;
1485
+ repositoryUrl?: string;
1486
+ websiteUrl?: string;
1487
+ packages: RemoteRegistryPackage[];
1488
+ remotes: RemoteRegistryTransport[];
1489
+ };
1490
+ /**
1491
+ * One validated record from a remote registry: the server plus the status
1492
+ * and timestamp the official registry reports under `_meta`.
1493
+ */
1494
+ export type RemoteRegistryRecord = {
1495
+ server: RemoteRegistryServer;
1496
+ status?: string;
1497
+ updatedAt?: string;
1498
+ };
1499
+ /**
1500
+ * One validated page of a registry's server list
1501
+ */
1502
+ export type RemoteRegistryPage = {
1503
+ records: RemoteRegistryRecord[];
1504
+ nextCursor?: string;
1505
+ };
1506
+ /**
1507
+ * A registry the client can query: its configuration plus the resolved,
1508
+ * credential-free API base URL (`.../v0.1`)
1509
+ */
1510
+ export type RemoteRegistryTarget = {
1511
+ config: RegistryConfig;
1512
+ apiBase: string;
1513
+ };
1418
1514
  /**
1419
1515
  * Load balancing strategies
1420
1516
  */
@@ -31,12 +31,13 @@ export type CatalogModelSpec = {
31
31
  /**
32
32
  * Per-model override of the provider-level `capabilities.tools`, for a
33
33
  * "model-dependent" provider whose vendor docs name which specific models
34
- * accept tool definitions (e.g. Reka: "Currently, only Reka Flash
35
- * supports function calling"). Absent means "inherit the provider-level
36
- * answer", same as every model before this field existed. Only meaningful
37
- * when `capabilities.tools` is `"model-dependent"` — a provider that
38
- * already declares a plain `true`/`false` has no need for it, since that
39
- * single value already applies to every model it serves.
34
+ * accept tool definitions (e.g. Reka: its Models table has a Tools column,
35
+ * Yes for reka-edge-2603 and No for reka-flash-3). Absent means "inherit
36
+ * the provider-level answer", same as every model before this field
37
+ * existed. Only meaningful when `capabilities.tools` is
38
+ * `"model-dependent"` — a provider that already declares a plain
39
+ * `true`/`false` has no need for it, since that single value already
40
+ * applies to every model it serves.
40
41
  */
41
42
  tools?: boolean;
42
43
  status: CatalogModelStatus;
@@ -263,7 +264,7 @@ export type ProviderCatalogJson = {
263
264
  };
264
265
  export type DecideFormatterName = "fastapi-validation";
265
266
  export type DecideBaseURLTransform = "strip-trailing-slash" | "strip-v1-suffix";
266
- /** A check a base URL must pass once normalised; `origin` allows only an http(s) URL with no userinfo, query string or fragment. */
267
+ /** A check a base URL must pass once normalised; `origin` allows only an https URL (http only for localhost, 127.0.0.1 or [::1]) with no userinfo, query string or fragment. */
267
268
  export type DecideBaseURLShape = "origin";
268
269
  export type DecideErrorRule = {
269
270
  /** One status or a list of them. A rule sets this or `statusMin`, never both. */