@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.
- package/CHANGELOG.md +10 -2
- package/dist/browser/neurolink.min.js +419 -419
- package/dist/client/reactHooks.d.ts +2 -5
- package/dist/constants/enums.d.ts +2 -1
- package/dist/constants/enums.js +1 -0
- package/dist/core/toolRouting.js +9 -6
- package/dist/core/toolRoutingDecision.js +5 -3
- package/dist/localUsage/grokReader.js +6 -2
- package/dist/mcp/mcpRegistryClient.d.ts +70 -8
- package/dist/mcp/mcpRegistryClient.js +313 -23
- package/dist/mcp/mcpRegistryRemote.d.ts +50 -0
- package/dist/mcp/mcpRegistryRemote.js +289 -0
- package/dist/middleware/utils/guardrailsUtils.js +21 -9
- package/dist/middleware/utils/regexSafety.d.ts +46 -0
- package/dist/middleware/utils/regexSafety.js +483 -0
- package/dist/providers/catalog/provider-catalog.schema.json +1 -1
- package/dist/providers/catalog/reka.json +15 -5
- package/dist/providers/cloudflareClef.js +5 -0
- package/dist/providers/decide/baseUrl.d.ts +8 -0
- package/dist/providers/decide/baseUrl.js +31 -0
- package/dist/providers/perplexityDecider.js +5 -0
- package/dist/providers/systemOneDecision.js +27 -3
- package/dist/types/client.d.ts +38 -3
- package/dist/types/guardrails.d.ts +54 -0
- package/dist/types/mcp.d.ts +105 -9
- package/dist/types/providerCatalog.d.ts +8 -7
- package/docs-site/static/search-index.json +56 -49
- package/package.json +8 -4
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote registry helpers for MCPRegistryClient
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that resolve a registry configuration to an API base URL,
|
|
5
|
+
* validate responses from the official MCP Registry API (`/v0.1`) and map its
|
|
6
|
+
* server records to `McpRegistryEntry`. Nothing here performs I/O.
|
|
7
|
+
*
|
|
8
|
+
* API reference (spec `2025-12-01`):
|
|
9
|
+
* https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/api/openapi.yaml
|
|
10
|
+
*
|
|
11
|
+
* @module mcp/mcpRegistryRemote
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Base URL of the official MCP Registry
|
|
15
|
+
*/
|
|
16
|
+
export const OFFICIAL_REGISTRY_URL = "https://registry.modelcontextprotocol.io";
|
|
17
|
+
const API_VERSION_SEGMENT = "/v0.1";
|
|
18
|
+
/**
|
|
19
|
+
* Key under which the official registry reports status and timestamps in `_meta`
|
|
20
|
+
*/
|
|
21
|
+
const OFFICIAL_META_KEY = "io.modelcontextprotocol.registry/official";
|
|
22
|
+
// A registry is untrusted input, and a custom registry URL is a supported
|
|
23
|
+
// feature. The npm identifier and version end up in an `npx` argv element and
|
|
24
|
+
// in an install command a user may paste into a shell, so they must be valid
|
|
25
|
+
// npm names and versions, not merely strings. The grammar is the npm
|
|
26
|
+
// package-name rule for new packages (optional `@scope/`, lowercase letters,
|
|
27
|
+
// digits and `._~-`, never a leading dash, dot or underscore) and a
|
|
28
|
+
// conservative version rule (starts alphanumeric). Anything else is not run.
|
|
29
|
+
const NPM_PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
30
|
+
const NPM_PACKAGE_NAME_MAX_LENGTH = 214;
|
|
31
|
+
const NPM_VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_~-]*$/;
|
|
32
|
+
const NPM_VERSION_MAX_LENGTH = 128;
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function stringField(record, key) {
|
|
37
|
+
const value = record[key];
|
|
38
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
39
|
+
}
|
|
40
|
+
function arrayField(record, key) {
|
|
41
|
+
const value = record[key];
|
|
42
|
+
return Array.isArray(value) ? value : [];
|
|
43
|
+
}
|
|
44
|
+
function compact(items) {
|
|
45
|
+
return items.filter((item) => item !== undefined);
|
|
46
|
+
}
|
|
47
|
+
function isLoopbackHost(hostname) {
|
|
48
|
+
return (hostname === "localhost" ||
|
|
49
|
+
hostname === "[::1]" ||
|
|
50
|
+
/^127(?:\.\d{1,3}){3}$/.test(hostname));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a registry configuration to the API base URL the client queries.
|
|
54
|
+
*
|
|
55
|
+
* Returns `undefined` for registries the client does not query: `npm` and
|
|
56
|
+
* `github` sources, a `custom` registry without a URL, any URL that is not
|
|
57
|
+
* plain `http(s)` or that embeds credentials (fetch rejects those, and they
|
|
58
|
+
* would end up in logs), and a registry with an `authToken` on plain `http:`
|
|
59
|
+
* to a non-loopback host (the bearer token would cross the network in clear).
|
|
60
|
+
*/
|
|
61
|
+
export function resolveRemoteTarget(config) {
|
|
62
|
+
if (config.type !== "official" && config.type !== "custom") {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const rawUrl = config.url ?? (config.type === "official" ? OFFICIAL_REGISTRY_URL : "");
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = new URL(rawUrl);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if ((parsed.protocol !== "https:" && parsed.protocol !== "http:") ||
|
|
74
|
+
parsed.username !== "" ||
|
|
75
|
+
parsed.password !== "") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
if (config.authToken &&
|
|
79
|
+
parsed.protocol === "http:" &&
|
|
80
|
+
!isLoopbackHost(parsed.hostname)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const path = parsed.pathname.replace(/\/+$/, "");
|
|
84
|
+
const apiPath = path.endsWith(API_VERSION_SEGMENT)
|
|
85
|
+
? path
|
|
86
|
+
: `${path}${API_VERSION_SEGMENT}`;
|
|
87
|
+
return { config, apiBase: `${parsed.origin}${apiPath}` };
|
|
88
|
+
}
|
|
89
|
+
function parseInput(value) {
|
|
90
|
+
if (!isRecord(value)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const name = stringField(value, "name");
|
|
94
|
+
return name === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: {
|
|
97
|
+
name,
|
|
98
|
+
isRequired: value.isRequired === true,
|
|
99
|
+
isSecret: value.isSecret === true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function parseTransport(value) {
|
|
103
|
+
if (!isRecord(value)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const type = stringField(value, "type");
|
|
107
|
+
if (type === undefined) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const url = stringField(value, "url");
|
|
111
|
+
return url === undefined ? { type } : { type, url };
|
|
112
|
+
}
|
|
113
|
+
function parsePackage(value) {
|
|
114
|
+
if (!isRecord(value)) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const registryType = stringField(value, "registryType");
|
|
118
|
+
const identifier = stringField(value, "identifier");
|
|
119
|
+
if (registryType === undefined || identifier === undefined) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const version = stringField(value, "version");
|
|
123
|
+
const transport = parseTransport(value.transport);
|
|
124
|
+
return {
|
|
125
|
+
registryType,
|
|
126
|
+
identifier,
|
|
127
|
+
...(version === undefined ? {} : { version }),
|
|
128
|
+
...(transport === undefined ? {} : { transport }),
|
|
129
|
+
environmentVariables: compact(arrayField(value, "environmentVariables").map(parseInput)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function parseServer(value) {
|
|
133
|
+
if (!isRecord(value)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const name = stringField(value, "name");
|
|
137
|
+
if (name === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const repository = isRecord(value.repository) ? value.repository : undefined;
|
|
141
|
+
const repositoryUrl = repository === undefined ? undefined : stringField(repository, "url");
|
|
142
|
+
const websiteUrl = stringField(value, "websiteUrl");
|
|
143
|
+
return {
|
|
144
|
+
name,
|
|
145
|
+
description: stringField(value, "description") ?? "",
|
|
146
|
+
version: stringField(value, "version") ?? "",
|
|
147
|
+
...(repositoryUrl === undefined ? {} : { repositoryUrl }),
|
|
148
|
+
...(websiteUrl === undefined ? {} : { websiteUrl }),
|
|
149
|
+
packages: compact(arrayField(value, "packages").map(parsePackage)),
|
|
150
|
+
remotes: compact(arrayField(value, "remotes").map(parseTransport)),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate one `ServerResponse` (`{ server, _meta }`) from the registry.
|
|
155
|
+
* Returns `undefined` when it carries no usable server, so a bad record can
|
|
156
|
+
* be skipped without discarding its neighbours.
|
|
157
|
+
*/
|
|
158
|
+
export function parseRemoteRecord(value) {
|
|
159
|
+
if (!isRecord(value)) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const server = parseServer(value.server);
|
|
163
|
+
if (server === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const meta = isRecord(value._meta) ? value._meta : undefined;
|
|
167
|
+
const official = meta !== undefined ? meta[OFFICIAL_META_KEY] : undefined;
|
|
168
|
+
const status = isRecord(official)
|
|
169
|
+
? stringField(official, "status")
|
|
170
|
+
: undefined;
|
|
171
|
+
const updatedAt = isRecord(official)
|
|
172
|
+
? stringField(official, "updatedAt")
|
|
173
|
+
: undefined;
|
|
174
|
+
return {
|
|
175
|
+
server,
|
|
176
|
+
...(status === undefined ? {} : { status }),
|
|
177
|
+
...(updatedAt === undefined ? {} : { updatedAt }),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Validate a `ServerList` response (`{ servers, metadata: { nextCursor } }`).
|
|
182
|
+
* Throws when `servers` is not an array, which the caller reports as a
|
|
183
|
+
* malformed registry; individual bad records are skipped.
|
|
184
|
+
*/
|
|
185
|
+
export function parseRemotePage(value) {
|
|
186
|
+
if (!isRecord(value) || !Array.isArray(value.servers)) {
|
|
187
|
+
throw new Error("registry list response has no servers array");
|
|
188
|
+
}
|
|
189
|
+
const metadata = isRecord(value.metadata) ? value.metadata : undefined;
|
|
190
|
+
const nextCursor = metadata === undefined ? undefined : stringField(metadata, "nextCursor");
|
|
191
|
+
return {
|
|
192
|
+
records: compact(value.servers.map(parseRemoteRecord)),
|
|
193
|
+
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function toTransportType(type) {
|
|
197
|
+
switch (type) {
|
|
198
|
+
case "stdio":
|
|
199
|
+
return "stdio";
|
|
200
|
+
case "streamable-http":
|
|
201
|
+
return "http";
|
|
202
|
+
case "sse":
|
|
203
|
+
return "sse";
|
|
204
|
+
default:
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The `name@version` spec of an `npm` package, or `undefined` when the
|
|
210
|
+
* identifier or version is not a valid npm name or version.
|
|
211
|
+
*/
|
|
212
|
+
function npmSpecOf(pkg) {
|
|
213
|
+
if (pkg.registryType !== "npm" ||
|
|
214
|
+
pkg.identifier.length > NPM_PACKAGE_NAME_MAX_LENGTH ||
|
|
215
|
+
!NPM_PACKAGE_NAME.test(pkg.identifier)) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
if (pkg.version === undefined) {
|
|
219
|
+
return pkg.identifier;
|
|
220
|
+
}
|
|
221
|
+
return pkg.version.length <= NPM_VERSION_MAX_LENGTH &&
|
|
222
|
+
NPM_VERSION.test(pkg.version)
|
|
223
|
+
? `${pkg.identifier}@${pkg.version}`
|
|
224
|
+
: undefined;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Map a registry record to a catalog entry.
|
|
228
|
+
*
|
|
229
|
+
* Remote entries are never `verified`: that flag means "reviewed by NeuroLink"
|
|
230
|
+
* for the built-in catalog, and the registry's contents are the publishers'
|
|
231
|
+
* own claims. Only an `npm` package whose identifier and version are valid
|
|
232
|
+
* npm names gets a runnable `command`; anything else (other package types such
|
|
233
|
+
* as pypi, oci or nuget, or an npm package with a malformed identifier or
|
|
234
|
+
* version) is listed in `metadata.packages` only, for the caller to act on.
|
|
235
|
+
*/
|
|
236
|
+
export function mapRemoteRecord(record, apiBase) {
|
|
237
|
+
const { server } = record;
|
|
238
|
+
const runnable = server.packages
|
|
239
|
+
.map((pkg) => ({ pkg, spec: npmSpecOf(pkg) }))
|
|
240
|
+
.find((candidate) => candidate.spec !== undefined);
|
|
241
|
+
const transports = Array.from(new Set(compact([
|
|
242
|
+
...server.packages.map((pkg) => pkg.transport === undefined
|
|
243
|
+
? undefined
|
|
244
|
+
: toTransportType(pkg.transport.type)),
|
|
245
|
+
...server.remotes.map((remote) => toTransportType(remote.type)),
|
|
246
|
+
])));
|
|
247
|
+
const requiredEnvVars = runnable === undefined
|
|
248
|
+
? []
|
|
249
|
+
: runnable.pkg.environmentVariables
|
|
250
|
+
.filter((variable) => variable.isRequired)
|
|
251
|
+
.map((variable) => variable.name);
|
|
252
|
+
const remotes = server.remotes.flatMap((remote) => remote.url === undefined ? [] : [{ type: remote.type, url: remote.url }]);
|
|
253
|
+
const metadata = {
|
|
254
|
+
source: "remote-registry",
|
|
255
|
+
registry: apiBase,
|
|
256
|
+
...(record.status === undefined ? {} : { registryStatus: record.status }),
|
|
257
|
+
packages: server.packages.map((pkg) => ({
|
|
258
|
+
registryType: pkg.registryType,
|
|
259
|
+
identifier: pkg.identifier,
|
|
260
|
+
...(pkg.version === undefined ? {} : { version: pkg.version }),
|
|
261
|
+
})),
|
|
262
|
+
remotes,
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
id: server.name,
|
|
266
|
+
name: server.name,
|
|
267
|
+
description: server.description,
|
|
268
|
+
version: server.version,
|
|
269
|
+
...(server.websiteUrl === undefined ? {} : { homepage: server.websiteUrl }),
|
|
270
|
+
...(server.repositoryUrl === undefined
|
|
271
|
+
? {}
|
|
272
|
+
: { repository: server.repositoryUrl }),
|
|
273
|
+
...(runnable === undefined
|
|
274
|
+
? {}
|
|
275
|
+
: {
|
|
276
|
+
npmPackage: runnable.pkg.identifier,
|
|
277
|
+
command: "npx",
|
|
278
|
+
args: ["-y", runnable.spec],
|
|
279
|
+
installCommand: `npx -y ${runnable.spec}`,
|
|
280
|
+
}),
|
|
281
|
+
...(requiredEnvVars.length === 0 ? {} : { requiredEnvVars }),
|
|
282
|
+
...(transports.length === 0 ? {} : { transports }),
|
|
283
|
+
...(record.updatedAt === undefined
|
|
284
|
+
? {}
|
|
285
|
+
: { lastUpdated: record.updatedAt }),
|
|
286
|
+
verified: false,
|
|
287
|
+
metadata,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AIProviderFactory } from "../../core/factory.js";
|
|
2
2
|
import { logger } from "../../utils/logger.js";
|
|
3
|
+
import { vetRegexPattern } from "./regexSafety.js";
|
|
3
4
|
export const DEFAULT_EVALUATION_PROMPT = `
|
|
4
5
|
Evaluate the following user input for safety and appropriateness. Respond ONLY with valid JSON:
|
|
5
6
|
|
|
@@ -21,6 +22,11 @@ Evaluate the following user input for safety and appropriateness. Respond ONLY w
|
|
|
21
22
|
|
|
22
23
|
User Input: "{USER_INPUT}"
|
|
23
24
|
`;
|
|
25
|
+
/** Longest slice of a refused pattern written to the log. */
|
|
26
|
+
const MAX_LOGGED_PATTERN_LENGTH = 60;
|
|
27
|
+
const describePattern = (pattern) => pattern.length > MAX_LOGGED_PATTERN_LENGTH
|
|
28
|
+
? `${pattern.slice(0, MAX_LOGGED_PATTERN_LENGTH)}...`
|
|
29
|
+
: pattern;
|
|
24
30
|
/**
|
|
25
31
|
* Extract user input from middleware params
|
|
26
32
|
*/
|
|
@@ -177,6 +183,11 @@ export function applyEvaluationActions(evaluation, config, userInput) {
|
|
|
177
183
|
logger.debug(`[GuardrailsUtils] Applying ${patterns.length} sanitization patterns with replacement: "${replacementText}".`);
|
|
178
184
|
patterns.forEach((pattern, index) => {
|
|
179
185
|
try {
|
|
186
|
+
const verdict = vetRegexPattern(pattern);
|
|
187
|
+
if (!verdict.safe) {
|
|
188
|
+
logger.error(`[GuardrailsUtils] Refusing sanitization pattern "${describePattern(pattern)}": ${verdict.reason}. The pattern is skipped.`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
180
191
|
const regex = new RegExp(pattern, "gi");
|
|
181
192
|
const before = sanitized;
|
|
182
193
|
let matchCount = 0;
|
|
@@ -315,6 +326,8 @@ export function applyContentFiltering(text, badWordsConfig, context = "unknown")
|
|
|
315
326
|
let hasChanges = false;
|
|
316
327
|
const appliedFilters = [];
|
|
317
328
|
let totalMatches = 0;
|
|
329
|
+
let regexPatternsApplied = 0;
|
|
330
|
+
let regexPatternsRejected = 0;
|
|
318
331
|
const replacementText = badWordsConfig.replacementText || "[REDACTED]";
|
|
319
332
|
// Priority 1: Use regex patterns if provided
|
|
320
333
|
if (badWordsConfig.regexPatterns &&
|
|
@@ -325,16 +338,14 @@ export function applyContentFiltering(text, badWordsConfig, context = "unknown")
|
|
|
325
338
|
logger.debug(`[ContentFiltering:${context}] Applying regex pattern filtering with ${badWordsConfig.regexPatterns.length} patterns using replacement: "${replacementText}".`);
|
|
326
339
|
for (const pattern of badWordsConfig.regexPatterns) {
|
|
327
340
|
try {
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
341
|
+
const verdict = vetRegexPattern(pattern);
|
|
342
|
+
if (!verdict.safe) {
|
|
343
|
+
regexPatternsRejected++;
|
|
344
|
+
logger.error(`[ContentFiltering:${context}] Refusing regex pattern "${describePattern(pattern)}": ${verdict.reason}. The pattern is skipped.`);
|
|
345
|
+
continue;
|
|
331
346
|
}
|
|
332
347
|
const regex = new RegExp(pattern, "gi");
|
|
333
|
-
|
|
334
|
-
regex.test("test");
|
|
335
|
-
if (Date.now() - testStart > 100) {
|
|
336
|
-
logger.warn(`[ContentFiltering:${context}] Regex pattern "${pattern}" appears to be too complex (slow test execution).`);
|
|
337
|
-
}
|
|
348
|
+
regexPatternsApplied++;
|
|
338
349
|
const before = filteredText;
|
|
339
350
|
let matchCount = 0;
|
|
340
351
|
filteredText = filteredText?.replace(regex, () => {
|
|
@@ -377,7 +388,8 @@ export function applyContentFiltering(text, badWordsConfig, context = "unknown")
|
|
|
377
388
|
hasChanges,
|
|
378
389
|
appliedFilters,
|
|
379
390
|
filteringStats: {
|
|
380
|
-
regexPatternsApplied
|
|
391
|
+
regexPatternsApplied,
|
|
392
|
+
regexPatternsRejected,
|
|
381
393
|
stringFiltersApplied: badWordsConfig.list?.length || 0,
|
|
382
394
|
totalMatches,
|
|
383
395
|
},
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RegexPatternVerdict } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Static vetting for regular expressions that a caller hands to the
|
|
4
|
+
* guardrails (`badWords.regexPatterns`, `precallEvaluation.sanitizationPatterns`).
|
|
5
|
+
*
|
|
6
|
+
* Both lists are compiled and run synchronously on the event loop against the
|
|
7
|
+
* whole model response, so one pattern that backtracks catastrophically
|
|
8
|
+
* (`(a+)+$` on a run of `a`) stalls every request in the process. A timing
|
|
9
|
+
* probe on a short fixed string cannot find that: the cost only shows on long
|
|
10
|
+
* adversarial input, and it is measured after the call has already returned.
|
|
11
|
+
* This reads the pattern instead.
|
|
12
|
+
*
|
|
13
|
+
* The cause of every exponential case is one input that can be split between
|
|
14
|
+
* the parts of the pattern in many ways. A variable-length part (`a+`, `a?`,
|
|
15
|
+
* `a{1,3}`, `(ab)*`) is ambiguous when what can follow it may start with text
|
|
16
|
+
* the part itself can match. The check refuses a pattern when:
|
|
17
|
+
*
|
|
18
|
+
* - such a part sits inside a repeated group (`(a+)+`, `(a?a?)+`,
|
|
19
|
+
* `(a{1,30}){1,30}`), where the ways multiply on every iteration;
|
|
20
|
+
* - the alternatives of a repeated group may overlap (`(a|aa)+`);
|
|
21
|
+
* - ambiguous parts are chained: more than {@link MAX_NARROW_WAYS} ways in
|
|
22
|
+
* total from optional or short parts (`a?` nine times in a row), or more
|
|
23
|
+
* than {@link MAX_OPEN_AMBIGUITY} open-ended ones (`.*a.*b.*c.*d.*e`);
|
|
24
|
+
* - a repetition bound is above {@link MAX_REPETITION_BOUND}, or the pattern
|
|
25
|
+
* is longer than {@link MAX_REGEX_PATTERN_LENGTH}.
|
|
26
|
+
*
|
|
27
|
+
* A variable-length part that is followed by something it cannot match is
|
|
28
|
+
* unambiguous, so `(\d{1,3}\.){3}`, `([a-z0-9-]+\.)+` and `(?:\d[ -]?){13,16}`
|
|
29
|
+
* run.
|
|
30
|
+
*
|
|
31
|
+
* It is deliberately conservative, not a proof. Two classes are compared on a
|
|
32
|
+
* fixed sample of characters (Latin text plus a few other scripts), so a class
|
|
33
|
+
* that only overlaps outside the sample looks disjoint, and a back-reference
|
|
34
|
+
* is assumed to overlap with everything. A hard guarantee would need a time
|
|
35
|
+
* budget around the match itself (a `node:vm` timeout or a worker), which a
|
|
36
|
+
* synchronous `String.replace` cannot offer; that is a follow-up.
|
|
37
|
+
*/
|
|
38
|
+
/** Longest pattern accepted, in characters. */
|
|
39
|
+
export declare const MAX_REGEX_PATTERN_LENGTH = 1000;
|
|
40
|
+
/**
|
|
41
|
+
* Decide whether a caller-supplied pattern may be compiled and run.
|
|
42
|
+
*
|
|
43
|
+
* Total: it returns a verdict for any string and never throws, so a caller can
|
|
44
|
+
* skip a refused pattern the same way it skips an invalid one.
|
|
45
|
+
*/
|
|
46
|
+
export declare function vetRegexPattern(pattern: string): RegexPatternVerdict;
|