@exodus/errors 2.1.0 → 2.2.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 -0
- package/lib/common-errors.d.ts +40 -0
- package/lib/common-errors.js +59 -0
- package/lib/safe-error.d.ts +11 -5
- package/lib/safe-error.js +13 -19
- package/lib/stack.js +3 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [2.2.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@2.1.1...@exodus/errors@2.2.0) (2025-05-21)
|
|
7
|
+
|
|
8
|
+
- refactor: pass through safe-strings (#12479)
|
|
9
|
+
|
|
10
|
+
## [2.1.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@2.1.0...@exodus/errors@2.1.1) (2025-05-08)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- fix: remove `void` keyword (#12267)
|
|
15
|
+
|
|
6
16
|
## [2.1.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@2.0.2...@exodus/errors@2.1.0) (2025-04-28)
|
|
7
17
|
|
|
8
18
|
### Features
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
declare const commonErrors: readonly [{
|
|
2
|
+
readonly pattern: RegExp;
|
|
3
|
+
readonly normalized: "Value is not a function";
|
|
4
|
+
}, {
|
|
5
|
+
readonly pattern: RegExp;
|
|
6
|
+
readonly normalized: "Value is not a constructor";
|
|
7
|
+
}, {
|
|
8
|
+
readonly pattern: RegExp;
|
|
9
|
+
readonly normalized: "Cannot convert null to object";
|
|
10
|
+
}, {
|
|
11
|
+
readonly pattern: RegExp;
|
|
12
|
+
readonly normalized: "Cannot convert undefined to object";
|
|
13
|
+
}, {
|
|
14
|
+
readonly pattern: RegExp;
|
|
15
|
+
readonly normalized: "Cannot read property/properties of null";
|
|
16
|
+
}, {
|
|
17
|
+
readonly pattern: RegExp;
|
|
18
|
+
readonly normalized: "Cannot read property/properties of undefined";
|
|
19
|
+
}, {
|
|
20
|
+
readonly pattern: RegExp;
|
|
21
|
+
readonly normalized: "Cannot set property/properties of null";
|
|
22
|
+
}, {
|
|
23
|
+
readonly pattern: RegExp;
|
|
24
|
+
readonly normalized: "Cannot set property/properties of undefined";
|
|
25
|
+
}, {
|
|
26
|
+
readonly pattern: RegExp;
|
|
27
|
+
readonly normalized: "Cannot access property of undefined";
|
|
28
|
+
}, {
|
|
29
|
+
readonly pattern: RegExp;
|
|
30
|
+
readonly normalized: "Cannot access property of null";
|
|
31
|
+
}, {
|
|
32
|
+
readonly pattern: RegExp;
|
|
33
|
+
readonly normalized: "Unexpected token";
|
|
34
|
+
}, {
|
|
35
|
+
readonly pattern: RegExp;
|
|
36
|
+
readonly normalized: "Unexpected end of input";
|
|
37
|
+
}];
|
|
38
|
+
export type CommonErrorString = (typeof commonErrors)[number]['normalized'];
|
|
39
|
+
export declare function toCommonErrorMessage(errorMessage: string): CommonErrorString | undefined;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const commonErrors = [
|
|
2
|
+
{
|
|
3
|
+
pattern: /.* is not a function/iu,
|
|
4
|
+
normalized: 'Value is not a function',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
pattern: /.* is not a constructor/iu,
|
|
8
|
+
normalized: 'Value is not a constructor',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
pattern: /cannot convert null to object/iu,
|
|
12
|
+
normalized: 'Cannot convert null to object',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
pattern: /cannot convert undefined to object/iu,
|
|
16
|
+
normalized: 'Cannot convert undefined to object',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
pattern: /cannot read (property|properties).* of null/iu,
|
|
20
|
+
normalized: 'Cannot read property/properties of null',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: /cannot read (property|properties).* of undefined/iu,
|
|
24
|
+
normalized: 'Cannot read property/properties of undefined',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pattern: /cannot set (property|properties).* of null/iu,
|
|
28
|
+
normalized: 'Cannot set property/properties of null',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /cannot set (property|properties).* of undefined/iu,
|
|
32
|
+
normalized: 'Cannot set property/properties of undefined',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern: /undefined is not an object \(evaluating '.*'\)/iu,
|
|
36
|
+
normalized: 'Cannot access property of undefined',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: /null is not an object \(evaluating '.*'\)/iu,
|
|
40
|
+
normalized: 'Cannot access property of null',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /unexpected (token|character)/iu,
|
|
44
|
+
normalized: 'Unexpected token',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pattern: /unexpected end of( JSON)? input/iu,
|
|
48
|
+
normalized: 'Unexpected end of input',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
export function toCommonErrorMessage(errorMessage) {
|
|
52
|
+
if (!errorMessage)
|
|
53
|
+
return;
|
|
54
|
+
for (const { pattern, normalized } of commonErrors) {
|
|
55
|
+
if (pattern.test(errorMessage)) {
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
package/lib/safe-error.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { Frame } from './types.js';
|
|
2
|
+
import type { CommonErrorString } from './common-errors.js';
|
|
2
3
|
type ReadonlySetValues<S> = S extends ReadonlySet<infer T> ? T : never;
|
|
3
4
|
declare const SAFE_NAMES_SET: ReadonlySet<"Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError">;
|
|
4
|
-
|
|
5
|
+
type StaticAllowlistString = string & {
|
|
6
|
+
__branded_type: 'StaticAllowlistString';
|
|
7
|
+
};
|
|
8
|
+
type RuntimeSafeString = string & {
|
|
9
|
+
__branded_type: 'RuntimeSafeString';
|
|
10
|
+
};
|
|
5
11
|
export type SafeName = ReadonlySetValues<typeof SAFE_NAMES_SET>;
|
|
6
|
-
export type SafeHint = ReadonlySetValues<typeof SAFE_HINTS_SET>;
|
|
7
12
|
export type SafeCode = string & {
|
|
8
13
|
__branded_type: 'SafeCodeOutcome';
|
|
9
14
|
};
|
|
15
|
+
export type SafeString = CommonErrorString | StaticAllowlistString | RuntimeSafeString;
|
|
10
16
|
type UnknownError = Error & {
|
|
11
17
|
hint?: unknown;
|
|
12
18
|
code?: unknown;
|
|
@@ -35,7 +41,7 @@ export declare class SafeError {
|
|
|
35
41
|
static from<T extends UnknownError>(err: T): SafeError;
|
|
36
42
|
get name(): "Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError";
|
|
37
43
|
get code(): SafeCode | undefined;
|
|
38
|
-
get hint():
|
|
44
|
+
get hint(): SafeString | undefined;
|
|
39
45
|
get stackFrames(): {
|
|
40
46
|
function?: string | null;
|
|
41
47
|
method?: string | null;
|
|
@@ -52,14 +58,14 @@ export declare class SafeError {
|
|
|
52
58
|
toJSON(): {
|
|
53
59
|
name: SafeName;
|
|
54
60
|
code?: SafeCode;
|
|
55
|
-
hint?:
|
|
61
|
+
hint?: SafeString;
|
|
56
62
|
stack?: string;
|
|
57
63
|
timestamp: number;
|
|
58
64
|
};
|
|
59
65
|
constructor({ name, code, hint, stack, initSymbol, }: {
|
|
60
66
|
name: SafeName;
|
|
61
67
|
code?: SafeCode;
|
|
62
|
-
hint?:
|
|
68
|
+
hint?: SafeString;
|
|
63
69
|
stack?: Frame[];
|
|
64
70
|
initSymbol: typeof FACTORY_SYMBOL;
|
|
65
71
|
});
|
package/lib/safe-error.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { getAllowlist, isSafe } from '@exodus/safe-string';
|
|
1
2
|
import assert from 'minimalistic-assert';
|
|
2
3
|
import parseStackTraceNatively, { stackFramesToString } from './stack.js';
|
|
3
4
|
import { omitUndefined } from './utils.js';
|
|
5
|
+
import { toCommonErrorMessage } from './common-errors.js';
|
|
4
6
|
function makeReadonlySet(values) {
|
|
5
7
|
return new Set(values);
|
|
6
8
|
}
|
|
@@ -32,24 +34,6 @@ const safeHints = {
|
|
|
32
34
|
failedToParse: 'failed to parse error',
|
|
33
35
|
},
|
|
34
36
|
};
|
|
35
|
-
const SAFE_HINTS_SET = makeReadonlySet(
|
|
36
|
-
// @ts-expect-error doesn't like __proto__: null
|
|
37
|
-
Object.values(safeHints).flatMap((safeHintCategory) => Object.values(safeHintCategory)));
|
|
38
|
-
function getSafeHint(value) {
|
|
39
|
-
if (typeof value !== 'string') {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
if (value === '') {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
if (isSafeHint(value)) {
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
return [...SAFE_HINTS_SET].find((sh) => value.includes(sh));
|
|
49
|
-
}
|
|
50
|
-
function isSafeHint(value) {
|
|
51
|
-
return SAFE_HINTS_SET.has(value);
|
|
52
|
-
}
|
|
53
37
|
function isSafeName(value) {
|
|
54
38
|
return SAFE_NAMES_SET.has(value);
|
|
55
39
|
}
|
|
@@ -59,6 +43,11 @@ function isExodusErrorCode(code) {
|
|
|
59
43
|
function isSafeCode(value) {
|
|
60
44
|
return SAFE_CODES_SET.has(value) || isExodusErrorCode(value);
|
|
61
45
|
}
|
|
46
|
+
const staticAllowlist = getAllowlist();
|
|
47
|
+
function getSafeString(str) {
|
|
48
|
+
if (isSafe(str))
|
|
49
|
+
return str;
|
|
50
|
+
}
|
|
62
51
|
const FACTORY_SYMBOL = Symbol('SafeError');
|
|
63
52
|
export class SafeError {
|
|
64
53
|
static hints = safeHints;
|
|
@@ -71,7 +60,12 @@ export class SafeError {
|
|
|
71
60
|
safeName = isSafeName(name) ? name : 'UnknownError';
|
|
72
61
|
const safeCodeCandidate = `${code}`;
|
|
73
62
|
safeCode = isSafeCode(safeCodeCandidate) ? safeCodeCandidate : undefined;
|
|
74
|
-
|
|
63
|
+
const hintCandidates = [hint, message].filter((str) => typeof str === 'string');
|
|
64
|
+
safeHint =
|
|
65
|
+
// chicken sacrifice to TypeScript, otherwise would be hintCandidates.find((str) => isSafe(str))
|
|
66
|
+
hintCandidates.map((str) => getSafeString(str)).find(Boolean) ||
|
|
67
|
+
staticAllowlist.find((safePrefix) => hintCandidates.some((str) => str.startsWith(safePrefix))) ||
|
|
68
|
+
toCommonErrorMessage(message);
|
|
75
69
|
const safeStack = parseStackTraceNatively(err);
|
|
76
70
|
return new SafeError({
|
|
77
71
|
name: safeName,
|
package/lib/stack.js
CHANGED
|
@@ -25,7 +25,9 @@ export function captureStackTrace(err) {
|
|
|
25
25
|
return err.stack;
|
|
26
26
|
};
|
|
27
27
|
try {
|
|
28
|
-
void
|
|
28
|
+
// do NOT add "void", it will remove the whole line in a tragic transpilation accident
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
30
|
+
err.stack; // trigger prepareStackTrace
|
|
29
31
|
}
|
|
30
32
|
finally {
|
|
31
33
|
Error.prepareStackTrace = originalPrepareStackTrace;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/errors",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"description": "Utilities for error handling in client code, such as sanitization",
|
|
6
6
|
"author": "Exodus Movement, Inc.",
|
|
7
7
|
"repository": {
|
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
"test:hermes": "EXODUS_TEST_COVERAGE=0 run -T exodus-test --engine hermes:bundle --esbuild --jest src/__tests__/hermes/*.test.ts"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@exodus/safe-string": "^1.0.0",
|
|
37
38
|
"minimalistic-assert": "^1.0.1"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@exodus/errors-fixture": "^1.0.0",
|
|
41
|
-
"@types/minimalistic-assert": "^1.0.1"
|
|
42
|
-
"hermes-engine-cli": "^0.12.0"
|
|
42
|
+
"@types/minimalistic-assert": "^1.0.1"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "30ebd4bfc064662078e5819e335a3d658f00bd1b"
|
|
48
48
|
}
|