@exodus/errors 3.3.0 → 3.5.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 CHANGED
@@ -3,6 +3,20 @@
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
+ ## [3.5.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.4.0...@exodus/errors@3.5.0) (2025-11-04)
7
+
8
+ ### Features
9
+
10
+ - feat: allow exporting `traceId` in errors context (#14099)
11
+
12
+ - feat: handle `cause` properties of errors (#14307)
13
+
14
+ ## [3.4.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.3.0...@exodus/errors@3.4.0) (2025-10-14)
15
+
16
+ ### Features
17
+
18
+ - feat: support capturing error context (#13933)
19
+
6
20
  ## [3.3.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.2.0...@exodus/errors@3.3.0) (2025-08-08)
7
21
 
8
22
  ### Features
package/lib/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { default as sanitizeErrorMessage } from './sanitize.js';
2
2
  export { default as parseStackTrace, captureStackTrace } from './stack.js';
3
3
  export * from './safe-error.js';
4
+ export { SafeContext, type SafeContextType } from './safe-context/index.js';
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { default as sanitizeErrorMessage } from './sanitize.js';
2
2
  export { default as parseStackTrace, captureStackTrace } from './stack.js';
3
3
  export * from './safe-error.js';
4
+ export { SafeContext } from './safe-context/index.js';
@@ -0,0 +1,30 @@
1
+ import { type SafeContextType } from './schemas.js';
2
+ export declare const SafeContext: {
3
+ parse(unsafeContext: unknown): SafeContextType;
4
+ getSchema: () => ReturnType<() => import("@exodus/zod").ZodOptional<import("@exodus/zod").ZodNullable<import("@exodus/zod").ZodObject<{
5
+ navigation: import("@exodus/zod").ZodOptional<import("@exodus/zod").ZodNullable<import("@exodus/zod").ZodObject<{
6
+ currentRouteName: import("@exodus/zod").ZodEffects<import("@exodus/zod").ZodString, string, string>;
7
+ previousRouteName: import("@exodus/zod").ZodOptional<import("@exodus/zod").ZodNullable<import("@exodus/zod").ZodEffects<import("@exodus/zod").ZodString, string, string>>>;
8
+ }, "strict", import("@exodus/zod").ZodTypeAny, {
9
+ currentRouteName: string;
10
+ previousRouteName?: string | null | undefined;
11
+ }, {
12
+ currentRouteName: string;
13
+ previousRouteName?: string | null | undefined;
14
+ }>>>;
15
+ traceId: import("@exodus/zod").ZodOptional<import("@exodus/zod").ZodNullable<import("@exodus/zod").ZodEffects<import("@exodus/zod").ZodString, string, string>>>;
16
+ }, "strict", import("@exodus/zod").ZodTypeAny, {
17
+ navigation?: {
18
+ currentRouteName: string;
19
+ previousRouteName?: string | null | undefined;
20
+ } | null | undefined;
21
+ traceId?: string | null | undefined;
22
+ }, {
23
+ navigation?: {
24
+ currentRouteName: string;
25
+ previousRouteName?: string | null | undefined;
26
+ } | null | undefined;
27
+ traceId?: string | null | undefined;
28
+ }>>>>;
29
+ };
30
+ export type { SafeContextType } from './schemas.js';
@@ -0,0 +1,12 @@
1
+ import { getContextSchema } from './schemas.js';
2
+ export const SafeContext = {
3
+ parse(unsafeContext) {
4
+ try {
5
+ return getContextSchema().parse(unsafeContext);
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ },
11
+ getSchema: getContextSchema,
12
+ };
@@ -0,0 +1,29 @@
1
+ import { z } from '@exodus/zod';
2
+ declare const createContextSchema: () => z.ZodOptional<z.ZodNullable<z.ZodObject<{
3
+ navigation: z.ZodOptional<z.ZodNullable<z.ZodObject<{
4
+ currentRouteName: z.ZodEffects<z.ZodString, string, string>;
5
+ previousRouteName: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
6
+ }, "strict", z.ZodTypeAny, {
7
+ currentRouteName: string;
8
+ previousRouteName?: string | null | undefined;
9
+ }, {
10
+ currentRouteName: string;
11
+ previousRouteName?: string | null | undefined;
12
+ }>>>;
13
+ traceId: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
14
+ }, "strict", z.ZodTypeAny, {
15
+ navigation?: {
16
+ currentRouteName: string;
17
+ previousRouteName?: string | null | undefined;
18
+ } | null | undefined;
19
+ traceId?: string | null | undefined;
20
+ }, {
21
+ navigation?: {
22
+ currentRouteName: string;
23
+ previousRouteName?: string | null | undefined;
24
+ } | null | undefined;
25
+ traceId?: string | null | undefined;
26
+ }>>>;
27
+ declare const getContextSchema: () => ReturnType<typeof createContextSchema>;
28
+ export type SafeContextType = z.infer<ReturnType<typeof createContextSchema>>;
29
+ export { getContextSchema };
@@ -0,0 +1,19 @@
1
+ import { z } from '@exodus/zod';
2
+ import { isSafe } from '@exodus/safe-string';
3
+ import { memoize } from '@exodus/basic-utils';
4
+ const createContextSchema = () => z
5
+ .object({
6
+ navigation: z
7
+ .object({
8
+ currentRouteName: z.string().refine(isSafe),
9
+ previousRouteName: z.string().refine(isSafe).nullish(),
10
+ })
11
+ .strict()
12
+ .nullish(),
13
+ traceId: z.string().refine(isSafe).nullish(),
14
+ })
15
+ .strict()
16
+ .nullish();
17
+ // Memoize the factory for lazy loading.
18
+ const getContextSchema = memoize(createContextSchema);
19
+ export { getContextSchema };
@@ -1,5 +1,6 @@
1
1
  import type { Frame } from './types.js';
2
2
  import type { CommonErrorString } from './common-errors.js';
3
+ export declare const MAX_LINKED_ERRORS_DEPTH = 5;
3
4
  type ReadonlySetValues<S> = S extends ReadonlySet<infer T> ? T : never;
4
5
  declare const SAFE_NAMES_SET: ReadonlySet<"Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError" | "SyntaxError">;
5
6
  type StaticAllowlistString = string & {
@@ -13,14 +14,23 @@ export type SafeCode = string & {
13
14
  __branded_type: 'SafeCodeOutcome';
14
15
  };
15
16
  export type SafeString = CommonErrorString | StaticAllowlistString | RuntimeSafeString;
17
+ export type SafeErrorJSON = {
18
+ name: SafeName;
19
+ code?: SafeCode;
20
+ hint?: SafeString;
21
+ stack?: string;
22
+ linkedErrors?: SafeErrorJSON[];
23
+ timestamp: number;
24
+ };
16
25
  type UnknownError = Error & {
17
26
  hint?: unknown;
18
27
  code?: unknown;
28
+ cause?: unknown;
19
29
  };
20
30
  declare const FACTORY_SYMBOL: unique symbol;
21
31
  export declare class SafeError {
22
32
  #private;
23
- static from<T extends UnknownError>(err: T): SafeError;
33
+ static from<T extends UnknownError>(err: T, depth?: number): SafeError;
24
34
  get name(): "Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError" | "SyntaxError";
25
35
  get code(): SafeCode | undefined;
26
36
  get hint(): SafeString | undefined;
@@ -36,19 +46,15 @@ export declare class SafeError {
36
46
  __proto__: null;
37
47
  }[] | undefined;
38
48
  get stack(): string | undefined;
49
+ get linkedErrors(): SafeError[] | undefined;
39
50
  get timestamp(): number;
40
- toJSON(): {
41
- name: SafeName;
42
- code?: SafeCode;
43
- hint?: SafeString;
44
- stack?: string;
45
- timestamp: number;
46
- };
47
- constructor({ name, code, hint, stack, initSymbol, }: {
51
+ toJSON(): SafeErrorJSON;
52
+ constructor({ name, code, hint, stack, linkedErrors, initSymbol, }: {
48
53
  name: SafeName;
49
54
  code?: SafeCode;
50
55
  hint?: SafeString;
51
56
  stack?: Frame[];
57
+ linkedErrors?: SafeError[];
52
58
  initSymbol: typeof FACTORY_SYMBOL;
53
59
  });
54
60
  }
package/lib/safe-error.js CHANGED
@@ -3,6 +3,7 @@ import assert from 'minimalistic-assert';
3
3
  import parseStackTraceNatively, { stackFramesToString } from './stack.js';
4
4
  import { omitUndefined } from './utils.js';
5
5
  import { toCommonErrorMessage } from './common-errors.js';
6
+ export const MAX_LINKED_ERRORS_DEPTH = 5;
6
7
  function makeReadonlySet(values) {
7
8
  return new Set(values);
8
9
  }
@@ -29,12 +30,13 @@ function isSafeCode(value) {
29
30
  const staticAllowlist = getAllowlist();
30
31
  const FACTORY_SYMBOL = Symbol('SafeError');
31
32
  export class SafeError {
32
- static from(err) {
33
+ static from(err, depth = 0) {
33
34
  let safeName;
34
35
  let safeCode;
35
36
  let safeHint;
37
+ let linkedSafeErrors;
36
38
  try {
37
- const { name, message, hint, code } = err;
39
+ const { name, message, hint, code, cause } = err;
38
40
  safeName = isSafeName(name) ? name : 'UnknownError';
39
41
  const safeCodeCandidate = `${code}`;
40
42
  safeCode = isSafeCode(safeCodeCandidate) ? safeCodeCandidate : undefined;
@@ -45,11 +47,27 @@ export class SafeError {
45
47
  staticAllowlist.find((safePrefix) => hintCandidates.some((str) => str.startsWith(safePrefix))) ||
46
48
  toCommonErrorMessage(message);
47
49
  const safeStack = parseStackTraceNatively(err);
50
+ if (cause instanceof Error && depth === 0) {
51
+ linkedSafeErrors = [];
52
+ let currentCause = cause;
53
+ while (currentCause instanceof Error && ++depth <= MAX_LINKED_ERRORS_DEPTH) {
54
+ try {
55
+ linkedSafeErrors.push(SafeError.from(currentCause, depth));
56
+ }
57
+ catch {
58
+ linkedSafeErrors.push(FAILED_TO_PARSE_ERROR);
59
+ }
60
+ finally {
61
+ currentCause = currentCause.cause;
62
+ }
63
+ }
64
+ }
48
65
  return new SafeError({
49
66
  name: safeName,
50
67
  code: safeCode,
51
68
  hint: safeHint,
52
69
  stack: safeStack,
70
+ linkedErrors: linkedSafeErrors,
53
71
  initSymbol: FACTORY_SYMBOL,
54
72
  });
55
73
  }
@@ -60,6 +78,7 @@ export class SafeError {
60
78
  name: safeName,
61
79
  code: safeCode,
62
80
  hint: safeHint,
81
+ linkedErrors: linkedSafeErrors,
63
82
  initSymbol: FACTORY_SYMBOL,
64
83
  })
65
84
  : FAILED_TO_PARSE_ERROR;
@@ -73,6 +92,7 @@ export class SafeError {
73
92
  #code;
74
93
  #hint;
75
94
  #stackFrames;
95
+ #linkedErrors;
76
96
  #timestamp;
77
97
  get name() {
78
98
  return this.#name;
@@ -92,6 +112,9 @@ export class SafeError {
92
112
  ? `${this.name}: ${this.code || this.hint || 'unknownHint'}\n${stackTrace}`
93
113
  : undefined;
94
114
  }
115
+ get linkedErrors() {
116
+ return this.#linkedErrors;
117
+ }
95
118
  get timestamp() {
96
119
  return this.#timestamp;
97
120
  }
@@ -102,15 +125,17 @@ export class SafeError {
102
125
  code: this.code,
103
126
  hint: this.hint,
104
127
  stack: this.stack,
128
+ linkedErrors: this.linkedErrors?.map((linkedError) => linkedError.toJSON()),
105
129
  timestamp: this.timestamp,
106
130
  });
107
131
  }
108
- constructor({ name, code, hint, stack, initSymbol, }) {
132
+ constructor({ name, code, hint, stack, linkedErrors, initSymbol, }) {
109
133
  assert(initSymbol === FACTORY_SYMBOL, 'SafeError: use SafeError.from()');
110
134
  this.#name = name;
111
135
  this.#code = code;
112
136
  this.#hint = hint;
113
137
  this.#stackFrames = stack?.map((frame) => ({ ...frame }));
138
+ this.#linkedErrors = linkedErrors;
114
139
  this.#timestamp = Date.now();
115
140
  }
116
141
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exodus/errors",
3
3
  "type": "module",
4
- "version": "3.3.0",
4
+ "version": "3.5.0",
5
5
  "description": "Utilities for error handling in client code, such as sanitization",
6
6
  "author": "Exodus Movement, Inc.",
7
7
  "repository": {
@@ -34,7 +34,9 @@
34
34
  "test:hermes": "EXODUS_TEST_COVERAGE=0 run -T exodus-test --engine hermes:bundle --esbuild --jest src/__tests__/hermes/*.test.ts src/__tests__/engine-agnostic/*.test.ts"
35
35
  },
36
36
  "dependencies": {
37
+ "@exodus/basic-utils": "^3.0.1",
37
38
  "@exodus/safe-string": "^1.2.0",
39
+ "@exodus/zod": "^3.24.2",
38
40
  "minimalistic-assert": "^1.0.1"
39
41
  },
40
42
  "devDependencies": {
@@ -42,7 +44,8 @@
42
44
  "@types/minimalistic-assert": "^1.0.1"
43
45
  },
44
46
  "publishConfig": {
45
- "access": "public"
47
+ "access": "public",
48
+ "provenance": false
46
49
  },
47
- "gitHead": "15487091c1c6bd495e315ea164df34fb314ee156"
50
+ "gitHead": "32d28daa40c3e8c600440732d2792699448e1be8"
48
51
  }