resafe 1.0.2 → 1.0.3

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/dist/index.cjs CHANGED
@@ -321,9 +321,21 @@ var Formatter = class _Formatter {
321
321
  red() {
322
322
  return this.code("38;2;255;85;85");
323
323
  }
324
+ yellow() {
325
+ return this.code("38;2;255;200;50");
326
+ }
327
+ blue() {
328
+ return this.code("38;2;100;149;237");
329
+ }
330
+ green() {
331
+ return this.code("38;2;85;255;85");
332
+ }
324
333
  pastelRedBg() {
325
334
  return this.code("48;2;255;85;85");
326
335
  }
336
+ pastelYellowBg() {
337
+ return this.code("48;2;255;200;50");
338
+ }
327
339
  toString() {
328
340
  return `${this.parts}${this.text}${ESC}0m`;
329
341
  }
@@ -332,53 +344,82 @@ var Formatter = class _Formatter {
332
344
  }
333
345
  };
334
346
  var fmt = (text) => Formatter.create(text);
335
- var log = {
336
- error: (msg, regex, extra) => {
337
- let firstLine = `${fmt(" RESAFE ").bold().pastelRedBg().white()} ${fmt(msg).white()}`;
338
- if (regex) {
339
- firstLine += ` ${fmt("regex").red()}${fmt("=").darkGray()}${fmt(regex).white()}`;
340
- }
341
- import_node_process.stdout.write(`${firstLine}
342
- `);
343
- if (extra) log.quote(extra);
344
- },
345
- warn: (msg, regex, extra) => {
346
- let firstLine = `${fmt(" RESAFE ").bold().pastelRedBg().white()} ${fmt(msg).white()}`;
347
- if (regex) {
348
- firstLine += ` ${fmt("regex").red()}${fmt("=").darkGray()}${fmt(regex).white()}`;
349
- }
350
- import_node_process.stdout.write(`${firstLine}
347
+ function formatLogLine(prefixBg, msg, options) {
348
+ const prefix = prefixBg(fmt(" RESAFE ").bold()).white();
349
+ let line = `${prefix} ${fmt(msg).white()}`;
350
+ if (options?.property) {
351
+ const prop = options.property;
352
+ const nameFmt = prop.color ? prop.color(fmt(prop.name)) : fmt(prop.name).bold();
353
+ line += ` ${nameFmt}${fmt("=").darkGray()}${fmt(prop.value).white()}`;
354
+ }
355
+ import_node_process.stdout.write(`${line}
351
356
  `);
352
- if (extra) log.quote(extra);
353
- },
354
- hint: (msg) => {
355
- const lines = Array.isArray(msg) ? msg : [msg];
356
- log.quote(lines);
357
- },
358
- quote: (lines) => {
359
- lines.forEach((line) => {
360
- import_node_process.stdout.write(` ${fmt("\u2502").darkGray()} ${fmt(line).white()}
357
+ if (options?.lines) {
358
+ for (const l of options.lines) {
359
+ import_node_process.stdout.write(` ${fmt("\u2502").darkGray()} ${fmt(l).white()}
361
360
  `);
362
- });
361
+ }
363
362
  import_node_process.stdout.write("\n");
364
363
  }
364
+ }
365
+ var log = {
366
+ error: (msg, options) => formatLogLine((f) => f.pastelRedBg(), msg, options),
367
+ warn: (msg, options) => formatLogLine((f) => f.pastelYellowBg(), msg, options)
365
368
  };
366
369
 
367
370
  // src/index.ts
368
371
  function check(regex, options = {}) {
372
+ if (regex == null || regex === "") {
373
+ const err = new Error("Empty regex. Provide a valid pattern.");
374
+ log.warn("Empty regex!", {
375
+ lines: ["? Provide a valid regex."]
376
+ });
377
+ if (options.throwErr) throw err;
378
+ return null;
379
+ }
380
+ if (typeof regex !== "string" && !(regex instanceof RegExp)) {
381
+ const err = new TypeError("Regex must be string or RegExp");
382
+ log.error("Invalid regex!", {
383
+ property: { name: "type", value: typeof regex }
384
+ });
385
+ throw err;
386
+ }
369
387
  const pattern = typeof regex === "string" ? regex : regex.source;
388
+ try {
389
+ new RegExp(pattern);
390
+ } catch (err) {
391
+ if (err instanceof SyntaxError) {
392
+ log.error("Invalid regex syntax!", {
393
+ lines: [`? ${err.message}`]
394
+ });
395
+ if (options.throwErr) throw err;
396
+ return null;
397
+ }
398
+ throw err;
399
+ }
400
+ if (/\\u\{[0-9A-Fa-f]+\}/.test(pattern)) {
401
+ log.warn("Unicode escape sequences may not be fully supported", {
402
+ property: { name: "pattern", value: pattern }
403
+ });
404
+ }
370
405
  const result = analyze(pattern, options);
371
406
  const radius = Number(result.radius.toFixed(4));
372
- if (!result.safe && !options.silent) {
373
- log.error("Unsafe Regex!", `/${pattern}/`, [
374
- `Spectral radius: ${radius} (threshold: ${options.threshold ?? 1})`,
375
- "? Consider simplifying quantifiers"
376
- ]);
377
- }
378
- if (!result.safe && options.throwErr) {
379
- throw new Error(
380
- `Unsafe regex (spectral radius ${radius})`
381
- );
407
+ if (!result.safe) {
408
+ const err = new Error(`Unsafe regex (spectral radius ${radius})`);
409
+ if (!options.silent) {
410
+ log.error("Unsafe Regex!", {
411
+ property: {
412
+ name: "regex",
413
+ value: `/${pattern}/`,
414
+ color: (f) => f.red()
415
+ },
416
+ lines: [
417
+ `Spectral radius: ${radius} (threshold: ${options.threshold ?? 1})`,
418
+ "? Consider simplifying quantifiers"
419
+ ]
420
+ });
421
+ }
422
+ if (options.throwErr) throw err;
382
423
  }
383
424
  return result;
384
425
  }
package/dist/index.d.cts CHANGED
@@ -10,7 +10,7 @@ interface Options extends Config {
10
10
  silent?: boolean;
11
11
  throwErr?: boolean;
12
12
  }
13
- declare function check(regex: string | RegExp, options?: Options): Result;
14
- declare function checkAsync(regex: string | RegExp, options?: Options): Promise<Result>;
13
+ declare function check(regex: string | RegExp, options?: Options): Result | null;
14
+ declare function checkAsync(regex: string | RegExp, options?: Options): Promise<Result | null>;
15
15
 
16
16
  export { type Config, type Options, type Result, check, checkAsync };
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ interface Options extends Config {
10
10
  silent?: boolean;
11
11
  throwErr?: boolean;
12
12
  }
13
- declare function check(regex: string | RegExp, options?: Options): Result;
14
- declare function checkAsync(regex: string | RegExp, options?: Options): Promise<Result>;
13
+ declare function check(regex: string | RegExp, options?: Options): Result | null;
14
+ declare function checkAsync(regex: string | RegExp, options?: Options): Promise<Result | null>;
15
15
 
16
16
  export { type Config, type Options, type Result, check, checkAsync };
package/dist/index.js CHANGED
@@ -294,9 +294,21 @@ var Formatter = class _Formatter {
294
294
  red() {
295
295
  return this.code("38;2;255;85;85");
296
296
  }
297
+ yellow() {
298
+ return this.code("38;2;255;200;50");
299
+ }
300
+ blue() {
301
+ return this.code("38;2;100;149;237");
302
+ }
303
+ green() {
304
+ return this.code("38;2;85;255;85");
305
+ }
297
306
  pastelRedBg() {
298
307
  return this.code("48;2;255;85;85");
299
308
  }
309
+ pastelYellowBg() {
310
+ return this.code("48;2;255;200;50");
311
+ }
300
312
  toString() {
301
313
  return `${this.parts}${this.text}${ESC}0m`;
302
314
  }
@@ -305,53 +317,82 @@ var Formatter = class _Formatter {
305
317
  }
306
318
  };
307
319
  var fmt = (text) => Formatter.create(text);
308
- var log = {
309
- error: (msg, regex, extra) => {
310
- let firstLine = `${fmt(" RESAFE ").bold().pastelRedBg().white()} ${fmt(msg).white()}`;
311
- if (regex) {
312
- firstLine += ` ${fmt("regex").red()}${fmt("=").darkGray()}${fmt(regex).white()}`;
313
- }
314
- stdout.write(`${firstLine}
315
- `);
316
- if (extra) log.quote(extra);
317
- },
318
- warn: (msg, regex, extra) => {
319
- let firstLine = `${fmt(" RESAFE ").bold().pastelRedBg().white()} ${fmt(msg).white()}`;
320
- if (regex) {
321
- firstLine += ` ${fmt("regex").red()}${fmt("=").darkGray()}${fmt(regex).white()}`;
322
- }
323
- stdout.write(`${firstLine}
320
+ function formatLogLine(prefixBg, msg, options) {
321
+ const prefix = prefixBg(fmt(" RESAFE ").bold()).white();
322
+ let line = `${prefix} ${fmt(msg).white()}`;
323
+ if (options?.property) {
324
+ const prop = options.property;
325
+ const nameFmt = prop.color ? prop.color(fmt(prop.name)) : fmt(prop.name).bold();
326
+ line += ` ${nameFmt}${fmt("=").darkGray()}${fmt(prop.value).white()}`;
327
+ }
328
+ stdout.write(`${line}
324
329
  `);
325
- if (extra) log.quote(extra);
326
- },
327
- hint: (msg) => {
328
- const lines = Array.isArray(msg) ? msg : [msg];
329
- log.quote(lines);
330
- },
331
- quote: (lines) => {
332
- lines.forEach((line) => {
333
- stdout.write(` ${fmt("\u2502").darkGray()} ${fmt(line).white()}
330
+ if (options?.lines) {
331
+ for (const l of options.lines) {
332
+ stdout.write(` ${fmt("\u2502").darkGray()} ${fmt(l).white()}
334
333
  `);
335
- });
334
+ }
336
335
  stdout.write("\n");
337
336
  }
337
+ }
338
+ var log = {
339
+ error: (msg, options) => formatLogLine((f) => f.pastelRedBg(), msg, options),
340
+ warn: (msg, options) => formatLogLine((f) => f.pastelYellowBg(), msg, options)
338
341
  };
339
342
 
340
343
  // src/index.ts
341
344
  function check(regex, options = {}) {
345
+ if (regex == null || regex === "") {
346
+ const err = new Error("Empty regex. Provide a valid pattern.");
347
+ log.warn("Empty regex!", {
348
+ lines: ["? Provide a valid regex."]
349
+ });
350
+ if (options.throwErr) throw err;
351
+ return null;
352
+ }
353
+ if (typeof regex !== "string" && !(regex instanceof RegExp)) {
354
+ const err = new TypeError("Regex must be string or RegExp");
355
+ log.error("Invalid regex!", {
356
+ property: { name: "type", value: typeof regex }
357
+ });
358
+ throw err;
359
+ }
342
360
  const pattern = typeof regex === "string" ? regex : regex.source;
361
+ try {
362
+ new RegExp(pattern);
363
+ } catch (err) {
364
+ if (err instanceof SyntaxError) {
365
+ log.error("Invalid regex syntax!", {
366
+ lines: [`? ${err.message}`]
367
+ });
368
+ if (options.throwErr) throw err;
369
+ return null;
370
+ }
371
+ throw err;
372
+ }
373
+ if (/\\u\{[0-9A-Fa-f]+\}/.test(pattern)) {
374
+ log.warn("Unicode escape sequences may not be fully supported", {
375
+ property: { name: "pattern", value: pattern }
376
+ });
377
+ }
343
378
  const result = analyze(pattern, options);
344
379
  const radius = Number(result.radius.toFixed(4));
345
- if (!result.safe && !options.silent) {
346
- log.error("Unsafe Regex!", `/${pattern}/`, [
347
- `Spectral radius: ${radius} (threshold: ${options.threshold ?? 1})`,
348
- "? Consider simplifying quantifiers"
349
- ]);
350
- }
351
- if (!result.safe && options.throwErr) {
352
- throw new Error(
353
- `Unsafe regex (spectral radius ${radius})`
354
- );
380
+ if (!result.safe) {
381
+ const err = new Error(`Unsafe regex (spectral radius ${radius})`);
382
+ if (!options.silent) {
383
+ log.error("Unsafe Regex!", {
384
+ property: {
385
+ name: "regex",
386
+ value: `/${pattern}/`,
387
+ color: (f) => f.red()
388
+ },
389
+ lines: [
390
+ `Spectral radius: ${radius} (threshold: ${options.threshold ?? 1})`,
391
+ "? Consider simplifying quantifiers"
392
+ ]
393
+ });
394
+ }
395
+ if (options.throwErr) throw err;
355
396
  }
356
397
  return result;
357
398
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resafe",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "🛡️ Detects ReDoS vulnerabilities in regexes using Thompson NFA construction and spectral radius analysis",
5
5
  "license": "MIT",
6
6
  "homepage": "https://resafe.js.org",