@ubiquity-os/plugin-sdk 3.2.0 → 3.2.2

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/README.md CHANGED
@@ -5,7 +5,7 @@ This project provides a software development kit (SDK) for creating plugins usin
5
5
  - TypeScript
6
6
  - Creating a plugin instance
7
7
  - Injection of the context
8
- - Provider with a logger, an authenticated Octokit instance and the event payload
8
+ - Provider with a logger, an authenticated Octokit instance, and the event payload
9
9
 
10
10
  ## Key Functions
11
11
 
@@ -58,3 +58,59 @@ To start Jest tests, run:
58
58
  ```sh
59
59
  bun run test
60
60
  ```
61
+
62
+ ## Markdown Cleaning Utility
63
+
64
+ `cleanMarkdown` removes top-level HTML comments and configured HTML tags while preserving content inside fenced/indented code blocks, inline code spans, and blockquotes.
65
+
66
+ ### Import
67
+
68
+ ```ts
69
+ import { cleanMarkdown, type CleanMarkdownOptions } from "@ubiquity-os/plugin-sdk/markdown";
70
+ ```
71
+
72
+ ### Options (`CleanMarkdownOptions`)
73
+
74
+ | Option | Type | Default | Description |
75
+ | -------------------- | --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
76
+ | `tags` | `(keyof HTMLElementTagNameMap)[]` | `[]` | List of HTML tag names to strip. Whole block tokens that are a single matching root element are removed entirely. Inline self-closing/void-like occurrences (e.g. `<br>`) are also removed. |
77
+ | `collapseEmptyLines` | `boolean` | `false` | Collapses runs of 3+ blank lines down to exactly 2. |
78
+
79
+ ### Behavior Summary
80
+
81
+ - Strips HTML comments (`<!-- ... -->`) outside protected contexts:
82
+ - Not inside fenced/indented code blocks
83
+ - Not inside inline code spans
84
+ - Not inside blockquotes (blockquote content is left untouched)
85
+ - Removes entire HTML block tokens consisting of a single root element whose tag is in `tags`.
86
+ - Removes inline occurrences of any tag in `tags` (void/self-closing style).
87
+ - Leaves everything else unchanged to minimize diff noise.
88
+ - Final output is trimmed (no trailing blank lines).
89
+
90
+ ### Example
91
+
92
+ ```ts
93
+ const input = `
94
+ <!-- build badge -->
95
+ <details>
96
+ <summary>Info</summary>
97
+ Content inside details
98
+ </details>
99
+
100
+ Paragraph with <br> line break and \`<br>\` in code.
101
+
102
+ \`\`\`ts
103
+ // Code block with <!-- comment --> and <br>
104
+ const x = 1;
105
+ \`\`\`
106
+
107
+ > Blockquote with <!-- preserved comment --> and <br>.
108
+ `;
109
+
110
+ const cleaned = cleanMarkdown(input, {
111
+ tags: ["details", "br"],
112
+ collapseEmptyLines: true,
113
+ });
114
+
115
+ console.log(cleaned);
116
+ ```
@@ -24,15 +24,15 @@ __export(compression_exports, {
24
24
  decompressString: () => decompressString
25
25
  });
26
26
  module.exports = __toCommonJS(compression_exports);
27
- var import_brotli = require("brotli");
27
+ var import_node_zlib = require("zlib");
28
28
  function compressString(str) {
29
29
  const input = Buffer.from(str, "utf8");
30
- const compressed = (0, import_brotli.compress)(input);
30
+ const compressed = (0, import_node_zlib.brotliCompressSync)(input);
31
31
  return Buffer.from(compressed).toString("base64");
32
32
  }
33
33
  function decompressString(compressed) {
34
34
  const buffer = Buffer.from(compressed, "base64");
35
- const decompressed = (0, import_brotli.decompress)(buffer);
35
+ const decompressed = (0, import_node_zlib.brotliDecompressSync)(buffer);
36
36
  return Buffer.from(decompressed).toString("utf8");
37
37
  }
38
38
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,13 +1,13 @@
1
1
  // src/helpers/compression.ts
2
- import { compress, decompress } from "brotli";
2
+ import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
3
3
  function compressString(str) {
4
4
  const input = Buffer.from(str, "utf8");
5
- const compressed = compress(input);
5
+ const compressed = brotliCompressSync(input);
6
6
  return Buffer.from(compressed).toString("base64");
7
7
  }
8
8
  function decompressString(compressed) {
9
9
  const buffer = Buffer.from(compressed, "base64");
10
- const decompressed = decompress(buffer);
10
+ const decompressed = brotliDecompressSync(buffer);
11
11
  return Buffer.from(decompressed).toString("utf8");
12
12
  }
13
13
  export {
package/dist/index.d.mts CHANGED
@@ -116,7 +116,7 @@ interface Context<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSuppor
116
116
  type Return = Record<string, unknown> | undefined | void;
117
117
  type HandlerReturn = Promise<Return> | Return;
118
118
 
119
- interface Options {
119
+ interface Options$1 {
120
120
  kernelPublicKey?: string;
121
121
  logLevel?: LogLevel;
122
122
  postCommentOnError?: boolean;
@@ -129,8 +129,38 @@ interface Options {
129
129
  bypassSignatureVerification?: boolean;
130
130
  }
131
131
 
132
- declare function createActionsPlugin<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSupportedEvents extends EmitterWebhookEventName = EmitterWebhookEventName>(handler: (context: Context<TConfig, TEnv, TCommand, TSupportedEvents>) => HandlerReturn, options?: Options): Promise<void>;
132
+ declare function createActionsPlugin<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSupportedEvents extends EmitterWebhookEventName = EmitterWebhookEventName>(handler: (context: Context<TConfig, TEnv, TCommand, TSupportedEvents>) => HandlerReturn, options?: Options$1): Promise<void>;
133
133
 
134
- declare function createPlugin<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSupportedEvents extends EmitterWebhookEventName = EmitterWebhookEventName>(handler: (context: Context<TConfig, TEnv, TCommand, TSupportedEvents>) => HandlerReturn, manifest: Manifest, options?: Options): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
134
+ declare function createPlugin<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSupportedEvents extends EmitterWebhookEventName = EmitterWebhookEventName>(handler: (context: Context<TConfig, TEnv, TCommand, TSupportedEvents>) => HandlerReturn, manifest: Manifest, options?: Options$1): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
135
135
 
136
- export { CommentHandler, type Context, type Options, createActionsPlugin, createPlugin };
136
+ /**
137
+ * Options for cleanMarkdown.
138
+ *
139
+ * tags:
140
+ * A single list of HTML tag names (keyof HTMLElementTagNameMap) to remove.
141
+ * Behavior per tag:
142
+ * 1. If an HTML block token consists solely of a single root element whose tag matches, the entire block (its content) is removed.
143
+ * (e.g. <details> ... </details> when "details" is in tags).
144
+ * 2. Standalone inline occurrences of that tag treated as void/self-closing (e.g. <br>) are stripped.
145
+ *
146
+ * collapseEmptyLines:
147
+ * If true, collapses sequences of 3+ blank lines down to exactly 2, preserving paragraph spacing while shrinking payload size.
148
+ */
149
+ type Options = {
150
+ tags?: string[];
151
+ shouldCollapseEmptyLines?: boolean;
152
+ };
153
+ /**
154
+ * Cleans a GitHub-flavored markdown string by:
155
+ * - Removing top‑level HTML comments (<!-- ... -->) (outside code / inline code / blockquote context)
156
+ * - Removing blocks for configured tags when the entire html token is a single element of that tag
157
+ * - Removing inline occurrences of configured tags treated as void (e.g. \<br\>) outside fenced / inline code
158
+ * - Preserving comments and tags inside:
159
+ * * fenced or indented code blocks
160
+ * * inline code spans
161
+ * * blockquotes (their contents unchanged)
162
+ * - Optionally collapsing excessive blank lines
163
+ */
164
+ declare function cleanMarkdown(md: string, options?: Options): string;
165
+
166
+ export { CommentHandler, type Context, type Options$1 as Options, cleanMarkdown, createActionsPlugin, createPlugin };
package/dist/index.d.ts CHANGED
@@ -116,7 +116,7 @@ interface Context<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSuppor
116
116
  type Return = Record<string, unknown> | undefined | void;
117
117
  type HandlerReturn = Promise<Return> | Return;
118
118
 
119
- interface Options {
119
+ interface Options$1 {
120
120
  kernelPublicKey?: string;
121
121
  logLevel?: LogLevel;
122
122
  postCommentOnError?: boolean;
@@ -129,8 +129,38 @@ interface Options {
129
129
  bypassSignatureVerification?: boolean;
130
130
  }
131
131
 
132
- declare function createActionsPlugin<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSupportedEvents extends EmitterWebhookEventName = EmitterWebhookEventName>(handler: (context: Context<TConfig, TEnv, TCommand, TSupportedEvents>) => HandlerReturn, options?: Options): Promise<void>;
132
+ declare function createActionsPlugin<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSupportedEvents extends EmitterWebhookEventName = EmitterWebhookEventName>(handler: (context: Context<TConfig, TEnv, TCommand, TSupportedEvents>) => HandlerReturn, options?: Options$1): Promise<void>;
133
133
 
134
- declare function createPlugin<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSupportedEvents extends EmitterWebhookEventName = EmitterWebhookEventName>(handler: (context: Context<TConfig, TEnv, TCommand, TSupportedEvents>) => HandlerReturn, manifest: Manifest, options?: Options): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
134
+ declare function createPlugin<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSupportedEvents extends EmitterWebhookEventName = EmitterWebhookEventName>(handler: (context: Context<TConfig, TEnv, TCommand, TSupportedEvents>) => HandlerReturn, manifest: Manifest, options?: Options$1): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
135
135
 
136
- export { CommentHandler, type Context, type Options, createActionsPlugin, createPlugin };
136
+ /**
137
+ * Options for cleanMarkdown.
138
+ *
139
+ * tags:
140
+ * A single list of HTML tag names (keyof HTMLElementTagNameMap) to remove.
141
+ * Behavior per tag:
142
+ * 1. If an HTML block token consists solely of a single root element whose tag matches, the entire block (its content) is removed.
143
+ * (e.g. <details> ... </details> when "details" is in tags).
144
+ * 2. Standalone inline occurrences of that tag treated as void/self-closing (e.g. <br>) are stripped.
145
+ *
146
+ * collapseEmptyLines:
147
+ * If true, collapses sequences of 3+ blank lines down to exactly 2, preserving paragraph spacing while shrinking payload size.
148
+ */
149
+ type Options = {
150
+ tags?: string[];
151
+ shouldCollapseEmptyLines?: boolean;
152
+ };
153
+ /**
154
+ * Cleans a GitHub-flavored markdown string by:
155
+ * - Removing top‑level HTML comments (<!-- ... -->) (outside code / inline code / blockquote context)
156
+ * - Removing blocks for configured tags when the entire html token is a single element of that tag
157
+ * - Removing inline occurrences of configured tags treated as void (e.g. \<br\>) outside fenced / inline code
158
+ * - Preserving comments and tags inside:
159
+ * * fenced or indented code blocks
160
+ * * inline code spans
161
+ * * blockquotes (their contents unchanged)
162
+ * - Optionally collapsing excessive blank lines
163
+ */
164
+ declare function cleanMarkdown(md: string, options?: Options): string;
165
+
166
+ export { CommentHandler, type Context, type Options$1 as Options, cleanMarkdown, createActionsPlugin, createPlugin };
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  CommentHandler: () => CommentHandler,
34
+ cleanMarkdown: () => cleanMarkdown,
34
35
  createActionsPlugin: () => createActionsPlugin,
35
36
  createPlugin: () => createPlugin
36
37
  });
@@ -354,12 +355,17 @@ function getCommand(inputs, pluginOptions) {
354
355
  }
355
356
 
356
357
  // src/helpers/compression.ts
357
- var import_brotli = require("brotli");
358
+ var import_node_zlib = require("zlib");
358
359
  function compressString(str) {
359
360
  const input = Buffer.from(str, "utf8");
360
- const compressed = (0, import_brotli.compress)(input);
361
+ const compressed = (0, import_node_zlib.brotliCompressSync)(input);
361
362
  return Buffer.from(compressed).toString("base64");
362
363
  }
364
+ function decompressString(compressed) {
365
+ const buffer = Buffer.from(compressed, "base64");
366
+ const decompressed = (0, import_node_zlib.brotliDecompressSync)(buffer);
367
+ return Buffer.from(decompressed).toString("utf8");
368
+ }
363
369
 
364
370
  // src/octokit.ts
365
371
  var import_core = require("@octokit/core");
@@ -431,9 +437,9 @@ var commandCallSchema = import_typebox.Type.Union([import_typebox.Type.Null(), i
431
437
  // src/types/util.ts
432
438
  var import_typebox2 = require("@sinclair/typebox");
433
439
  var import_value2 = require("@sinclair/typebox/value");
434
- function jsonType(type) {
440
+ function jsonType(type, decompress = false) {
435
441
  return import_typebox2.Type.Transform(import_typebox2.Type.String()).Decode((value) => {
436
- const parsed = JSON.parse(value);
442
+ const parsed = JSON.parse(decompress ? decompressString(value) : value);
437
443
  return import_value2.Value.Decode(type, import_value2.Value.Default(type, parsed));
438
444
  }).Encode((value) => JSON.stringify(value));
439
445
  }
@@ -442,7 +448,7 @@ function jsonType(type) {
442
448
  var inputSchema = import_typebox3.Type.Object({
443
449
  stateId: import_typebox3.Type.String(),
444
450
  eventName: import_typebox3.Type.String(),
445
- eventPayload: jsonType(import_typebox3.Type.Record(import_typebox3.Type.String(), import_typebox3.Type.Any())),
451
+ eventPayload: jsonType(import_typebox3.Type.Record(import_typebox3.Type.String(), import_typebox3.Type.Any()), true),
446
452
  command: jsonType(commandCallSchema),
447
453
  authToken: import_typebox3.Type.String(),
448
454
  settings: jsonType(import_typebox3.Type.Record(import_typebox3.Type.String(), import_typebox3.Type.Any())),
@@ -620,9 +626,63 @@ function createPlugin(handler, manifest, options) {
620
626
  });
621
627
  return app;
622
628
  }
629
+
630
+ // src/markdown.ts
631
+ var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
632
+ function cleanMarkdown(md, options = {}) {
633
+ const codeBlockRegex = /(```[\s\S]*?```|~~~[\s\S]*?~~~)/g;
634
+ const { tags = [], shouldCollapseEmptyLines = false } = options;
635
+ const segments = [];
636
+ let lastIndex = 0;
637
+ const matches = [...md.matchAll(codeBlockRegex)];
638
+ for (const match of matches) {
639
+ if (match.index > lastIndex) {
640
+ segments.push(processSegment(md.slice(lastIndex, match.index), tags, shouldCollapseEmptyLines));
641
+ }
642
+ segments.push(match[0]);
643
+ lastIndex = match.index + match[0].length;
644
+ }
645
+ if (lastIndex < md.length) {
646
+ segments.push(processSegment(md.slice(lastIndex), tags, shouldCollapseEmptyLines));
647
+ }
648
+ return segments.join("");
649
+ }
650
+ function processSegment(segment, extraTags, shouldCollapseEmptyLines) {
651
+ const inlineCodeRegex = /`[^`]*`/g;
652
+ const inlineCodes = [];
653
+ let s = segment.replace(inlineCodeRegex, (m) => {
654
+ inlineCodes.push(m);
655
+ return `__INLINE_CODE_${inlineCodes.length - 1}__`;
656
+ });
657
+ s = s.replace(/<!--[\s\S]*?-->/g, "");
658
+ for (const raw of extraTags) {
659
+ if (!raw) continue;
660
+ const tag = raw.toLowerCase().trim().replace(/[^\w:-]/g, "");
661
+ if (!tag) continue;
662
+ if (VOID_TAGS.has(tag)) {
663
+ const voidRe = new RegExp(`<${tag}\\b[^>]*\\/?>`, "gi");
664
+ s = s.replace(voidRe, "");
665
+ continue;
666
+ }
667
+ const pairRe = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, "gi");
668
+ let prev;
669
+ do {
670
+ prev = s;
671
+ s = s.replace(pairRe, "");
672
+ } while (s !== prev);
673
+ const openCloseRe = new RegExp(`<\\/?${tag}\\b[^>]*>`, "gi");
674
+ s = s.replace(openCloseRe, "");
675
+ }
676
+ s = s.replace(/__INLINE_CODE_(\d+)__/g, (str, idx) => inlineCodes[+idx]);
677
+ if (shouldCollapseEmptyLines) {
678
+ s = s.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n");
679
+ }
680
+ return s;
681
+ }
623
682
  // Annotate the CommonJS export names for ESM import in node:
624
683
  0 && (module.exports = {
625
684
  CommentHandler,
685
+ cleanMarkdown,
626
686
  createActionsPlugin,
627
687
  createPlugin
628
688
  });
package/dist/index.mjs CHANGED
@@ -316,12 +316,17 @@ function getCommand(inputs, pluginOptions) {
316
316
  }
317
317
 
318
318
  // src/helpers/compression.ts
319
- import { compress, decompress } from "brotli";
319
+ import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
320
320
  function compressString(str) {
321
321
  const input = Buffer.from(str, "utf8");
322
- const compressed = compress(input);
322
+ const compressed = brotliCompressSync(input);
323
323
  return Buffer.from(compressed).toString("base64");
324
324
  }
325
+ function decompressString(compressed) {
326
+ const buffer = Buffer.from(compressed, "base64");
327
+ const decompressed = brotliDecompressSync(buffer);
328
+ return Buffer.from(decompressed).toString("utf8");
329
+ }
325
330
 
326
331
  // src/octokit.ts
327
332
  import { Octokit } from "@octokit/core";
@@ -393,9 +398,9 @@ var commandCallSchema = T.Union([T.Null(), T.Object({ name: T.String(), paramete
393
398
  // src/types/util.ts
394
399
  import { Type } from "@sinclair/typebox";
395
400
  import { Value as Value2 } from "@sinclair/typebox/value";
396
- function jsonType(type) {
401
+ function jsonType(type, decompress = false) {
397
402
  return Type.Transform(Type.String()).Decode((value) => {
398
- const parsed = JSON.parse(value);
403
+ const parsed = JSON.parse(decompress ? decompressString(value) : value);
399
404
  return Value2.Decode(type, Value2.Default(type, parsed));
400
405
  }).Encode((value) => JSON.stringify(value));
401
406
  }
@@ -404,7 +409,7 @@ function jsonType(type) {
404
409
  var inputSchema = T2.Object({
405
410
  stateId: T2.String(),
406
411
  eventName: T2.String(),
407
- eventPayload: jsonType(T2.Record(T2.String(), T2.Any())),
412
+ eventPayload: jsonType(T2.Record(T2.String(), T2.Any()), true),
408
413
  command: jsonType(commandCallSchema),
409
414
  authToken: T2.String(),
410
415
  settings: jsonType(T2.Record(T2.String(), T2.Any())),
@@ -582,8 +587,62 @@ function createPlugin(handler, manifest, options) {
582
587
  });
583
588
  return app;
584
589
  }
590
+
591
+ // src/markdown.ts
592
+ var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
593
+ function cleanMarkdown(md, options = {}) {
594
+ const codeBlockRegex = /(```[\s\S]*?```|~~~[\s\S]*?~~~)/g;
595
+ const { tags = [], shouldCollapseEmptyLines = false } = options;
596
+ const segments = [];
597
+ let lastIndex = 0;
598
+ const matches = [...md.matchAll(codeBlockRegex)];
599
+ for (const match of matches) {
600
+ if (match.index > lastIndex) {
601
+ segments.push(processSegment(md.slice(lastIndex, match.index), tags, shouldCollapseEmptyLines));
602
+ }
603
+ segments.push(match[0]);
604
+ lastIndex = match.index + match[0].length;
605
+ }
606
+ if (lastIndex < md.length) {
607
+ segments.push(processSegment(md.slice(lastIndex), tags, shouldCollapseEmptyLines));
608
+ }
609
+ return segments.join("");
610
+ }
611
+ function processSegment(segment, extraTags, shouldCollapseEmptyLines) {
612
+ const inlineCodeRegex = /`[^`]*`/g;
613
+ const inlineCodes = [];
614
+ let s = segment.replace(inlineCodeRegex, (m) => {
615
+ inlineCodes.push(m);
616
+ return `__INLINE_CODE_${inlineCodes.length - 1}__`;
617
+ });
618
+ s = s.replace(/<!--[\s\S]*?-->/g, "");
619
+ for (const raw of extraTags) {
620
+ if (!raw) continue;
621
+ const tag = raw.toLowerCase().trim().replace(/[^\w:-]/g, "");
622
+ if (!tag) continue;
623
+ if (VOID_TAGS.has(tag)) {
624
+ const voidRe = new RegExp(`<${tag}\\b[^>]*\\/?>`, "gi");
625
+ s = s.replace(voidRe, "");
626
+ continue;
627
+ }
628
+ const pairRe = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, "gi");
629
+ let prev;
630
+ do {
631
+ prev = s;
632
+ s = s.replace(pairRe, "");
633
+ } while (s !== prev);
634
+ const openCloseRe = new RegExp(`<\\/?${tag}\\b[^>]*>`, "gi");
635
+ s = s.replace(openCloseRe, "");
636
+ }
637
+ s = s.replace(/__INLINE_CODE_(\d+)__/g, (str, idx) => inlineCodes[+idx]);
638
+ if (shouldCollapseEmptyLines) {
639
+ s = s.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n");
640
+ }
641
+ return s;
642
+ }
585
643
  export {
586
644
  CommentHandler,
645
+ cleanMarkdown,
587
646
  createActionsPlugin,
588
647
  createPlugin
589
648
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubiquity-os/plugin-sdk",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
4
4
  "description": "SDK for plugin support.",
5
5
  "author": "Ubiquity DAO",
6
6
  "license": "MIT",
@@ -103,7 +103,6 @@
103
103
  "@octokit/types": "^13.8.0",
104
104
  "@octokit/webhooks": "^13.7.4",
105
105
  "@ubiquity-os/ubiquity-os-logger": "^1.4.0",
106
- "brotli": "^1.3.3",
107
106
  "dotenv": "^16.4.5",
108
107
  "hono": "^4.6.9"
109
108
  },
@@ -119,7 +118,6 @@
119
118
  "@eslint/js": "^9.14.0",
120
119
  "@jest/globals": "^29.7.0",
121
120
  "@mswjs/data": "0.16.1",
122
- "@types/brotli": "^1.3.4",
123
121
  "@types/node": "^20.11.19",
124
122
  "@ubiquity-os/eslint-plugin-no-empty-strings": "^1.0.3",
125
123
  "cross-env": "^7.0.3",