From e17d5bc060f3ad9fa2994e5d50ad4d3c2cfe84cf Mon Sep 17 00:00:00 2001 From: agent_coder Date: Sun, 5 Jul 2026 04:54:07 +0300 Subject: [PATCH] fix(#345): restore prom-client, harden normalizer against ReDoS, strip frontmatter (review round 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the round-1 review of #369: F1 [CRITICAL] Restore prom-client. The prior commit removed it as a 'stray dep', but metrics.registry.ts imports it unconditionally at startup (main.ts boot), so a clean frozen install had no prom-client -> server tsc TS2307 + boot crash. It was surviving only via hoisting from a warm store. Restored to apps/server dependencies + regenerated the lock (prom-client/tdigest/bintrees return), keeping the @docmost/prosemirror-markdown dep. Verified: clean frozen install -> require.resolve('prom-client') ok, server tsc EXIT 0. F2 [HIGH] Two quadratic ReDoS vectors in foreign-markdown.ts on untrusted import (runs synchronously on the request thread, 30MB cap): (a) pass-2 was O(lines x defs) — a per-def RegExp rebuilt and run over every line. Replaced with ONE precompiled alternation regex over all def ids, built once per document, with an id->body lookup in the replacer: O(text). (b) the inline-code split alternation backtracks quadratically on a long UNCLOSED backtick run. Lines over 8KB now skip the split (left untouched) — a real footnote line is never that long. F3 [WARNING] Restore the leading YAML front-matter strip that the retired markdownToHtml layer did. Without it, Obsidian/Hugo/Jekyll/git-sync files leak their front-matter into the body (and 'title:' renders as a setext heading that title extraction can hijack). F4 [WARNING] Extend the zip-import spec with an image (width+align) + callout fidelity assertion through the PM->HTML->PM hop (the one hop the package suite does not cover). F5/F6 Update AGENTS.md (apps/server is now a prosemirror-markdown consumer) and make the server pretest build prosemirror-markdown too. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 4 +- apps/server/package.json | 3 +- ...task.service.footnote-canonicalize.spec.ts | 191 +++++++++++------- .../import/utils/foreign-markdown.spec.ts | 43 ++++ .../import/utils/foreign-markdown.ts | 66 ++++-- pnpm-lock.yaml | 24 +++ 6 files changed, 240 insertions(+), 91 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a68d5d7a..4093d15c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,7 +201,7 @@ pnpm workspace (`pnpm@10.4.0`) orchestrated by **Nx**. Four workspace packages: | `apps/client` | `client` | React 18 + Vite + Mantine 8 + TanStack Query + Jotai | SPA frontend | | `packages/editor-ext` | `@docmost/editor-ext` | Tiptap/ProseMirror | Shared Tiptap node/mark extensions, imported by both the client and the server | | `packages/mcp` | `@docmost/mcp` | MCP SDK, Tiptap, Yjs | Standalone MCP server, also bundled into the server at `/mcp`. Consumes the shared converter/schema from `@docmost/prosemirror-markdown` (#293) — it no longer carries its own vendored converter/schema copy | -| `packages/prosemirror-markdown` | `@docmost/prosemirror-markdown` | Tiptap, marked, jsdom | The single, canonical ProseMirror↔Markdown converter + Docmost schema mirror (#293). Consumed by `mcp` and `git-sync`; there is exactly ONE copy of the converter now | +| `packages/prosemirror-markdown` | `@docmost/prosemirror-markdown` | Tiptap, marked, jsdom | The single, canonical ProseMirror↔Markdown converter + Docmost schema mirror (#293). Consumed by `mcp`, `git-sync`, AND `apps/server` (server-side markdown import/export, #345); there is exactly ONE copy of the converter now | `build` targets are Nx-cached and dependency-ordered (`dependsOn: ["^build"]`), so `editor-ext` builds before the apps. `nx.json` sets `affected.defaultBase: main`. @@ -284,7 +284,7 @@ The API server is a Fastify app with a global `/api` prefix (`main.ts` excludes ### Client structure Vite SPA. Code is organized by feature under `apps/client/src/features/*` (mirrors the server domains: `page`, `space`, `comment`, `ai-chat`, `editor`, …). Conventions: - **TanStack Query** for server state (one `queries/` file per feature), **Jotai** atoms for local/shared UI state, **Mantine 8** + CSS modules (`*.module.css`) + `postcss-preset-mantine` for UI. -- The editor is Tiptap; shared node/mark extensions live in `packages/editor-ext` and are imported by **both the client and the server** (collaboration, import/export) — editor schema changes often need to be made in `editor-ext`, not just the client. The ProseMirror↔Markdown converter and its Docmost schema mirror now live in a SINGLE package, `@docmost/prosemirror-markdown` (#293), consumed by both `mcp` and `git-sync` — do NOT reintroduce a per-package copy. `editor-ext` is the upstream source of the Tiptap schema; the package's `docmost-schema.ts` mirrors it and a serializer-contract test (`packages/prosemirror-markdown/test/serializer-contract.test.ts`) guards the boundary (every schema node must have a converter case), so a drift surfaces as a failing test rather than silent divergence. +- The editor is Tiptap; shared node/mark extensions live in `packages/editor-ext` and are imported by **both the client and the server** (collaboration, schema, `canonicalizeFootnotes`) — editor schema changes often need to be made in `editor-ext`, not just the client. Server-side markdown import/export no longer lives in `editor-ext`: it goes through the canonical converter (#345, see below). The ProseMirror↔Markdown converter and its Docmost schema mirror now live in a SINGLE package, `@docmost/prosemirror-markdown` (#293), consumed by `mcp`, `git-sync`, and `apps/server` (#345) — do NOT reintroduce a per-package copy. `editor-ext` is the upstream source of the Tiptap schema; the package's `docmost-schema.ts` mirrors it and a serializer-contract test (`packages/prosemirror-markdown/test/serializer-contract.test.ts`) guards the boundary (every schema node must have a converter case), so a drift surfaces as a failing test rather than silent divergence. - API access goes through `apps/client/src/lib/api-client.ts` (axios). The `@` alias maps to `apps/client/src`. - Runtime config is injected at build time by `vite.config.ts` via `define` (`APP_URL`, `COLLAB_URL`, `APP_VERSION`, …) — these come from the root `.env`, not from `import.meta.env`. diff --git a/apps/server/package.json b/apps/server/package.json index dd9b7c03..f12c1b2d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -23,7 +23,7 @@ "migration:reset": "tsx src/database/migrate.ts down-to NO_MIGRATIONS", "migration:codegen": "kysely-codegen --dialect=postgres --camel-case --env-file=../../.env --out-file=./src/database/types/db.d.ts", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "pretest": "pnpm --filter @docmost/editor-ext build", + "pretest": "pnpm --filter @docmost/editor-ext build && pnpm --filter @docmost/prosemirror-markdown build", "test": "jest", "test:int": "jest --config test/jest-integration.json", "test:watch": "jest --watch", @@ -112,6 +112,7 @@ "pino-pretty": "^13.1.3", "postgres": "^3.4.8", "postmark": "^4.0.7", + "prom-client": "^15.1.3", "react": "^18.3.1", "react-email": "6.0.8", "reflect-metadata": "^0.2.2", diff --git a/apps/server/src/integrations/import/services/file-import-task.service.footnote-canonicalize.spec.ts b/apps/server/src/integrations/import/services/file-import-task.service.footnote-canonicalize.spec.ts index 10d36902..114999e9 100644 --- a/apps/server/src/integrations/import/services/file-import-task.service.footnote-canonicalize.spec.ts +++ b/apps/server/src/integrations/import/services/file-import-task.service.footnote-canonicalize.spec.ts @@ -91,88 +91,127 @@ function chainable(result: any): any { return proxy; } +/** + * Run one markdown file through the REAL zip-import pipeline + * (`processGenericImport` -> `markdownToProseMirror` -> `jsonToHtml` -> + * `processHTML`/`htmlToJson`) and return the persisted page `content`. This is + * the server-specific PM->HTML->PM hop that the package's own PM<->MD tests do + * NOT cover. + */ +async function runZipImport(markdown: string): Promise { + const extractDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fit-canon-')); + await fs.writeFile(path.join(extractDir, 'note.md'), markdown, 'utf-8'); + + const importService = new ImportService( + {} as any, + {} as any, + {} as any, + {} as any, + ); + jest + .spyOn(importService as any, 'createYdoc') + .mockResolvedValue(Buffer.from([]) as any); + + let captured: any = null; + const trx = { + insertInto: (table: string) => ({ + values: (v: any) => { + if (table === 'pages') captured = v; + return { execute: async () => {} }; + }, + }), + }; + const db: any = { + selectFrom: () => chainable({ slug: 'space-slug' }), + transaction: () => ({ execute: (fn: any) => fn(trx) }), + }; + + const importAttachmentService = { + processAttachments: async ({ html }: any) => html, + }; + const service = new FileImportTaskService( + {} as any, // storageService + importService as any, + { nextPagePosition: async () => 'a0' } as any, + { insertBacklink: jest.fn() } as any, + db, + importAttachmentService as any, + { emit: jest.fn() } as any, + { logBatchWithContext: jest.fn() } as any, + ); + + const fileTask: any = { + id: 'task-1', + source: 'generic', + spaceId: 'space-1', + workspaceId: 'ws-1', + creatorId: 'user-1', + }; + + try { + await service.processGenericImport({ extractDir, fileTask }); + expect(captured).toBeTruthy(); + return captured.content; + } finally { + await fs.rm(extractDir, { recursive: true, force: true }); + } +} + +/** Find the first node of a given type anywhere in a PM content tree. */ +function findFirst(node: any, type: string): any { + if (!node || typeof node !== 'object') return null; + if (node.type === type) return node; + for (const child of node.content ?? []) { + const hit = findFirst(child, type); + if (hit) return hit; + } + return null; +} + describe('FileImportTaskService.processGenericImport — footnote canonicalization (#228)', () => { it('orders footnotes by first reference, dedupes reuse, and drops orphans on zip import', async () => { - const extractDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fit-canon-')); - await fs.writeFile(path.join(extractDir, 'note.md'), MARKDOWN, 'utf-8'); - - // Real ImportService for the html -> JSON conversion; stub the yjs encode. - const importService = new ImportService( - {} as any, - {} as any, - {} as any, - {} as any, + const content = await runZipImport(MARKDOWN); + // Definitions ordered by FIRST REFERENCE (C, A, B), NOT the markdown + // definition order (A, B, C). Ids are the parser's fresh `fn-*`, so pin + // the BODIES. + expect(footnoteListBodies(content)).toEqual(['note C', 'note A', 'note B']); + // Orphan [^z] dropped; reused [^a] collapses to one definition; one list. + expect(footnoteListBodies(content)).not.toContain('orphan note'); + const lists = (content.content ?? []).filter( + (n: any) => n.type === 'footnotesList', ); - jest - .spyOn(importService as any, 'createYdoc') - .mockResolvedValue(Buffer.from([]) as any); + expect(lists).toHaveLength(1); + expect( + footnoteListBodies(content).filter((b) => b === 'note A'), + ).toHaveLength(1); + }); - let captured: any = null; - const trx = { - insertInto: (table: string) => ({ - values: (v: any) => { - if (table === 'pages') captured = v; - return { execute: async () => {} }; - }, - }), - }; - const db: any = { - selectFrom: () => chainable({ slug: 'space-slug' }), - transaction: () => ({ execute: (fn: any) => fn(trx) }), - }; + // #345 F4: the zip path routes markdown through jsonToHtml -> processHTML -> + // htmlToJson (the shared HTML attachment pipeline). #345's headline is LOSSLESS + // image width/align via the `` comment; a callout carries its + // `type`. This asserts those survive the PM->HTML->PM hop — the one hop the + // package's PM<->MD suite does not exercise. + it('preserves image width/align and callout type through the PM->HTML->PM hop', async () => { + const md = [ + '# Doc', + '', + '![a picture](https://example.com/i.png) ', + '', + ':::warning', + 'Careful now.', + ':::', + ].join('\n'); - const importAttachmentService = { - processAttachments: async ({ html }: any) => html, - }; - const backlinkRepo = { insertBacklink: jest.fn() }; - const eventEmitter = { emit: jest.fn() }; - const auditService = { logBatchWithContext: jest.fn() }; + const content = await runZipImport(md); - const pageService = { nextPagePosition: async () => 'a0' }; + const image = findFirst(content, 'image'); + expect(image).toBeTruthy(); + // The lossless sizing/alignment must survive the HTML hop. + expect(String(image.attrs?.width)).toBe('320'); + expect(image.attrs?.align).toBe('left'); - const service = new FileImportTaskService( - {} as any, // storageService - importService as any, - pageService as any, - backlinkRepo as any, - db, - importAttachmentService as any, - eventEmitter as any, - auditService as any, - ); - - const fileTask: any = { - id: 'task-1', - source: 'generic', - spaceId: 'space-1', - workspaceId: 'ws-1', - creatorId: 'user-1', - }; - - try { - await service.processGenericImport({ extractDir, fileTask }); - - expect(captured).toBeTruthy(); - const content = captured.content; - // Definitions ordered by FIRST REFERENCE (C, A, B), NOT the markdown - // definition order (A, B, C). Ids are the parser's fresh `fn-*`, so pin - // the BODIES. - expect(footnoteListBodies(content)).toEqual([ - 'note C', - 'note A', - 'note B', - ]); - // Orphan [^z] dropped; reused [^a] collapses to one definition; one list. - expect(footnoteListBodies(content)).not.toContain('orphan note'); - const lists = (content.content ?? []).filter( - (n: any) => n.type === 'footnotesList', - ); - expect(lists).toHaveLength(1); - expect( - footnoteListBodies(content).filter((b) => b === 'note A'), - ).toHaveLength(1); - } finally { - await fs.rm(extractDir, { recursive: true, force: true }); - } + const callout = findFirst(content, 'callout'); + expect(callout).toBeTruthy(); + expect(callout.attrs?.type).toBe('warning'); }); }); diff --git a/apps/server/src/integrations/import/utils/foreign-markdown.spec.ts b/apps/server/src/integrations/import/utils/foreign-markdown.spec.ts index 94318075..0e50dd2c 100644 --- a/apps/server/src/integrations/import/utils/foreign-markdown.spec.ts +++ b/apps/server/src/integrations/import/utils/foreign-markdown.spec.ts @@ -83,6 +83,49 @@ describe('normalizeForeignMarkdown — GFM reference footnotes', () => { const callouts = ':::info\nHi\n:::\n\n> [!warning]\n> Careful'; expect(normalizeForeignMarkdown(callouts)).toBe(callouts); }); + + it('strips a leading YAML front-matter block (Obsidian/Hugo/git-sync files)', () => { + const out = normalizeForeignMarkdown( + '---\ntitle: My Page\ntags: [a, b]\n---\n\n# Heading\n\nBody.', + ); + expect(out).toBe('# Heading\n\nBody.'); + // The front-matter must not leak into the body as a setext heading. + expect(out).not.toContain('title: My Page'); + expect(out).not.toContain('---'); + }); + + it('does not strip a horizontal rule that is not leading front-matter', () => { + const md = 'Intro paragraph.\n\n---\n\nAfter the rule.'; + expect(normalizeForeignMarkdown(md)).toBe(md); + }); + + it('is linear on a document with thousands of definitions (no quadratic blowup)', () => { + // F2(a): the pass-2 rewrite must be O(text), not O(text × defs). Build a + // pathological doc (many defs + many plain text lines) and assert it + // completes well under a second — a quadratic implementation took ~14s. + const N = 4000; + const refs = Array.from({ length: N }, (_, i) => `line ${i} plain text`).join('\n'); + const defs = Array.from({ length: N }, (_, i) => `[^n${i}]: def ${i}`).join('\n'); + const doc = `start[^n0] and[^n${N - 1}] end\n\n${refs}\n\n${defs}`; + const t0 = Date.now(); + const out = normalizeForeignMarkdown(doc); + const elapsed = Date.now() - t0; + expect(elapsed).toBeLessThan(2000); + // Sanity: the two real references were still inlined. + expect(out).toContain('^[def 0]'); + expect(out).toContain(`^[def ${N - 1}]`); + }); + + it('is bounded on a long unclosed backtick run (no inline-split ReDoS)', () => { + // F2(b): a huge unterminated backtick run must not cause quadratic + // backtracking in the inline-code split. Oversized lines skip the split + // entirely (left untouched), so this returns promptly. + const line = 'x' + '`'.repeat(200000); + const doc = `${line}\n\n[^1]: def`; + const t0 = Date.now(); + normalizeForeignMarkdown(doc); + expect(Date.now() - t0).toBeLessThan(2000); + }); }); describe('foreign markdown import acceptance (normalizer + canonical parser)', () => { diff --git a/apps/server/src/integrations/import/utils/foreign-markdown.ts b/apps/server/src/integrations/import/utils/foreign-markdown.ts index 531171fe..f0079aa0 100644 --- a/apps/server/src/integrations/import/utils/foreign-markdown.ts +++ b/apps/server/src/integrations/import/utils/foreign-markdown.ts @@ -74,10 +74,26 @@ function escapeFootnoteBody(body: string): string { * We split the line on inline-code spans (paired backtick runs) and rewrite only * the non-code segments. */ +// Above this length a single line is not split into inline-code spans (see +// below). A genuine markdown line carrying a footnote reference is never tens of +// KB; the cap only bypasses the inline-code protection for pathological lines. +const INLINE_SPLIT_MAX_LINE = 8192; + function rewriteRefsOutsideInlineCode( line: string, replace: (text: string) => string, ): string { + // The inline-code split alternation `(`+)(?:(?!\1)[\s\S])*\1` backtracks + // quadratically on a long UNCLOSED backtick run (its middle can consume the + // rest of the line, then fail to find a closing run and retry from each + // position). On an untrusted import this is a request-thread ReDoS. A real + // footnote line is short, so for an oversized line we skip the inline-code + // protection entirely and leave the line UNTOUCHED (rewriting it wholesale + // could corrupt a `[^id]` that legitimately lives inside inline code). This is + // a conservative bypass: an over-8KB line simply does not get its reference + // footnotes inlined — acceptable for a pathological input. + if (line.length > INLINE_SPLIT_MAX_LINE) return line; + // Alternation: an inline-code span (one or more backticks, then anything up to // the SAME run of backticks) OR a run of non-backtick text. Unterminated // backticks fall through as ordinary text (matched by the second branch on the @@ -161,6 +177,26 @@ function convertReferenceFootnotes(markdown: string): string { return markdown; } + // ONE precompiled alternation regex over ALL definition ids, built once per + // document (not once per definition per line). This makes pass 2 O(total text) + // instead of O(text × defs): a line with no reference pays a single failed + // scan, and the replacer looks the matched id up in `defs`. The previous + // per-def loop (`for (id) line.replace(new RegExp(...))`) was quadratic in the + // definition count — a modest upload with thousands of defs could freeze the + // request thread (and thus the whole instance, since import runs synchronously + // on it). The ids are escaped and joined; `defs` is the id→body lookup. + const refRe = new RegExp( + '\\[\\^(' + [...defs.keys()].map(escapeRegExp).join('|') + ')\\]', + 'g', + ); + const rewriteSegment = (segment: string): string => + segment.replace(refRe, (whole, id: string) => { + const body = defs.get(id); + // A ref whose id is not a real definition should not be reachable (the + // alternation only contains real ids), but stay defensive: leave it as-is. + return body === undefined ? whole : `^[${escapeFootnoteBody(body)}]`; + }); + // Pass 2: rewrite in-text references, skipping fenced code and dropped lines. const out: string[] = []; inFence = false; @@ -185,27 +221,33 @@ function convertReferenceFootnotes(markdown: string): string { continue; } - line = rewriteRefsOutsideInlineCode(line, (segment) => { - let s = segment; - for (const [id, body] of defs) { - const ref = new RegExp('\\[\\^' + escapeRegExp(id) + '\\]', 'g'); - s = s.replace(ref, `^[${escapeFootnoteBody(body)}]`); - } - return s; - }); + line = rewriteRefsOutsideInlineCode(line, rewriteSegment); out.push(line); } return out.join('\n'); } +/** + * Strip a single leading YAML front-matter block (`---\n…\n---`). Foreign files + * from Obsidian / Hugo / Jekyll / Notion — and Docmost's OWN git-sync page files + * — open with front-matter that the canonical parser does not consume, so + * without this it leaks into the body (and `title: Foo` above the closing `---` + * renders as a setext `

` that `extractTitleAndRemoveHeading` can hijack as + * the page title). This mirrors the strip the retired `markdownToHtml` layer did + * (editor-ext marked.utils.ts). It is a no-op for front-matter-free input. + */ +const YAML_FRONT_MATTER_RE = /^\s*---[\s\S]*?---\s*/; + /** * Normalize a foreign markdown string into Docmost's canonical markdown surface - * so the strict canonical parser accepts it losslessly. Currently this rewrites - * GFM reference footnotes into inline footnotes; add further fixture-driven - * foreign-surface cases here as they are found. + * so the strict canonical parser accepts it losslessly: strip a leading YAML + * front-matter block, then rewrite GFM reference footnotes into inline + * footnotes. Add further fixture-driven foreign-surface cases here as they are + * found. */ export function normalizeForeignMarkdown(markdown: string): string { if (!markdown) return markdown; - return convertReferenceFootnotes(markdown); + const withoutFrontMatter = markdown.replace(YAML_FRONT_MATTER_RE, '').trimStart(); + return convertReferenceFootnotes(withoutFrontMatter); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bedf2e5..f70155a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -750,6 +750,9 @@ importers: postmark: specifier: ^4.0.7 version: 4.0.7 + prom-client: + specifier: ^15.1.3 + version: 15.1.3 react: specifier: ^18.3.1 version: 18.3.1 @@ -5994,6 +5997,9 @@ packages: bind-event-listener@3.0.0: resolution: {integrity: sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==} + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -9324,6 +9330,10 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -10151,6 +10161,9 @@ packages: resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} engines: {node: '>=18'} + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + terser-webpack-plugin@5.4.0: resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} @@ -16671,6 +16684,8 @@ snapshots: bind-event-listener@3.0.0: {} + bintrees@1.0.2: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -20482,6 +20497,11 @@ snapshots: process-warning@5.0.0: {} + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.0 + tdigest: 0.1.2 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -21527,6 +21547,10 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + terser-webpack-plugin@5.4.0(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: '@jridgewell/trace-mapping': 0.3.31