chore: bump deps (#15124)

This commit is contained in:
DarkSky
2026-06-18 12:55:18 +08:00
committed by GitHub
parent 13d9fe506e
commit d500e472f0
30 changed files with 1239 additions and 504 deletions
+2 -2
View File
@@ -92,7 +92,7 @@
"nanoid": "^5.1.6",
"nest-winston": "^1.9.7",
"nestjs-cls": "^6.0.0",
"nodemailer": "^8.0.4",
"nodemailer": "^8.0.11",
"on-headers": "^1.1.0",
"piscina": "^5.1.4",
"prisma": "^6.6.0",
@@ -102,7 +102,7 @@
"rxjs": "^7.8.2",
"semver": "^7.7.4",
"ses": "^1.15.0",
"socket.io": "^4.8.1",
"socket.io": "^4.8.3",
"stripe": "^17.7.0",
"tldts": "^7.0.19",
"winston": "^3.17.0",
+1 -1
View File
@@ -51,7 +51,7 @@
"react-dom": "^19.2.1",
"react-hook-form": "^7.54.1",
"react-resizable-panels": "^3.0.6",
"react-router-dom": "^7.12.0",
"react-router-dom": "^7.18.0",
"recharts": "^2.15.4",
"sonner": "^2.0.7",
"swr": "^2.3.7",
+1 -1
View File
@@ -31,7 +31,7 @@
"next-themes": "^0.4.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-router-dom": "^6.30.3"
"react-router-dom": "^6.30.4"
},
"devDependencies": {
"@capacitor/cli": "^7.6.5",
@@ -23,7 +23,7 @@
"next-themes": "^0.4.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-router-dom": "^6.30.3",
"react-router-dom": "^6.30.4",
"uuid": "^14.0.0"
},
"devDependencies": {
+1 -1
View File
@@ -59,7 +59,7 @@
"electron-log": "^5.4.3",
"electron-squirrel-startup": "1.0.1",
"electron-window-state": "^5.0.3",
"esbuild": "^0.25.0",
"esbuild": "^0.25.12",
"fs-extra": "^11.2.0",
"glob": "^11.0.0",
"lodash-es": "^4.17.23",
+1 -1
View File
@@ -36,7 +36,7 @@
"next-themes": "^0.4.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-router-dom": "^6.30.3"
"react-router-dom": "^6.30.4"
},
"devDependencies": {
"@affine-tools/cli": "workspace:*",
+1 -1
View File
@@ -18,7 +18,7 @@
"@toeverything/infra": "workspace:*",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-router-dom": "^6.30.3"
"react-router-dom": "^6.30.4"
},
"devDependencies": {
"@types/react": "^19.0.1",
+1 -1
View File
@@ -18,7 +18,7 @@
"@toeverything/infra": "workspace:*",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-router-dom": "^6.30.3"
"react-router-dom": "^6.30.4"
},
"devDependencies": {
"@types/react": "^19.0.1",
+2 -2
View File
@@ -62,7 +62,7 @@
"react": "^19.2.1",
"react-dom": "19.2.1",
"react-paginate": "^8.3.0",
"react-router-dom": "^6.30.3",
"react-router-dom": "^6.30.4",
"react-transition-state": "^2.2.0",
"sonner": "^2.0.7",
"swr": "^2.3.7",
@@ -81,7 +81,7 @@
"storybook": "^10.2.14",
"typescript": "^5.9.3",
"unplugin-swc": "^1.5.9",
"vite": "^7.2.7",
"vite": "^7.3.5",
"vitest": "^4.1.8"
},
"version": "0.26.3"
+4 -2
View File
@@ -56,7 +56,7 @@
"cmdk": "^1.0.4",
"core-js": "^3.39.0",
"dayjs": "^1.11.13",
"dompurify": "^3.3.0",
"dompurify": "^3.4.11",
"eventemitter2": "^6.4.9",
"file-type": "^21.0.0",
"filesize": "^10.1.6",
@@ -82,7 +82,7 @@
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-error-boundary": "^6.0.0",
"react-router-dom": "^6.30.3",
"react-router-dom": "^6.30.4",
"react-transition-state": "^2.2.0",
"react-virtuoso": "^4.12.3",
"recharts": "^2.15.4",
@@ -98,6 +98,7 @@
},
"devDependencies": {
"@blocksuite/affine-ext-loader": "workspace:*",
"@playwright/test": "=1.58.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.1.0",
"@types/bytes": "^3.1.5",
@@ -107,6 +108,7 @@
"@vanilla-extract/css": "^1.17.0",
"fake-indexeddb": "^6.0.0",
"happy-dom": "^20.3.0",
"vite": "^7.3.5",
"vitest": "^4.1.8"
}
}
@@ -41,12 +41,12 @@ describe('preview render bridge', () => {
});
});
test('uses worker renderers and only sanitizes mermaid output', async () => {
test('uses worker renderers and sanitizes preview svg output', async () => {
mermaidRender.mockResolvedValue({
svg: '<svg><script>alert(1)</script><text>mermaid</text></svg>',
});
typstRender.mockResolvedValue({
svg: '<div><script>window.__xss__=1</script><svg><text>typst</text></svg></div>',
svg: '<svg><script>window.__xss__=1</script><text>typst</text></svg>',
});
const mermaid = await renderMermaidSvg({ code: 'flowchart TD;A-->B' });
@@ -57,9 +57,9 @@ describe('preview render bridge', () => {
expect(mermaid.svg).toContain('<svg');
expect(mermaid.svg).toContain('mermaid');
expect(mermaid.svg).not.toContain('<script');
expect(typst.svg).toBe(
'<div><script>window.__xss__=1</script><svg><text>typst</text></svg></div>'
);
expect(typst.svg).toContain('<svg');
expect(typst.svg).toContain('typst');
expect(typst.svg).not.toContain('<script');
});
test('sanitizeSvg keeps svg text nodes', () => {
@@ -101,6 +101,37 @@ describe('preview render bridge', () => {
expect(sanitized).not.toContain('<script');
});
test('sanitizeSvg wraps sanitized svg fragments back into root svg', () => {
if (typeof DOMParser === 'undefined') {
return;
}
domPurifySanitize.mockImplementation((value: unknown) => {
if (typeof value !== 'string') {
return '';
}
return '<rect width="100" height="100"></rect>';
});
const sanitized = sanitizeSvg(
'<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100"></rect></svg>'
);
expect(sanitized).toContain('<svg');
expect(sanitized).toContain('width="100"');
expect(sanitized).toContain('<rect');
});
test('throws when sanitized typst svg is empty', async () => {
typstRender.mockResolvedValue({
svg: '<div><text>invalid</text></div>',
});
await expect(renderTypstSvg({ code: '= Title' })).rejects.toThrow(
'Preview renderer returned invalid SVG.'
);
});
test('throws when sanitized svg is empty', async () => {
mermaidRender.mockResolvedValue({
svg: '<div><text>invalid</text></div>',
@@ -10,56 +10,10 @@ import type {
TypstRenderRequest,
TypstRenderResult,
} from '@affine/core/modules/typst/renderer';
import type { Config } from 'dompurify';
import DOMPurify from 'dompurify';
/** Mermaid SVG uses `<use>`, `<style>`, and sometimes `<foreignObject>` for labels. */
const MERMAID_SVG_SANITIZE_CONFIG: Config = {
USE_PROFILES: { svg: true },
ADD_TAGS: ['use'],
ADD_ATTR: ['href', 'xlink:href', 'class', 'style', 'id'],
};
const FOREIGN_OBJECT_HTML_SANITIZE_CONFIG: Config = {
USE_PROFILES: { html: true },
};
function sanitizeForeignObjects(root: ParentNode) {
root.querySelectorAll('foreignObject, foreignobject').forEach(element => {
element.innerHTML = DOMPurify.sanitize(
element.innerHTML,
FOREIGN_OBJECT_HTML_SANITIZE_CONFIG
);
});
}
import { sanitizeSvg as sanitizeSvgDocument } from '@blocksuite/affine-shared/utils';
export function sanitizeSvg(svg: string): string {
if (
typeof DOMParser === 'undefined' ||
typeof XMLSerializer === 'undefined'
) {
const sanitized = DOMPurify.sanitize(svg, MERMAID_SVG_SANITIZE_CONFIG);
if (typeof sanitized !== 'string' || !/^\s*<svg[\s>]/i.test(sanitized)) {
return '';
}
return sanitized.trim();
}
const parser = new DOMParser();
const parsed = parser.parseFromString(svg, 'image/svg+xml');
const root = parsed.documentElement;
if (!root || root.tagName.toLowerCase() !== 'svg') return '';
const sanitized = DOMPurify.sanitize(root, MERMAID_SVG_SANITIZE_CONFIG);
if (typeof sanitized !== 'string') return '';
const sanitizedDoc = parser.parseFromString(sanitized, 'image/svg+xml');
const sanitizedRoot = sanitizedDoc.documentElement;
if (!sanitizedRoot || sanitizedRoot.tagName.toLowerCase() !== 'svg')
return '';
sanitizeForeignObjects(sanitizedRoot);
return new XMLSerializer().serializeToString(sanitizedRoot).trim();
return sanitizeSvgDocument(svg);
}
export async function renderMermaidSvg(
@@ -79,5 +33,9 @@ export async function renderTypstSvg(
): Promise<TypstRenderResult> {
const rendered = await renderTypstSvgBackend(request);
return { svg: rendered.svg };
const sanitizedSvg = sanitizeSvgDocument(rendered.svg);
if (!sanitizedSvg) {
throw new Error('Preview renderer returned invalid SVG.');
}
return { svg: sanitizedSvg };
}
@@ -90,4 +90,14 @@ describe('renderClassicMermaidSvg', () => {
})
);
});
test('keeps mermaid classic renderer in strict security mode', async () => {
render.mockResolvedValue({ svg: '<svg>ok</svg>' });
await renderClassicMermaidSvg({ code: 'flowchart TD;A-->B' });
expect(initialize).toHaveBeenCalledWith(
expect.objectContaining({ securityLevel: 'strict' })
);
});
});
@@ -1,39 +1,179 @@
/**
* @vitest-environment happy-dom
* @vitest-environment node
*/
import { describe, expect, test } from 'vitest';
import { createReadStream } from 'node:fs';
import { createServer, type Server } from 'node:http';
import { extname, join, resolve } from 'node:path';
import { sanitizeSvg } from './bridge';
import { renderClassicMermaidSvg } from './classic-mermaid';
import { type Browser, chromium } from '@playwright/test';
import type DOMPurifyDefault from 'dompurify';
import type { Mermaid } from 'mermaid';
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
const canRunDomIntegration =
typeof document !== 'undefined' &&
typeof DOMParser !== 'undefined' &&
typeof XMLSerializer !== 'undefined';
const workspaceRoot = resolve('.');
describe.skipIf(!canRunDomIntegration)('mermaid preview integration', () => {
test('flowchart labels survive classic render and svg sanitization', async () => {
const { svg: raw } = await renderClassicMermaidSvg({
code: 'flowchart TD; A-->B',
options: { theme: 'default' },
});
let server: Server;
let serverUrl: string;
let browser: Browser;
expect(raw).toMatch(/<svg[\s>]/i);
function contentType(path: string) {
switch (extname(path)) {
case '.js':
case '.mjs':
return 'text/javascript';
case '.json':
return 'application/json';
default:
return 'application/octet-stream';
}
}
const sanitized = sanitizeSvg(raw);
expect(sanitized).toMatch(/<svg[\s>]/i);
// happy-dom cannot lay out Mermaid (CSSStyleSheet); skip empty output.
if (!/<(?:rect|path|circle|polygon)\b/i.test(sanitized)) {
beforeAll(async () => {
server = createServer((request, response) => {
const url = new URL(request.url ?? '/', 'http://localhost');
if (url.pathname === '/') {
response.setHeader('Content-Type', 'text/html');
response.end('<!doctype html><html><body></body></html>');
return;
}
const hasLabelText =
/>\s*A\s*</i.test(sanitized) ||
/>\s*B\s*</i.test(sanitized) ||
/foreignObject[\s\S]*>\s*A\s*</i.test(sanitized) ||
/<tspan[^>]*>\s*A\s*</i.test(sanitized);
const filePath = resolve(
join(workspaceRoot, decodeURIComponent(url.pathname))
);
expect(hasLabelText).toBe(true);
if (!filePath.startsWith(workspaceRoot)) {
response.writeHead(403);
response.end();
return;
}
response.setHeader('Content-Type', contentType(filePath));
response.writeHead(200);
createReadStream(filePath)
.on('error', () => {
if (!response.headersSent) {
response.writeHead(404);
}
response.end();
})
.pipe(response);
});
await new Promise<void>(resolve => {
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Failed to start mermaid preview test server.');
}
serverUrl = `http://127.0.0.1:${address.port}`;
browser = await chromium.launch({ headless: true });
});
afterAll(async () => {
await browser?.close();
await new Promise<void>(resolve => server.close(() => resolve()));
});
describe('mermaid preview integration', () => {
test('flowchart labels survive strict classic render and svg sanitization in browser', async () => {
const page = await browser.newPage();
try {
await page.goto(serverUrl);
const result = await page.evaluate(async () => {
const browserImport = new Function('url', 'return import(url)') as <T>(
url: string
) => Promise<T>;
const mermaid = (
await browserImport<{ default: Mermaid }>(
'/node_modules/mermaid/dist/mermaid.esm.mjs'
)
).default;
const DOMPurify = (
await browserImport<{ default: typeof DOMPurifyDefault }>(
'/node_modules/dompurify/dist/purify.es.mjs'
)
).default;
const sanitizeSvg = (svg: string) => {
const sanitizeConfig = {
USE_PROFILES: { svg: true },
ADD_TAGS: ['use'],
ADD_ATTR: ['href', 'xlink:href', 'class', 'style', 'id'],
};
const foreignObjectConfig = {
USE_PROFILES: { html: true },
};
const parser = new DOMParser();
const parsed = parser.parseFromString(svg, 'image/svg+xml');
const root = parsed.documentElement;
if (!root || root.tagName.toLowerCase() !== 'svg') return '';
const sanitized = DOMPurify.sanitize(root, sanitizeConfig);
if (typeof sanitized !== 'string') return '';
const sanitizedDoc = parser.parseFromString(
sanitized,
'image/svg+xml'
);
const sanitizedRoot = sanitizedDoc.documentElement;
if (!sanitizedRoot || sanitizedRoot.tagName.toLowerCase() !== 'svg') {
return '';
}
sanitizedRoot
.querySelectorAll('foreignObject, foreignobject')
.forEach(element => {
element.innerHTML = DOMPurify.sanitize(
element.innerHTML,
foreignObjectConfig
);
});
return new XMLSerializer().serializeToString(sanitizedRoot).trim();
};
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'strict',
htmlLabels: false,
fontFamily: 'IBM Plex Mono',
flowchart: { useMaxWidth: true },
sequence: { useMaxWidth: true },
gantt: { useMaxWidth: true },
pie: { useMaxWidth: true },
journey: { useMaxWidth: true },
gitGraph: { useMaxWidth: true },
});
const { svg: raw } = await mermaid.render(
`mermaid-diagram-${Date.now()}`,
'flowchart TD; A-->B'
);
const sanitized = sanitizeSvg(raw);
return {
raw,
sanitized,
hasLabelText:
/>\s*A\s*</i.test(sanitized) ||
/>\s*B\s*</i.test(sanitized) ||
/foreignObject[\s\S]*>\s*A\s*</i.test(sanitized) ||
/<tspan[^>]*>\s*A\s*</i.test(sanitized),
};
});
expect(result.raw).toMatch(/<svg[\s>]/i);
expect(result.sanitized).toMatch(/<svg[\s>]/i);
expect(result.sanitized).toMatch(/<(?:rect|path|circle|polygon)\b/i);
expect(result.hasLabelText).toBe(true);
} finally {
await page.close();
}
}, 30_000);
});
@@ -0,0 +1,130 @@
/**
* @vitest-environment node
*/
import { resolve } from 'node:path';
import { type Browser, chromium } from '@playwright/test';
import { createServer, type ViteDevServer } from 'vite';
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
const typstDemo = String.raw`#set page(paper: "a5")
#set heading(numbering: "1.")
#show link: set text(fill: blue, weight: 700)
#show link: underline
= The Typst Playground
Welcome to the Typst Playground! This is a sandbox where you can experiment with Typst.
= Basics <basics>
Typst is a _markup_ language. You use it to express not just the content, but also the structure and formatting of your document.
- *Strongly emphasize* some text
- Refer to @basics
- Typeset math: $a, b in { 1/2, sqrt(4 a b) }$
= Next steps
To learn more about Typst, check out https://typst.app/docs/tutorial.`;
let server: ViteDevServer;
let serverUrl: string;
let browser: Browser;
const typstRuntimeUrl = `/@fs/${resolve(
'packages/frontend/core/src/modules/typst/renderer/runtime.ts'
)}`;
const svgUtilsUrl = `/@fs/${resolve(
'blocksuite/affine/shared/src/utils/svg.ts'
)}`;
beforeAll(async () => {
server = await createServer({
logLevel: 'silent',
server: {
host: '127.0.0.1',
port: 0,
},
});
await server.listen();
const address = server.httpServer?.address();
if (!address || typeof address === 'string') {
throw new Error('Failed to start typst preview test server.');
}
serverUrl = `http://127.0.0.1:${address.port}`;
browser = await chromium.launch({ headless: true });
});
afterAll(async () => {
await browser?.close();
await server?.close();
});
describe('typst preview integration', () => {
test('sanitization preserves rendered glyphs, styles, and link colors', async () => {
const page = await browser.newPage();
try {
await page.goto(serverUrl);
const result = await page.evaluate(
async ({ code, svgUtilsUrl, typstRuntimeUrl }) => {
const browserImport = new Function('url', 'return import(url)') as <
T,
>(
url: string
) => Promise<T>;
const [{ renderTypstSvgWithOptions }, { sanitizeSvg }] =
await Promise.all([
browserImport<{
renderTypstSvgWithOptions: (
code: string,
options: { fontUrls: string[] }
) => Promise<{ svg: string }>;
}>(typstRuntimeUrl),
browserImport<{
sanitizeSvg: (svg: string) => string;
}>(svgUtilsUrl),
]);
const { svg: raw } = await renderTypstSvgWithOptions(code, {
fontUrls: [],
});
const sanitized = sanitizeSvg(raw);
const parse = (svg: string) =>
new DOMParser().parseFromString(svg, 'image/svg+xml');
const rawDoc = parse(raw);
const sanitizedDoc = parse(sanitized);
return {
linkHrefs: Array.from(sanitizedDoc.querySelectorAll('a')).map(
element =>
element.getAttribute('href') ??
element.getAttribute('xlink:href') ??
''
),
rawUseCount: rawDoc.querySelectorAll('use').length,
sanitized,
sanitizedRoot: sanitizedDoc.documentElement.tagName,
sanitizedUseCount: sanitizedDoc.querySelectorAll('use').length,
hasParserError: !!sanitizedDoc.querySelector('parsererror'),
};
},
{ code: typstDemo, svgUtilsUrl, typstRuntimeUrl }
);
expect(result.sanitizedRoot).toBe('svg');
expect(result.hasParserError).toBe(false);
expect(result.rawUseCount).toBeGreaterThan(0);
expect(result.sanitizedUseCount).toBe(result.rawUseCount);
expect(result.linkHrefs).toContain('https://typst.app/docs/tutorial');
expect(result.sanitized).toContain('typst-text');
expect(result.sanitized).toContain('#0074d9');
expect(result.sanitized).not.toContain('<script');
} finally {
await page.close();
}
}, 30_000);
});
@@ -25,7 +25,7 @@
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-markdown": "^10.1.0",
"socket.io": "^4.7.4",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"swr": "^2.3.7",
"tailwindcss": "^4.1.17"
+1 -1
View File
@@ -17,6 +17,6 @@
},
"peerDependencies": {
"react": "^19.2.1",
"react-router-dom": "^7.12.0"
"react-router-dom": "^7.18.0"
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
"@affine/debug": "workspace:*",
"@sentry/react": "^10.53.1",
"nanoid": "^5.1.6",
"react-router-dom": "^6.30.3"
"react-router-dom": "^6.30.4"
},
"devDependencies": {
"@types/react": "^19.0.1",