@juspay/neurolink 13.0.3 → 13.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.
@@ -22,6 +22,32 @@ const TRANSFORMS = {
22
22
  export function normalizeBaseURL(raw, transforms) {
23
23
  return transforms.reduce((value, name) => TRANSFORMS[name](value), raw);
24
24
  }
25
+ /**
26
+ * The three spellings the rest of the SDK treats as this machine (the OTLP log
27
+ * sink, proxy replay and native telemetry use the same list). It is exact on
28
+ * purpose: `new URL()` has already folded `0x7f.1` and `2130706433` into
29
+ * `127.0.0.1` and lowercased the host, and the rest of 127/8, `0.0.0.0` and
30
+ * `*.localhost` are not loopback here.
31
+ */
32
+ const LOOPBACK_HOSTS = new Set([
33
+ "localhost",
34
+ "127.0.0.1",
35
+ "[::1]",
36
+ ]);
37
+ /** Whether a `URL.hostname` is this machine, so a plain-http request to it never leaves it. */
38
+ export function isLoopbackHost(hostname) {
39
+ return LOOPBACK_HOSTS.has(hostname);
40
+ }
41
+ /**
42
+ * Why a parsed base URL may not be used, or undefined: a plain-http URL that
43
+ * is not loopback would send the bearer key and the decision body in cleartext.
44
+ * The message names no part of the URL.
45
+ */
46
+ export function cleartextBaseURLProblem(url, displayName, fix) {
47
+ return url.protocol === "http:" && !isLoopbackHost(url.hostname)
48
+ ? `The ${displayName} base URL must use https:// unless it points at this machine (localhost, 127.0.0.1 or [::1]). ${fix}`
49
+ : undefined;
50
+ }
25
51
  const SHAPE_CHECKS = {
26
52
  // None of these messages repeats the base URL: it can hold a credential,
27
53
  // `fetch` rejects userinfo and echoes it, and a route appended after a query
@@ -35,6 +61,11 @@ const SHAPE_CHECKS = {
35
61
  if (url.protocol !== "https:" && url.protocol !== "http:") {
36
62
  return `The ${displayName} base URL must start with https:// or http://. ${fix}`;
37
63
  }
64
+ // A cleartext request would carry the bearer key and the decision body.
65
+ const cleartext = cleartextBaseURLProblem(url, displayName, fix);
66
+ if (cleartext) {
67
+ return cleartext;
68
+ }
38
69
  // The raw text is checked as well: `search` and `hash` are empty for a
39
70
  // lone "?" or "#", which still swallows the route appended after it.
40
71
  return url.username ||
@@ -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);
@@ -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. */