@assistant-ui/react-devtools 1.2.20 → 1.2.21

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.
Files changed (213) hide show
  1. package/dist/DevToolsModal.d.ts +8 -10
  2. package/dist/DevToolsModal.d.ts.map +1 -1
  3. package/dist/DevToolsModalImpl.d.ts +3 -5
  4. package/dist/DevToolsModalImpl.d.ts.map +1 -1
  5. package/dist/data/createInProcessClient.d.ts +1 -3
  6. package/dist/data/createInProcessClient.d.ts.map +1 -1
  7. package/dist/data/projectApi.d.ts +5 -6
  8. package/dist/data/projectApi.d.ts.map +1 -1
  9. package/dist/data/projectApi.js +2 -2
  10. package/dist/data/projectApi.js.map +1 -1
  11. package/dist/data/types.d.ts +28 -30
  12. package/dist/data/types.d.ts.map +1 -1
  13. package/dist/data/useDevToolsClient.d.ts +5 -7
  14. package/dist/data/useDevToolsClient.d.ts.map +1 -1
  15. package/dist/index.d.ts +10 -10
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/shell/DevToolsOverlay.d.ts +6 -8
  18. package/dist/shell/DevToolsOverlay.d.ts.map +1 -1
  19. package/dist/shell/DevToolsPanel.d.ts +11 -13
  20. package/dist/shell/DevToolsPanel.d.ts.map +1 -1
  21. package/dist/shell/ShadowRoot.d.ts +6 -8
  22. package/dist/shell/ShadowRoot.d.ts.map +1 -1
  23. package/dist/shell/SplitLayout.d.ts +11 -13
  24. package/dist/shell/SplitLayout.d.ts.map +1 -1
  25. package/dist/shell/registry.d.ts +30 -32
  26. package/dist/shell/registry.d.ts.map +1 -1
  27. package/dist/shell/tabs/ActivityTab.d.ts +2 -4
  28. package/dist/shell/tabs/ActivityTab.d.ts.map +1 -1
  29. package/dist/shell/tabs/ContextTab.d.ts +2 -4
  30. package/dist/shell/tabs/ContextTab.d.ts.map +1 -1
  31. package/dist/shell/tabs/RawTab.d.ts +2 -4
  32. package/dist/shell/tabs/RawTab.d.ts.map +1 -1
  33. package/dist/shell/tabs/ThreadTab.d.ts +2 -4
  34. package/dist/shell/tabs/ThreadTab.d.ts.map +1 -1
  35. package/dist/shell/tabs/navSelection.d.ts +0 -2
  36. package/dist/shell/tabs/navSelection.d.ts.map +1 -1
  37. package/dist/styles/panel.generated.d.ts +0 -2
  38. package/dist/styles/panel.generated.d.ts.map +1 -1
  39. package/dist/types.d.ts +5 -7
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/utils/common.d.ts +0 -2
  42. package/dist/utils/common.d.ts.map +1 -1
  43. package/dist/utils/diff.d.ts +4 -6
  44. package/dist/utils/diff.d.ts.map +1 -1
  45. package/dist/utils/serialization.d.ts +2 -4
  46. package/dist/utils/serialization.d.ts.map +1 -1
  47. package/dist/utils/serialization.js +17 -0
  48. package/dist/utils/serialization.js.map +1 -1
  49. package/dist/utils/toolNormalization.d.ts +12 -14
  50. package/dist/utils/toolNormalization.d.ts.map +1 -1
  51. package/dist/utils/unserializable.d.ts +0 -2
  52. package/dist/utils/unserializable.d.ts.map +1 -1
  53. package/dist/views/activity/EventLogRow.d.ts +12 -14
  54. package/dist/views/activity/EventLogRow.d.ts.map +1 -1
  55. package/dist/views/activity/EventStreamPane.d.ts +3 -5
  56. package/dist/views/activity/EventStreamPane.d.ts.map +1 -1
  57. package/dist/views/activity/OrphansPane.d.ts +3 -5
  58. package/dist/views/activity/OrphansPane.d.ts.map +1 -1
  59. package/dist/views/activity/RunDetailPane.d.ts +2 -4
  60. package/dist/views/activity/RunDetailPane.d.ts.map +1 -1
  61. package/dist/views/activity/activityNodes.d.ts +24 -26
  62. package/dist/views/activity/activityNodes.d.ts.map +1 -1
  63. package/dist/views/activity/index.d.ts +7 -6
  64. package/dist/views/activity/index.d.ts.map +1 -0
  65. package/dist/views/attachments/AttachmentList.d.ts +5 -7
  66. package/dist/views/attachments/AttachmentList.d.ts.map +1 -1
  67. package/dist/views/attachments/MediaPartView.d.ts +10 -12
  68. package/dist/views/attachments/MediaPartView.d.ts.map +1 -1
  69. package/dist/views/attachments/formatBytes.d.ts +0 -2
  70. package/dist/views/attachments/formatBytes.d.ts.map +1 -1
  71. package/dist/views/attachments/index.d.ts +5 -5
  72. package/dist/views/attachments/index.d.ts.map +1 -0
  73. package/dist/views/attachments/parseAttachment.d.ts +1 -3
  74. package/dist/views/attachments/parseAttachment.d.ts.map +1 -1
  75. package/dist/views/attachments/types.d.ts +10 -12
  76. package/dist/views/attachments/types.d.ts.map +1 -1
  77. package/dist/views/context/ContextDetail.d.ts +5 -7
  78. package/dist/views/context/ContextDetail.d.ts.map +1 -1
  79. package/dist/views/context/ToolUIsView.d.ts +1 -3
  80. package/dist/views/context/ToolUIsView.d.ts.map +1 -1
  81. package/dist/views/context/contextNodes.d.ts +17 -19
  82. package/dist/views/context/contextNodes.d.ts.map +1 -1
  83. package/dist/views/context/index.d.ts +5 -4
  84. package/dist/views/context/index.d.ts.map +1 -0
  85. package/dist/views/mcp/McpView.d.ts +3 -5
  86. package/dist/views/mcp/McpView.d.ts.map +1 -1
  87. package/dist/views/mcp/index.d.ts +2 -2
  88. package/dist/views/mcp/index.d.ts.map +1 -0
  89. package/dist/views/mcp/parse.d.ts +1 -3
  90. package/dist/views/mcp/parse.d.ts.map +1 -1
  91. package/dist/views/mcp/types.d.ts +12 -14
  92. package/dist/views/mcp/types.d.ts.map +1 -1
  93. package/dist/views/message/ChainOfThought.d.ts +3 -5
  94. package/dist/views/message/ChainOfThought.d.ts.map +1 -1
  95. package/dist/views/message/MessageDetailPane.d.ts +2 -4
  96. package/dist/views/message/MessageDetailPane.d.ts.map +1 -1
  97. package/dist/views/message/MessageItem.d.ts +5 -7
  98. package/dist/views/message/MessageItem.d.ts.map +1 -1
  99. package/dist/views/message/MessageList.d.ts +4 -6
  100. package/dist/views/message/MessageList.d.ts.map +1 -1
  101. package/dist/views/message/MessageMetrics.d.ts +2 -4
  102. package/dist/views/message/MessageMetrics.d.ts.map +1 -1
  103. package/dist/views/message/MessageTimeline.d.ts +5 -7
  104. package/dist/views/message/MessageTimeline.d.ts.map +1 -1
  105. package/dist/views/message/PartDisclosure.d.ts +6 -8
  106. package/dist/views/message/PartDisclosure.d.ts.map +1 -1
  107. package/dist/views/message/PartView.d.ts +4 -6
  108. package/dist/views/message/PartView.d.ts.map +1 -1
  109. package/dist/views/message/RoleLabel.d.ts +3 -5
  110. package/dist/views/message/RoleLabel.d.ts.map +1 -1
  111. package/dist/views/message/StatusBadge.d.ts +7 -9
  112. package/dist/views/message/StatusBadge.d.ts.map +1 -1
  113. package/dist/views/message/ToolCallView.d.ts +4 -6
  114. package/dist/views/message/ToolCallView.d.ts.map +1 -1
  115. package/dist/views/message/TtftBar.d.ts +2 -4
  116. package/dist/views/message/TtftBar.d.ts.map +1 -1
  117. package/dist/views/message/index.d.ts +6 -6
  118. package/dist/views/message/index.d.ts.map +1 -0
  119. package/dist/views/message/messagePlainText.d.ts +1 -3
  120. package/dist/views/message/messagePlainText.d.ts.map +1 -1
  121. package/dist/views/message/parse.d.ts +1 -3
  122. package/dist/views/message/parse.d.ts.map +1 -1
  123. package/dist/views/message/types.d.ts +76 -77
  124. package/dist/views/message/types.d.ts.map +1 -1
  125. package/dist/views/model-context/ModelContextView.d.ts +6 -8
  126. package/dist/views/model-context/ModelContextView.d.ts.map +1 -1
  127. package/dist/views/nav.d.ts +6 -8
  128. package/dist/views/nav.d.ts.map +1 -1
  129. package/dist/views/raw/RawDetail.d.ts +5 -7
  130. package/dist/views/raw/RawDetail.d.ts.map +1 -1
  131. package/dist/views/raw/index.d.ts +4 -3
  132. package/dist/views/raw/index.d.ts.map +1 -0
  133. package/dist/views/raw/rawNodes.d.ts +9 -11
  134. package/dist/views/raw/rawNodes.d.ts.map +1 -1
  135. package/dist/views/raw/statePreview.d.ts +0 -2
  136. package/dist/views/raw/statePreview.d.ts.map +1 -1
  137. package/dist/views/runs/PayloadDiff.d.ts +3 -5
  138. package/dist/views/runs/PayloadDiff.d.ts.map +1 -1
  139. package/dist/views/runs/parse.d.ts +2 -4
  140. package/dist/views/runs/parse.d.ts.map +1 -1
  141. package/dist/views/runs/types.d.ts +15 -17
  142. package/dist/views/runs/types.d.ts.map +1 -1
  143. package/dist/views/scopes/ScopesView.d.ts +1 -3
  144. package/dist/views/scopes/ScopesView.d.ts.map +1 -1
  145. package/dist/views/scopes/index.d.ts +2 -2
  146. package/dist/views/scopes/index.d.ts.map +1 -0
  147. package/dist/views/scopes/parse.d.ts +1 -3
  148. package/dist/views/scopes/parse.d.ts.map +1 -1
  149. package/dist/views/scopes/types.d.ts +4 -6
  150. package/dist/views/scopes/types.d.ts.map +1 -1
  151. package/dist/views/thread/ComposerAttachments.d.ts +3 -5
  152. package/dist/views/thread/ComposerAttachments.d.ts.map +1 -1
  153. package/dist/views/thread/ComposerFlags.d.ts +2 -4
  154. package/dist/views/thread/ComposerFlags.d.ts.map +1 -1
  155. package/dist/views/thread/ComposerQueue.d.ts +3 -5
  156. package/dist/views/thread/ComposerQueue.d.ts.map +1 -1
  157. package/dist/views/thread/ConversationList.d.ts +6 -8
  158. package/dist/views/thread/ConversationList.d.ts.map +1 -1
  159. package/dist/views/thread/ConversationStatusDot.d.ts +11 -13
  160. package/dist/views/thread/ConversationStatusDot.d.ts.map +1 -1
  161. package/dist/views/thread/ThreadDetails.d.ts +4 -6
  162. package/dist/views/thread/ThreadDetails.d.ts.map +1 -1
  163. package/dist/views/thread/Transcript.d.ts +7 -9
  164. package/dist/views/thread/Transcript.d.ts.map +1 -1
  165. package/dist/views/thread/index.d.ts +10 -10
  166. package/dist/views/thread/index.d.ts.map +1 -0
  167. package/dist/views/thread/resolve.d.ts +1 -3
  168. package/dist/views/thread/resolve.d.ts.map +1 -1
  169. package/dist/views/thread/types.d.ts +34 -36
  170. package/dist/views/thread/types.d.ts.map +1 -1
  171. package/dist/views/thread/utils.d.ts +1 -3
  172. package/dist/views/thread/utils.d.ts.map +1 -1
  173. package/dist/views/ui/CenteredMessage.d.ts +2 -4
  174. package/dist/views/ui/CenteredMessage.d.ts.map +1 -1
  175. package/dist/views/ui/Chip.d.ts +4 -6
  176. package/dist/views/ui/Chip.d.ts.map +1 -1
  177. package/dist/views/ui/ControlButton.d.ts +1 -3
  178. package/dist/views/ui/ControlButton.d.ts.map +1 -1
  179. package/dist/views/ui/CopyButton.d.ts +7 -9
  180. package/dist/views/ui/CopyButton.d.ts.map +1 -1
  181. package/dist/views/ui/EmptyState.d.ts +2 -4
  182. package/dist/views/ui/EmptyState.d.ts.map +1 -1
  183. package/dist/views/ui/InfoCard.d.ts +6 -8
  184. package/dist/views/ui/InfoCard.d.ts.map +1 -1
  185. package/dist/views/ui/JSONTree.d.ts +4 -6
  186. package/dist/views/ui/JSONTree.d.ts.map +1 -1
  187. package/dist/views/ui/PaneHeader.d.ts +8 -10
  188. package/dist/views/ui/PaneHeader.d.ts.map +1 -1
  189. package/dist/views/ui/RailTime.d.ts +3 -5
  190. package/dist/views/ui/RailTime.d.ts.map +1 -1
  191. package/dist/views/ui/SectionLabel.d.ts +2 -4
  192. package/dist/views/ui/SectionLabel.d.ts.map +1 -1
  193. package/dist/views/ui/SelectableRow.d.ts +7 -9
  194. package/dist/views/ui/SelectableRow.d.ts.map +1 -1
  195. package/dist/views/ui/Sparkline.d.ts +3 -5
  196. package/dist/views/ui/Sparkline.d.ts.map +1 -1
  197. package/dist/views/ui/SummaryItem.d.ts +5 -7
  198. package/dist/views/ui/SummaryItem.d.ts.map +1 -1
  199. package/dist/views/ui/ToneBadge.d.ts +7 -9
  200. package/dist/views/ui/ToneBadge.d.ts.map +1 -1
  201. package/dist/views/ui/badgeTone.d.ts +6 -8
  202. package/dist/views/ui/badgeTone.d.ts.map +1 -1
  203. package/dist/views/ui/getInlineJson.d.ts +0 -2
  204. package/dist/views/ui/getInlineJson.d.ts.map +1 -1
  205. package/dist/views/ui/index.d.ts +17 -16
  206. package/dist/views/ui/index.d.ts.map +1 -0
  207. package/dist/views/ui/useCopyToClipboard.d.ts +4 -6
  208. package/dist/views/ui/useCopyToClipboard.d.ts.map +1 -1
  209. package/package.json +5 -5
  210. package/src/data/projectApi.test.ts +41 -0
  211. package/src/data/projectApi.ts +2 -2
  212. package/src/utils/serialization.test.ts +81 -0
  213. package/src/utils/serialization.ts +36 -0
@@ -1,16 +1,17 @@
1
- import { CenteredMessage } from "./CenteredMessage.js";
2
- import { Chip } from "./Chip.js";
3
- import { ControlButton } from "./ControlButton.js";
4
- import { CopyButton } from "./CopyButton.js";
5
- import { EmptyState } from "./EmptyState.js";
6
- import { InfoCard } from "./InfoCard.js";
7
- import { JSONTree } from "./JSONTree.js";
8
- import { PaneHeader, PaneHeaderStack } from "./PaneHeader.js";
9
- import { RailTime } from "./RailTime.js";
10
- import { SectionLabel } from "./SectionLabel.js";
11
- import { SelectableRow } from "./SelectableRow.js";
12
- import { Sparkline } from "./Sparkline.js";
13
- import { SummaryItem, SummaryList } from "./SummaryItem.js";
14
- import { BadgeTone, CONNECTION_TONE, RAIL_STATUS_BADGE_CLASS, STATUS_TONE } from "./badgeTone.js";
15
- import { ToneBadge } from "./ToneBadge.js";
16
- export { type BadgeTone, CONNECTION_TONE, CenteredMessage, Chip, ControlButton, CopyButton, EmptyState, InfoCard, JSONTree, PaneHeader, PaneHeaderStack, RAIL_STATUS_BADGE_CLASS, RailTime, STATUS_TONE, SectionLabel, SelectableRow, Sparkline, SummaryItem, SummaryList, ToneBadge };
1
+ export { CONNECTION_TONE, RAIL_STATUS_BADGE_CLASS, STATUS_TONE, } from "./badgeTone.js";
2
+ export type { BadgeTone } from "./badgeTone.js";
3
+ export { CenteredMessage } from "./CenteredMessage.js";
4
+ export { Chip } from "./Chip.js";
5
+ export { ControlButton } from "./ControlButton.js";
6
+ export { CopyButton } from "./CopyButton.js";
7
+ export { EmptyState } from "./EmptyState.js";
8
+ export { InfoCard } from "./InfoCard.js";
9
+ export { JSONTree } from "./JSONTree.js";
10
+ export { PaneHeader, PaneHeaderStack } from "./PaneHeader.js";
11
+ export { RailTime } from "./RailTime.js";
12
+ export { SectionLabel } from "./SectionLabel.js";
13
+ export { SelectableRow } from "./SelectableRow.js";
14
+ export { Sparkline } from "./Sparkline.js";
15
+ export { SummaryItem, SummaryList } from "./SummaryItem.js";
16
+ export { ToneBadge } from "./ToneBadge.js";
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/views/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,WAAW,GACZ,uBAAoB;AACrB,YAAY,EAAE,SAAS,EAAE,uBAAoB;AAC7C,OAAO,EAAE,eAAe,EAAE,6BAA0B;AACpD,OAAO,EAAE,IAAI,EAAE,kBAAe;AAC9B,OAAO,EAAE,aAAa,EAAE,2BAAwB;AAChD,OAAO,EAAE,UAAU,EAAE,wBAAqB;AAC1C,OAAO,EAAE,UAAU,EAAE,wBAAqB;AAC1C,OAAO,EAAE,QAAQ,EAAE,sBAAmB;AACtC,OAAO,EAAE,QAAQ,EAAE,sBAAmB;AACtC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,wBAAqB;AAC3D,OAAO,EAAE,QAAQ,EAAE,sBAAmB;AACtC,OAAO,EAAE,YAAY,EAAE,0BAAuB;AAC9C,OAAO,EAAE,aAAa,EAAE,2BAAwB;AAChD,OAAO,EAAE,SAAS,EAAE,uBAAoB;AACxC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,yBAAsB;AACzD,OAAO,EAAE,SAAS,EAAE,uBAAoB"}
@@ -1,9 +1,7 @@
1
- //#region src/views/ui/useCopyToClipboard.d.ts
2
- export declare const useCopyToClipboard: ({ copiedDuration }?: {
3
- copiedDuration?: number;
1
+ export declare const useCopyToClipboard: ({ copiedDuration, }?: {
2
+ copiedDuration?: number;
4
3
  }) => {
5
- isCopied: boolean;
6
- copy: (value: string) => void;
4
+ isCopied: boolean;
5
+ copy: (value: string) => void;
7
6
  };
8
- //#endregion
9
7
  //# sourceMappingURL=useCopyToClipboard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useCopyToClipboard.d.ts","names":[],"sources":["../../../src/views/ui/useCopyToClipboard.ts"],"mappings":";qBAEa,uBAAsB;EAGjC;;;EAKU,OAAA"}
1
+ {"version":3,"file":"useCopyToClipboard.d.ts","sourceRoot":"","sources":["../../../src/views/ui/useCopyToClipboard.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,kBAAkB,GAAI,sBAEhC;IACD,cAAc,CAAC,EAAE,MAAM,CAAC;CACpB;;kBAIM,MAAM;CAgBjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-devtools",
3
- "version": "1.2.20",
3
+ "version": "1.2.21",
4
4
  "description": "React development tools for assistant-ui components",
5
5
  "keywords": [
6
6
  "assistant-ui",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@assistant-ui/react": "^0.15.0",
35
- "@assistant-ui/tap": "^0.9.18",
35
+ "@assistant-ui/tap": "^0.9.19",
36
36
  "@types/react": "*",
37
37
  "@types/react-dom": "*",
38
38
  "react": "^18 || ^19",
@@ -47,9 +47,9 @@
47
47
  }
48
48
  },
49
49
  "devDependencies": {
50
- "@assistant-ui/react": "0.15.21",
51
- "@assistant-ui/tap": "0.9.18",
52
- "@assistant-ui/x-buildutils": "0.0.28",
50
+ "@assistant-ui/react": "0.15.22",
51
+ "@assistant-ui/tap": "0.9.19",
52
+ "@assistant-ui/x-buildutils": "0.0.29",
53
53
  "@assistant-ui/x-performance": "0.0.0",
54
54
  "@tailwindcss/cli": "^4.3.3",
55
55
  "@types/node": "^26.5.1",
@@ -136,4 +136,45 @@ describe("projectApi", () => {
136
136
  const scopes = result.scopes as Array<{ name: string }>;
137
137
  expect(scopes.map((s) => s.name)).toEqual(["broken", "throwing"]);
138
138
  });
139
+
140
+ it("collects snapshots for prototype-named thread ids", () => {
141
+ const threads = scope(
142
+ "root",
143
+ {},
144
+ {
145
+ getState: () => ({
146
+ threadIds: ["__proto__"],
147
+ archivedThreadIds: [],
148
+ }),
149
+ __internal_getAssistantRuntime: () => ({
150
+ threads: {
151
+ getById: () => ({
152
+ getState: () => ({
153
+ messages: [],
154
+ suggestions: [],
155
+ capabilities: {},
156
+ }),
157
+ composer: { getState: () => ({ text: "" }) },
158
+ }),
159
+ },
160
+ }),
161
+ },
162
+ );
163
+
164
+ const projected = projectApi(1, {
165
+ api: { threads },
166
+ logs: [],
167
+ } as unknown as Parameters<typeof projectApi>[1]);
168
+
169
+ expect(Object.hasOwn(projected.threadSnapshots ?? {}, "__proto__")).toBe(
170
+ true,
171
+ );
172
+ expect(Object.getPrototypeOf(projected.threadSnapshots)).toBe(
173
+ Object.prototype,
174
+ );
175
+ expect(projected.threadSnapshots?.["__proto__"]).toMatchObject({
176
+ messages: [],
177
+ composer: { text: "" },
178
+ });
179
+ });
139
180
  });
@@ -82,7 +82,7 @@ const collectThreadSnapshots = (
82
82
  ];
83
83
  if (threadIds.length === 0) return undefined;
84
84
 
85
- const snapshots: Record<string, unknown> = {};
85
+ const snapshots: Record<string, unknown> = Object.create(null);
86
86
  for (const threadId of threadIds) {
87
87
  try {
88
88
  const threadRuntime = assistantRuntime.threads.getById(threadId);
@@ -101,7 +101,7 @@ const collectThreadSnapshots = (
101
101
  }
102
102
  }
103
103
 
104
- return Object.keys(snapshots).length > 0 ? snapshots : undefined;
104
+ return Object.keys(snapshots).length > 0 ? { ...snapshots } : undefined;
105
105
  };
106
106
 
107
107
  /**
@@ -307,3 +307,84 @@ describe("serializeModelContext", () => {
307
307
  });
308
308
  });
309
309
  });
310
+
311
+ describe("sanitizeForMessage errors", () => {
312
+ it("keeps the name, message and stack an Error hides behind non-enumerable properties", () => {
313
+ const result = sanitizeForMessage(new Error("boom")) as Record<
314
+ string,
315
+ unknown
316
+ >;
317
+
318
+ expect(result["name"]).toBe("Error");
319
+ expect(result["message"]).toBe("boom");
320
+ expect(typeof result["stack"]).toBe("string");
321
+ });
322
+
323
+ it("keeps custom fields and the error subclass name", () => {
324
+ class HttpError extends Error {
325
+ override name = "HttpError";
326
+ status = 503;
327
+ }
328
+
329
+ const result = sanitizeForMessage(new HttpError("unavailable")) as Record<
330
+ string,
331
+ unknown
332
+ >;
333
+
334
+ expect(result["name"]).toBe("HttpError");
335
+ expect(result["message"]).toBe("unavailable");
336
+ expect(result["status"]).toBe(503);
337
+ });
338
+
339
+ it("serializes a cause chain", () => {
340
+ const result = sanitizeForMessage(
341
+ new Error("outer", { cause: new Error("inner") }),
342
+ ) as Record<string, unknown>;
343
+
344
+ expect(result["cause"]).toMatchObject({ name: "Error", message: "inner" });
345
+ });
346
+
347
+ it("serializes an error nested in a value", () => {
348
+ const result = sanitizeForMessage({
349
+ status: { reason: "error", error: new Error("nested") },
350
+ }) as { status: { error: Record<string, unknown> } };
351
+
352
+ expect(result.status.error["message"]).toBe("nested");
353
+ });
354
+
355
+ it("sanitizes non-JSON values assigned to name, message or stack", () => {
356
+ const error = new Error("boom");
357
+ Object.defineProperty(error, "name", { value: Symbol("weird") });
358
+ Object.defineProperty(error, "message", { value: 10n });
359
+ Object.defineProperty(error, "stack", { value: { nested: 1n } });
360
+
361
+ const result = sanitizeForMessage(error) as Record<string, unknown>;
362
+
363
+ expect(result["name"]).toBe("Symbol(weird)");
364
+ expect(result["message"]).toBe("10");
365
+ expect(result["stack"]).toEqual({ nested: "1" });
366
+ expect(() => JSON.stringify(result)).not.toThrow();
367
+ });
368
+
369
+ it("serializes an error from another realm", () => {
370
+ // a cross-realm Error fails instanceof; both it and this stand-in answer
371
+ // the Object.prototype.toString brand check the same way
372
+ // a real cross-realm Error keeps name/message non-enumerable, so only the
373
+ // brand check can surface them; both answer Object.prototype.toString alike
374
+ const foreign = { [Symbol.toStringTag]: "Error" };
375
+ Object.defineProperty(foreign, "name", { value: "ForeignError" });
376
+ Object.defineProperty(foreign, "message", { value: "from an iframe" });
377
+
378
+ const result = sanitizeForMessage(foreign) as Record<string, unknown>;
379
+
380
+ expect(result["name"]).toBe("ForeignError");
381
+ expect(result["message"]).toBe("from an iframe");
382
+ });
383
+
384
+ it("does not recurse forever on a self-referencing cause", () => {
385
+ const error = new Error("loop") as Error & { cause?: unknown };
386
+ error.cause = error;
387
+
388
+ expect(() => sanitizeForMessage(error)).not.toThrow();
389
+ });
390
+ });
@@ -20,6 +20,12 @@ const setOwnProperty = (
20
20
  }
21
21
  };
22
22
 
23
+ // An Error from an iframe or worker fails `instanceof`, so the brand check is
24
+ // what keeps a cross-realm error from serializing as an empty object.
25
+ const isErrorLike = (value: object): boolean =>
26
+ value instanceof Error ||
27
+ Object.prototype.toString.call(value) === "[object Error]";
28
+
23
29
  export const sanitizeForMessage = (
24
30
  value: unknown,
25
31
  seen = new WeakSet<object>(),
@@ -99,6 +105,36 @@ export const sanitizeForMessage = (
99
105
  return result;
100
106
  }
101
107
 
108
+ // `name`, `message` and `stack` are not enumerable, so the branch below
109
+ // would render every Error as an empty object. Each is sanitized like any
110
+ // other value, since an Error carries whatever its author assigned.
111
+ if (isErrorLike(value)) {
112
+ const error: Record<string, unknown> = {
113
+ name: sanitizeForMessage(readProperty(value, "name"), seen),
114
+ message: sanitizeForMessage(readProperty(value, "message"), seen),
115
+ };
116
+ const stack = readProperty(value, "stack");
117
+ if (stack !== undefined) {
118
+ error["stack"] = sanitizeForMessage(stack, seen);
119
+ }
120
+ const cause = readProperty(value, "cause");
121
+ if (cause !== undefined) {
122
+ error["cause"] = sanitizeForMessage(cause, seen);
123
+ }
124
+ for (const key of Object.keys(value)) {
125
+ try {
126
+ setOwnProperty(
127
+ error,
128
+ key,
129
+ sanitizeForMessage(readProperty(value, key), seen),
130
+ );
131
+ } catch {
132
+ setOwnProperty(error, key, UNSERIALIZABLE);
133
+ }
134
+ }
135
+ return error;
136
+ }
137
+
102
138
  const result: Record<string, unknown> = {};
103
139
  for (const key of Object.keys(value)) {
104
140
  try {