@autumnsgrove/groveengine 0.6.1 → 0.6.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.
Files changed (77) hide show
  1. package/dist/auth/jwt.d.ts +10 -4
  2. package/dist/auth/jwt.js +18 -4
  3. package/dist/auth/session.d.ts +22 -15
  4. package/dist/auth/session.js +35 -16
  5. package/dist/components/admin/GutterManager.svelte +81 -139
  6. package/dist/components/admin/GutterManager.svelte.d.ts +6 -6
  7. package/dist/components/admin/MarkdownEditor.svelte +80 -23
  8. package/dist/components/admin/MarkdownEditor.svelte.d.ts +14 -8
  9. package/dist/components/admin/composables/useAmbientSounds.svelte.d.ts +52 -2
  10. package/dist/components/admin/composables/useAmbientSounds.svelte.js +38 -4
  11. package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +80 -10
  12. package/dist/components/admin/composables/useCommandPalette.svelte.js +45 -5
  13. package/dist/components/admin/composables/useDraftManager.svelte.d.ts +76 -14
  14. package/dist/components/admin/composables/useDraftManager.svelte.js +44 -10
  15. package/dist/components/admin/composables/useEditorTheme.svelte.d.ts +168 -2
  16. package/dist/components/admin/composables/useEditorTheme.svelte.js +40 -7
  17. package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +94 -22
  18. package/dist/components/admin/composables/useSlashCommands.svelte.js +58 -9
  19. package/dist/components/admin/composables/useSnippets.svelte.d.ts +51 -2
  20. package/dist/components/admin/composables/useSnippets.svelte.js +35 -3
  21. package/dist/components/admin/composables/useWritingSession.svelte.d.ts +64 -6
  22. package/dist/components/admin/composables/useWritingSession.svelte.js +42 -5
  23. package/dist/components/custom/ContentWithGutter.svelte +53 -23
  24. package/dist/components/custom/ContentWithGutter.svelte.d.ts +6 -14
  25. package/dist/components/custom/GutterItem.svelte +1 -1
  26. package/dist/components/custom/LeftGutter.svelte +43 -13
  27. package/dist/components/custom/LeftGutter.svelte.d.ts +6 -6
  28. package/dist/config/ai-models.js +1 -1
  29. package/dist/groveauth/client.js +11 -11
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.js +2 -2
  32. package/dist/server/logger.d.ts +74 -26
  33. package/dist/server/logger.js +133 -184
  34. package/dist/server/services/cache.js +1 -10
  35. package/dist/ui/components/charts/ActivityOverview.svelte +14 -3
  36. package/dist/ui/components/charts/ActivityOverview.svelte.d.ts +10 -7
  37. package/dist/ui/components/charts/RepoBreakdown.svelte +9 -3
  38. package/dist/ui/components/charts/RepoBreakdown.svelte.d.ts +12 -11
  39. package/dist/ui/components/charts/Sparkline.svelte +18 -7
  40. package/dist/ui/components/charts/Sparkline.svelte.d.ts +21 -2
  41. package/dist/ui/components/gallery/ImageGallery.svelte +12 -8
  42. package/dist/ui/components/gallery/ImageGallery.svelte.d.ts +2 -2
  43. package/dist/ui/components/gallery/Lightbox.svelte +5 -2
  44. package/dist/ui/components/gallery/ZoomableImage.svelte +8 -5
  45. package/dist/ui/components/primitives/accordion/index.d.ts +1 -1
  46. package/dist/ui/components/primitives/input/input.svelte.d.ts +1 -1
  47. package/dist/ui/components/primitives/tabs/index.d.ts +1 -1
  48. package/dist/ui/components/primitives/textarea/textarea.svelte.d.ts +1 -1
  49. package/dist/ui/components/ui/Button.svelte +5 -0
  50. package/dist/ui/components/ui/Button.svelte.d.ts +4 -1
  51. package/dist/ui/components/ui/Input.svelte +4 -0
  52. package/dist/ui/components/ui/Input.svelte.d.ts +3 -1
  53. package/dist/ui/components/ui/Logo.svelte +86 -0
  54. package/dist/ui/components/ui/Logo.svelte.d.ts +25 -0
  55. package/dist/ui/components/ui/LogoLoader.svelte +71 -0
  56. package/dist/ui/components/ui/LogoLoader.svelte.d.ts +9 -0
  57. package/dist/ui/components/ui/index.d.ts +2 -0
  58. package/dist/ui/components/ui/index.js +2 -0
  59. package/dist/ui/tailwind.preset.js +8 -8
  60. package/dist/utils/api.js +2 -1
  61. package/dist/utils/debounce.d.ts +4 -3
  62. package/dist/utils/debounce.js +10 -6
  63. package/dist/utils/gallery.d.ts +58 -32
  64. package/dist/utils/gallery.js +111 -129
  65. package/dist/utils/gutter.d.ts +47 -26
  66. package/dist/utils/gutter.js +116 -124
  67. package/dist/utils/imageProcessor.d.ts +66 -19
  68. package/dist/utils/imageProcessor.js +31 -10
  69. package/dist/utils/index.d.ts +11 -11
  70. package/dist/utils/index.js +4 -3
  71. package/dist/utils/json.js +1 -1
  72. package/dist/utils/markdown.d.ts +183 -103
  73. package/dist/utils/markdown.js +517 -678
  74. package/dist/utils/sanitize.d.ts +22 -12
  75. package/dist/utils/sanitize.js +268 -282
  76. package/dist/utils/validation.js +4 -3
  77. package/package.json +4 -3
@@ -1,25 +1,35 @@
1
+ /**
2
+ * Centralized sanitization utilities for XSS prevention
3
+ *
4
+ * Uses DOMPurify for client-side sanitization. On the server (SSR),
5
+ * content is passed through unsanitized since it will be sanitized
6
+ * when the page hydrates on the client.
7
+ *
8
+ * This approach avoids bundling jsdom (required by isomorphic-dompurify)
9
+ * which doesn't work in Cloudflare Workers.
10
+ */
1
11
  /**
2
12
  * Sanitize HTML content to prevent XSS attacks
3
- * @param {string} html - Raw HTML string to sanitize
4
- * @returns {string} - Sanitized HTML safe for rendering
13
+ * @param html - Raw HTML string to sanitize
14
+ * @returns Sanitized HTML safe for rendering
5
15
  */
6
- export function sanitizeHTML(html: string): string;
16
+ export declare function sanitizeHTML(html: string): string;
7
17
  /**
8
18
  * Sanitize SVG content specifically (stricter rules for SVG)
9
- * @param {string} svg - Raw SVG string to sanitize
10
- * @returns {string} - Sanitized SVG safe for rendering
19
+ * @param svg - Raw SVG string to sanitize
20
+ * @returns Sanitized SVG safe for rendering
11
21
  */
12
- export function sanitizeSVG(svg: string): string;
22
+ export declare function sanitizeSVG(svg: string): string;
13
23
  /**
14
24
  * Sanitize markdown-generated HTML with appropriate security rules
15
25
  * This is a convenience wrapper for sanitizeHTML with markdown-specific settings
16
- * @param {string} markdownHTML - HTML generated from markdown parsing
17
- * @returns {string} - Sanitized HTML safe for rendering
26
+ * @param markdownHTML - HTML generated from markdown parsing
27
+ * @returns Sanitized HTML safe for rendering
18
28
  */
19
- export function sanitizeMarkdown(markdownHTML: string): string;
29
+ export declare function sanitizeMarkdown(markdownHTML: string): string;
20
30
  /**
21
31
  * Sanitize URL to prevent dangerous protocols
22
- * @param {string} url - URL to sanitize
23
- * @returns {string} - Sanitized URL (returns empty string if dangerous)
32
+ * @param url - URL to sanitize
33
+ * @returns Sanitized URL (returns empty string if dangerous)
24
34
  */
25
- export function sanitizeURL(url: string): string;
35
+ export declare function sanitizeURL(url: string): string;
@@ -8,309 +8,295 @@
8
8
  * This approach avoids bundling jsdom (required by isomorphic-dompurify)
9
9
  * which doesn't work in Cloudflare Workers.
10
10
  */
11
-
12
- import { browser } from "$app/environment";
13
-
14
- // Dynamically import DOMPurify only in browser
11
+ import { BROWSER } from "esm-env";
12
+ // DOMPurify instance - dynamically imported only in browser
15
13
  let DOMPurify = null;
16
- if (browser) {
17
- import("dompurify").then((module) => {
18
- DOMPurify = module.default;
19
- });
14
+ if (BROWSER) {
15
+ import("dompurify").then((module) => {
16
+ DOMPurify = module.default;
17
+ });
20
18
  }
21
-
22
19
  /**
23
20
  * Sanitize HTML content to prevent XSS attacks
24
- * @param {string} html - Raw HTML string to sanitize
25
- * @returns {string} - Sanitized HTML safe for rendering
21
+ * @param html - Raw HTML string to sanitize
22
+ * @returns Sanitized HTML safe for rendering
26
23
  */
27
24
  export function sanitizeHTML(html) {
28
- if (!html || typeof html !== "string") {
29
- return "";
30
- }
31
-
32
- // On server, pass through - will be sanitized on client hydration
33
- if (!browser || !DOMPurify) {
34
- return html;
35
- }
36
-
37
- const config = {
38
- FORBID_TAGS: [
39
- "script",
40
- "iframe",
41
- "object",
42
- "embed",
43
- "link",
44
- "style",
45
- "form",
46
- "input",
47
- "button",
48
- "base",
49
- "meta",
50
- ],
51
- FORBID_ATTR: [
52
- "onerror",
53
- "onload",
54
- "onclick",
55
- "onmouseover",
56
- "onfocus",
57
- "onblur",
58
- "onchange",
59
- "onsubmit",
60
- "onmouseenter",
61
- "onmouseleave",
62
- "style",
63
- ],
64
- ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|\/|#)/i,
65
- ALLOW_DATA_ATTR: false,
66
- KEEP_CONTENT: true,
67
- SAFE_FOR_TEMPLATES: true,
68
- };
69
-
70
- return DOMPurify.sanitize(html, config);
25
+ if (!html || typeof html !== "string") {
26
+ return "";
27
+ }
28
+ // On server, pass through - will be sanitized on client hydration
29
+ if (!BROWSER || !DOMPurify) {
30
+ return html;
31
+ }
32
+ const config = {
33
+ FORBID_TAGS: [
34
+ "script",
35
+ "iframe",
36
+ "object",
37
+ "embed",
38
+ "link",
39
+ "style",
40
+ "form",
41
+ "input",
42
+ "button",
43
+ "base",
44
+ "meta",
45
+ ],
46
+ FORBID_ATTR: [
47
+ "onerror",
48
+ "onload",
49
+ "onclick",
50
+ "onmouseover",
51
+ "onfocus",
52
+ "onblur",
53
+ "onchange",
54
+ "onsubmit",
55
+ "onmouseenter",
56
+ "onmouseleave",
57
+ "style",
58
+ ],
59
+ ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|\/|#)/i,
60
+ ALLOW_DATA_ATTR: false,
61
+ KEEP_CONTENT: true,
62
+ SAFE_FOR_TEMPLATES: true,
63
+ RETURN_TRUSTED_TYPE: false,
64
+ };
65
+ return DOMPurify.sanitize(html, config);
71
66
  }
72
-
73
67
  /**
74
68
  * Sanitize SVG content specifically (stricter rules for SVG)
75
- * @param {string} svg - Raw SVG string to sanitize
76
- * @returns {string} - Sanitized SVG safe for rendering
69
+ * @param svg - Raw SVG string to sanitize
70
+ * @returns Sanitized SVG safe for rendering
77
71
  */
78
72
  export function sanitizeSVG(svg) {
79
- if (!svg || typeof svg !== "string") {
80
- return "";
81
- }
82
-
83
- // On server, pass through - will be sanitized on client hydration
84
- if (!browser || !DOMPurify) {
85
- return svg;
86
- }
87
-
88
- return DOMPurify.sanitize(svg, {
89
- USE_PROFILES: { svg: true, svgFilters: true },
90
- ALLOWED_TAGS: [
91
- "svg",
92
- "g",
93
- "path",
94
- "circle",
95
- "rect",
96
- "line",
97
- "polyline",
98
- "polygon",
99
- "ellipse",
100
- "text",
101
- "tspan",
102
- "defs",
103
- "marker",
104
- "pattern",
105
- "clipPath",
106
- "mask",
107
- "linearGradient",
108
- "radialGradient",
109
- "stop",
110
- "use",
111
- "symbol",
112
- "title",
113
- "desc",
114
- ],
115
- ALLOWED_ATTR: [
116
- "class",
117
- "id",
118
- "transform",
119
- "fill",
120
- "stroke",
121
- "stroke-width",
122
- "x",
123
- "y",
124
- "x1",
125
- "y1",
126
- "x2",
127
- "y2",
128
- "cx",
129
- "cy",
130
- "r",
131
- "rx",
132
- "ry",
133
- "width",
134
- "height",
135
- "d",
136
- "points",
137
- "viewBox",
138
- "xmlns",
139
- "version",
140
- "preserveAspectRatio",
141
- "opacity",
142
- "fill-opacity",
143
- "stroke-opacity",
144
- ],
145
- FORBID_TAGS: [
146
- "script",
147
- "iframe",
148
- "object",
149
- "embed",
150
- "link",
151
- "style",
152
- "foreignObject",
153
- "image",
154
- "a",
155
- ],
156
- FORBID_ATTR: [
157
- "onerror",
158
- "onload",
159
- "onclick",
160
- "onmouseover",
161
- "onfocus",
162
- "onblur",
163
- "style",
164
- "href",
165
- "xlink:href",
166
- ],
167
- KEEP_CONTENT: false,
168
- SAFE_FOR_TEMPLATES: true,
169
- });
73
+ if (!svg || typeof svg !== "string") {
74
+ return "";
75
+ }
76
+ // On server, pass through - will be sanitized on client hydration
77
+ if (!BROWSER || !DOMPurify) {
78
+ return svg;
79
+ }
80
+ return DOMPurify.sanitize(svg, {
81
+ USE_PROFILES: { svg: true, svgFilters: true },
82
+ ALLOWED_TAGS: [
83
+ "svg",
84
+ "g",
85
+ "path",
86
+ "circle",
87
+ "rect",
88
+ "line",
89
+ "polyline",
90
+ "polygon",
91
+ "ellipse",
92
+ "text",
93
+ "tspan",
94
+ "defs",
95
+ "marker",
96
+ "pattern",
97
+ "clipPath",
98
+ "mask",
99
+ "linearGradient",
100
+ "radialGradient",
101
+ "stop",
102
+ "use",
103
+ "symbol",
104
+ "title",
105
+ "desc",
106
+ ],
107
+ ALLOWED_ATTR: [
108
+ "class",
109
+ "id",
110
+ "transform",
111
+ "fill",
112
+ "stroke",
113
+ "stroke-width",
114
+ "x",
115
+ "y",
116
+ "x1",
117
+ "y1",
118
+ "x2",
119
+ "y2",
120
+ "cx",
121
+ "cy",
122
+ "r",
123
+ "rx",
124
+ "ry",
125
+ "width",
126
+ "height",
127
+ "d",
128
+ "points",
129
+ "viewBox",
130
+ "xmlns",
131
+ "version",
132
+ "preserveAspectRatio",
133
+ "opacity",
134
+ "fill-opacity",
135
+ "stroke-opacity",
136
+ ],
137
+ FORBID_TAGS: [
138
+ "script",
139
+ "iframe",
140
+ "object",
141
+ "embed",
142
+ "link",
143
+ "style",
144
+ "foreignObject",
145
+ "image",
146
+ "a",
147
+ ],
148
+ FORBID_ATTR: [
149
+ "onerror",
150
+ "onload",
151
+ "onclick",
152
+ "onmouseover",
153
+ "onfocus",
154
+ "onblur",
155
+ "style",
156
+ "href",
157
+ "xlink:href",
158
+ ],
159
+ KEEP_CONTENT: false,
160
+ SAFE_FOR_TEMPLATES: true,
161
+ RETURN_TRUSTED_TYPE: false,
162
+ });
170
163
  }
171
-
172
164
  /**
173
165
  * Sanitize markdown-generated HTML with appropriate security rules
174
166
  * This is a convenience wrapper for sanitizeHTML with markdown-specific settings
175
- * @param {string} markdownHTML - HTML generated from markdown parsing
176
- * @returns {string} - Sanitized HTML safe for rendering
167
+ * @param markdownHTML - HTML generated from markdown parsing
168
+ * @returns Sanitized HTML safe for rendering
177
169
  */
178
170
  export function sanitizeMarkdown(markdownHTML) {
179
- if (!markdownHTML || typeof markdownHTML !== "string") {
180
- return "";
181
- }
182
-
183
- // On server, pass through - will be sanitized on client hydration
184
- if (!browser || !DOMPurify) {
185
- return markdownHTML;
186
- }
187
-
188
- // For markdown, we allow a broader set of tags but still sanitize
189
- return DOMPurify.sanitize(markdownHTML, {
190
- ALLOWED_TAGS: [
191
- "a",
192
- "abbr",
193
- "b",
194
- "blockquote",
195
- "br",
196
- "code",
197
- "dd",
198
- "del",
199
- "div",
200
- "dl",
201
- "dt",
202
- "em",
203
- "h1",
204
- "h2",
205
- "h3",
206
- "h4",
207
- "h5",
208
- "h6",
209
- "hr",
210
- "i",
211
- "img",
212
- "ins",
213
- "kbd",
214
- "li",
215
- "mark",
216
- "ol",
217
- "p",
218
- "pre",
219
- "q",
220
- "s",
221
- "samp",
222
- "small",
223
- "span",
224
- "strong",
225
- "sub",
226
- "sup",
227
- "table",
228
- "tbody",
229
- "td",
230
- "tfoot",
231
- "th",
232
- "thead",
233
- "tr",
234
- "u",
235
- "ul",
236
- "var",
237
- "input",
238
- "label",
239
- ],
240
- ALLOWED_ATTR: [
241
- "href",
242
- "src",
243
- "alt",
244
- "title",
245
- "class",
246
- "id",
247
- "target",
248
- "rel",
249
- "width",
250
- "height",
251
- "align",
252
- "type",
253
- "checked",
254
- "disabled",
255
- ],
256
- FORBID_TAGS: [
257
- "script",
258
- "iframe",
259
- "object",
260
- "embed",
261
- "link",
262
- "style",
263
- "form",
264
- "button",
265
- ],
266
- FORBID_ATTR: [
267
- "onerror",
268
- "onload",
269
- "onclick",
270
- "onmouseover",
271
- "onfocus",
272
- "onblur",
273
- "onchange",
274
- "onsubmit",
275
- "style",
276
- ],
277
- ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|\/|#)/i,
278
- ALLOW_DATA_ATTR: false,
279
- KEEP_CONTENT: true,
280
- SAFE_FOR_TEMPLATES: true,
281
- });
171
+ if (!markdownHTML || typeof markdownHTML !== "string") {
172
+ return "";
173
+ }
174
+ // On server, pass through - will be sanitized on client hydration
175
+ if (!BROWSER || !DOMPurify) {
176
+ return markdownHTML;
177
+ }
178
+ // For markdown, we allow a broader set of tags but still sanitize
179
+ return DOMPurify.sanitize(markdownHTML, {
180
+ ALLOWED_TAGS: [
181
+ "a",
182
+ "abbr",
183
+ "b",
184
+ "blockquote",
185
+ "br",
186
+ "code",
187
+ "dd",
188
+ "del",
189
+ "div",
190
+ "dl",
191
+ "dt",
192
+ "em",
193
+ "h1",
194
+ "h2",
195
+ "h3",
196
+ "h4",
197
+ "h5",
198
+ "h6",
199
+ "hr",
200
+ "i",
201
+ "img",
202
+ "ins",
203
+ "kbd",
204
+ "li",
205
+ "mark",
206
+ "ol",
207
+ "p",
208
+ "pre",
209
+ "q",
210
+ "s",
211
+ "samp",
212
+ "small",
213
+ "span",
214
+ "strong",
215
+ "sub",
216
+ "sup",
217
+ "table",
218
+ "tbody",
219
+ "td",
220
+ "tfoot",
221
+ "th",
222
+ "thead",
223
+ "tr",
224
+ "u",
225
+ "ul",
226
+ "var",
227
+ "input",
228
+ "label",
229
+ ],
230
+ ALLOWED_ATTR: [
231
+ "href",
232
+ "src",
233
+ "alt",
234
+ "title",
235
+ "class",
236
+ "id",
237
+ "target",
238
+ "rel",
239
+ "width",
240
+ "height",
241
+ "align",
242
+ "type",
243
+ "checked",
244
+ "disabled",
245
+ ],
246
+ FORBID_TAGS: [
247
+ "script",
248
+ "iframe",
249
+ "object",
250
+ "embed",
251
+ "link",
252
+ "style",
253
+ "form",
254
+ "button",
255
+ ],
256
+ FORBID_ATTR: [
257
+ "onerror",
258
+ "onload",
259
+ "onclick",
260
+ "onmouseover",
261
+ "onfocus",
262
+ "onblur",
263
+ "onchange",
264
+ "onsubmit",
265
+ "style",
266
+ ],
267
+ ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|\/|#)/i,
268
+ ALLOW_DATA_ATTR: false,
269
+ KEEP_CONTENT: true,
270
+ SAFE_FOR_TEMPLATES: true,
271
+ RETURN_TRUSTED_TYPE: false,
272
+ });
282
273
  }
283
-
284
274
  /**
285
275
  * Sanitize URL to prevent dangerous protocols
286
- * @param {string} url - URL to sanitize
287
- * @returns {string} - Sanitized URL (returns empty string if dangerous)
276
+ * @param url - URL to sanitize
277
+ * @returns Sanitized URL (returns empty string if dangerous)
288
278
  */
289
279
  export function sanitizeURL(url) {
290
- if (!url || typeof url !== "string") {
291
- return "";
292
- }
293
-
294
- // Allow relative URLs
295
- if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
296
- return url;
297
- }
298
-
299
- // Check for dangerous protocols
300
- const dangerous = /^(javascript|data|vbscript|file|about):/i;
301
- if (dangerous.test(url)) {
302
- return "";
303
- }
304
-
305
- // Only allow safe protocols
306
- const safe = /^(https?|mailto|tel):/i;
307
- if (!safe.test(url)) {
308
- // If no protocol, assume relative
309
- if (!url.includes(":")) {
310
- return url;
280
+ if (!url || typeof url !== "string") {
281
+ return "";
282
+ }
283
+ // Allow relative URLs
284
+ if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
285
+ return url;
286
+ }
287
+ // Check for dangerous protocols
288
+ const dangerous = /^(javascript|data|vbscript|file|about):/i;
289
+ if (dangerous.test(url)) {
290
+ return "";
311
291
  }
312
- return "";
313
- }
314
-
315
- return url;
292
+ // Only allow safe protocols
293
+ const safe = /^(https?|mailto|tel):/i;
294
+ if (!safe.test(url)) {
295
+ // If no protocol, assume relative
296
+ if (!url.includes(":")) {
297
+ return url;
298
+ }
299
+ return "";
300
+ }
301
+ return url;
316
302
  }
@@ -26,12 +26,12 @@ const FILE_SIGNATURES = {
26
26
  */
27
27
  export async function validateFileSignature(file, expectedType) {
28
28
  const buffer = new Uint8Array(await file.arrayBuffer());
29
- const signatures = FILE_SIGNATURES[expectedType];
29
+ const signatures = /** @type {number[][] | undefined} */ (FILE_SIGNATURES[/** @type {keyof typeof FILE_SIGNATURES} */ (expectedType)]);
30
30
 
31
31
  if (!signatures) return false;
32
32
 
33
- return signatures.some(sig =>
34
- sig.every((byte, i) => buffer[i] === byte)
33
+ return signatures.some((/** @type {number[]} */ sig) =>
34
+ sig.every((/** @type {number} */ byte, /** @type {number} */ i) => buffer[i] === byte)
35
35
  );
36
36
  }
37
37
 
@@ -54,6 +54,7 @@ export function sanitizeObject(obj) {
54
54
  ));
55
55
  }
56
56
 
57
+ /** @type {Record<string, any>} */
57
58
  const sanitized = {};
58
59
 
59
60
  for (const [key, value] of Object.entries(obj)) {