diff --git a/package.json b/package.json index d7358c7ad8..aee33457d0 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,6 @@ "js-yaml": "^4.1.1", "jsdom": "^22.1.0", "jsonwebtoken": "^9.0.3", - "jszip": "^3.10.1", "katex": "^0.16.45", "kbar": "0.1.0-beta.48", "koa": "^3.0.3", @@ -271,6 +270,7 @@ "y-prosemirror": "^1.3.7", "y-protocols": "^1.0.7", "yauzl": "^3.2.1", + "yazl": "^3.3.1", "yjs": "^13.6.30", "zod": "^4.3.6" }, @@ -348,6 +348,7 @@ "@types/utf8": "^3.0.3", "@types/validator": "^13.15.10", "@types/yauzl": "^2.10.3", + "@types/yazl": "^2.4.6", "@vitest/ui": "^4.1.6", "babel-plugin-module-resolver": "^5.0.3", "babel-plugin-styled-components": "^2.1.4", diff --git a/server/queues/tasks/ExportDocumentTreeTask.ts b/server/queues/tasks/ExportDocumentTreeTask.ts index 448d09ab0f..cb01ff0bbd 100644 --- a/server/queues/tasks/ExportDocumentTreeTask.ts +++ b/server/queues/tasks/ExportDocumentTreeTask.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import type JSZip from "jszip"; import { escapeRegExp } from "es-toolkit/compat"; +import type { ZipFile } from "yazl"; import type { NavigationNode } from "@shared/types"; import { FileOperationFormat } from "@shared/types"; import Logger from "@server/logging/Logger"; @@ -17,7 +17,7 @@ export default abstract class ExportDocumentTreeTask extends ExportTask { /** * Exports the document tree to the given zip instance. * - * @param zip The JSZip instance to add files to + * @param zip The yazl ZipFile to add files to * @param documentId The document ID to export * @param pathInZip The path in the zip to add the document to * @param format The format to export in @@ -30,7 +30,7 @@ export default abstract class ExportDocumentTreeTask extends ExportTask { includeAttachments, pathMap, }: { - zip: JSZip; + zip: ZipFile; pathInZip: string; documentId: string; format: FileOperationFormat; @@ -64,38 +64,33 @@ export default abstract class ExportDocumentTreeTask extends ExportTask { // Add any referenced attachments to the zip file and replace the // reference in the document with the path to the attachment in the zip - await Promise.all( - attachments.map(async (attachment) => { - Logger.debug("task", `Adding attachment to archive`, { - documentId, - key: attachment.key, + for (const attachment of attachments) { + Logger.debug("task", `Adding attachment to archive`, { + documentId, + key: attachment.key, + }); + + const dir = path.dirname(pathInZip); + let buffer: Buffer; + try { + buffer = await attachment.buffer; + } catch (err) { + Logger.warn(`Failed to read attachment from storage`, { + attachmentId: attachment.id, + teamId: attachment.teamId, + error: err.message, }); + buffer = Buffer.from(""); + } + zip.addBuffer(buffer, path.join(dir, attachment.key), { + mtime: attachment.updatedAt, + }); - const dir = path.dirname(pathInZip); - zip.file( - path.join(dir, attachment.key), - new Promise((resolve) => { - attachment.buffer.then(resolve).catch((err) => { - Logger.warn(`Failed to read attachment from storage`, { - attachmentId: attachment.id, - teamId: attachment.teamId, - error: err.message, - }); - resolve(Buffer.from("")); - }); - }), - { - date: attachment.updatedAt, - createFolders: true, - } - ); - - text = text.replace( - new RegExp(escapeRegExp(attachment.redirectUrl), "g"), - encodeURI(attachment.key) - ); - }) - ); + text = text.replace( + new RegExp(escapeRegExp(attachment.redirectUrl), "g"), + encodeURI(attachment.key) + ); + } // Replace any internal links with relative paths to the document in the zip const internalLinks = [ @@ -117,10 +112,9 @@ export default abstract class ExportDocumentTreeTask extends ExportTask { }); // Finally, add the document to the zip file - zip.file(pathInZip, text, { - date: document.updatedAt, - createFolders: true, - comment: JSON.stringify({ + zip.addBuffer(Buffer.from(text), pathInZip, { + mtime: document.updatedAt, + fileComment: JSON.stringify({ createdAt: document.createdAt, updatedAt: document.updatedAt, }), @@ -131,7 +125,7 @@ export default abstract class ExportDocumentTreeTask extends ExportTask { * Exports the documents and attachments in the given collections to a zip file * and returns the path to the zip file in tmp. * - * @param zip The JSZip instance to add files to + * @param zip The yazl ZipFile to add files to * @param collections The collections to export * @param format The format to export in * @param includeAttachments Whether to include attachments in the export @@ -139,7 +133,7 @@ export default abstract class ExportDocumentTreeTask extends ExportTask { * @returns The path to the zip file in tmp. */ protected async addCollectionsToArchive( - zip: JSZip, + zip: ZipFile, collections: Collection[], format: FileOperationFormat, includeAttachments = true @@ -178,7 +172,7 @@ export default abstract class ExportDocumentTreeTask extends ExportTask { document: Document; format: FileOperationFormat; documentStructure: NavigationNode[]; - zip: JSZip; + zip: ZipFile; }) { const pathMap = new Map(); diff --git a/server/queues/tasks/ExportHTMLZipTask.ts b/server/queues/tasks/ExportHTMLZipTask.ts index 72d6d90b3b..a96ab596f6 100644 --- a/server/queues/tasks/ExportHTMLZipTask.ts +++ b/server/queues/tasks/ExportHTMLZipTask.ts @@ -1,4 +1,4 @@ -import JSZip from "jszip"; +import { ZipFile } from "yazl"; import type { NavigationNode } from "@shared/types"; import { FileOperationFormat } from "@shared/types"; import type { Collection, FileOperation } from "@server/models"; @@ -10,7 +10,7 @@ export default class ExportHTMLZipTask extends ExportDocumentTreeTask { collections: Collection[], fileOperation: FileOperation ) { - const zip = new JSZip(); + const zip = new ZipFile(); return await this.addCollectionsToArchive( zip, @@ -24,7 +24,7 @@ export default class ExportHTMLZipTask extends ExportDocumentTreeTask { document: Document, documentStructure: NavigationNode[] ): Promise { - const zip = new JSZip(); + const zip = new ZipFile(); return await this.addDocumentToArchive({ document, diff --git a/server/queues/tasks/ExportJSONTask.ts b/server/queues/tasks/ExportJSONTask.ts index 401b4aec8c..685c989fd0 100644 --- a/server/queues/tasks/ExportJSONTask.ts +++ b/server/queues/tasks/ExportJSONTask.ts @@ -1,4 +1,4 @@ -import JSZip from "jszip"; +import { ZipFile } from "yazl"; import { omit } from "es-toolkit/compat"; import type { NavigationNode } from "@shared/types"; import env from "@server/env"; @@ -19,7 +19,7 @@ export default class ExportJSONTask extends ExportTask { collections: Collection[], fileOperation: FileOperation ) { - const zip = new JSZip(); + const zip = new ZipFile(); const usedFilenames = new Set(); // serial to avoid overloading, slow and steady wins the race @@ -44,7 +44,10 @@ export default class ExportJSONTask extends ExportTask { return ZipHelper.toTmpFile(zip); } - private async addMetadataToArchive(zip: JSZip, fileOperation: FileOperation) { + private async addMetadataToArchive( + zip: ZipFile, + fileOperation: FileOperation + ) { const user = await fileOperation.$get("user"); const metadata: JSONExportMetadata = { @@ -55,16 +58,18 @@ export default class ExportJSONTask extends ExportTask { createdByEmail: user?.email ?? null, }; - zip.file( - `metadata.json`, - env.isDevelopment - ? JSON.stringify(metadata, null, 2) - : JSON.stringify(metadata) + zip.addBuffer( + Buffer.from( + env.isDevelopment + ? JSON.stringify(metadata, null, 2) + : JSON.stringify(metadata) + ), + `metadata.json` ); } private async addCollectionToArchive( - zip: JSZip, + zip: ZipFile, collection: Collection, includeAttachments: boolean, filename: string @@ -82,32 +87,27 @@ export default class ExportJSONTask extends ExportTask { }; async function addAttachments(attachments: Attachment[]) { - await Promise.all( - attachments.map(async (attachment) => { - zip.file( - attachment.key, - new Promise((resolve) => { - attachment.buffer.then(resolve).catch((err) => { - Logger.warn(`Failed to read attachment from storage`, { - attachmentId: attachment.id, - teamId: attachment.teamId, - error: err.message, - }); - resolve(Buffer.from("")); - }); - }), - { - date: attachment.updatedAt, - createFolders: true, - } - ); + for (const attachment of attachments) { + let buffer: Buffer; + try { + buffer = await attachment.buffer; + } catch (err) { + Logger.warn(`Failed to read attachment from storage`, { + attachmentId: attachment.id, + teamId: attachment.teamId, + error: err.message, + }); + buffer = Buffer.from(""); + } + zip.addBuffer(buffer, attachment.key, { + mtime: attachment.updatedAt, + }); - output.attachments[attachment.id] = { - ...omit(presentAttachment(attachment), "url"), - key: attachment.key, - }; - }) - ); + output.attachments[attachment.id] = { + ...omit(presentAttachment(attachment), "url"), + key: attachment.key, + }; + } } async function addDocumentTree(nodes: NavigationNode[]) { @@ -175,11 +175,13 @@ export default class ExportJSONTask extends ExportTask { await addDocumentTree(collection.documentStructure); } - zip.file( - `${filename}.json`, - env.isDevelopment - ? JSON.stringify(output, null, 2) - : JSON.stringify(output) + zip.addBuffer( + Buffer.from( + env.isDevelopment + ? JSON.stringify(output, null, 2) + : JSON.stringify(output) + ), + `${filename}.json` ); } diff --git a/server/queues/tasks/ExportMarkdownZipTask.ts b/server/queues/tasks/ExportMarkdownZipTask.ts index a3913328ef..6de91515d4 100644 --- a/server/queues/tasks/ExportMarkdownZipTask.ts +++ b/server/queues/tasks/ExportMarkdownZipTask.ts @@ -1,4 +1,4 @@ -import JSZip from "jszip"; +import { ZipFile } from "yazl"; import type { NavigationNode } from "@shared/types"; import { FileOperationFormat } from "@shared/types"; import type { Collection, FileOperation } from "@server/models"; @@ -10,7 +10,7 @@ export default class ExportMarkdownZipTask extends ExportDocumentTreeTask { collections: Collection[], fileOperation: FileOperation ) { - const zip = new JSZip(); + const zip = new ZipFile(); return await this.addCollectionsToArchive( zip, @@ -24,7 +24,7 @@ export default class ExportMarkdownZipTask extends ExportDocumentTreeTask { document: Document, documentStructure: NavigationNode[] ): Promise { - const zip = new JSZip(); + const zip = new ZipFile(); return await this.addDocumentToArchive({ document, diff --git a/server/routes/api/documents/documents.ts b/server/routes/api/documents/documents.ts index 8add886761..6928460dec 100644 --- a/server/routes/api/documents/documents.ts +++ b/server/routes/api/documents/documents.ts @@ -3,7 +3,6 @@ import fractionalIndex from "fractional-index"; import fs from "fs-extra"; import invariant from "invariant"; import contentDisposition from "content-disposition"; -import JSZip from "jszip"; import Router from "koa-router"; import { escapeRegExp, has, remove, uniq } from "es-toolkit/compat"; import mime from "mime-types"; @@ -84,8 +83,8 @@ import EmptyTrashTask from "@server/queues/tasks/EmptyTrashTask"; import FileStorage from "@server/storage/files"; import type { APIContext } from "@server/types"; import { RateLimiterStrategy } from "@server/utils/RateLimiter"; -import ZipHelper from "@server/utils/ZipHelper"; import { convertBareUrlsToEmbedMarkdown } from "@server/utils/embeds"; +import { streamZipResponse } from "@server/utils/koa"; import { getTeamFromContext } from "@server/utils/passport"; import { assertPresent } from "@server/validation"; import pagination, { paginateQuery } from "../middlewares/pagination"; @@ -890,51 +889,35 @@ router.post( return; } - const zip = new JSZip(); - - await Promise.all( - attachments.map(async (attachment) => { + await streamZipResponse(ctx, `${fileName}.zip`, async (zip) => { + for (const attachment of attachments) { const location = path.join( "attachments", `${attachment.id}.${mime.extension(attachment.contentType)}` ); - zip.file( - location, - new Promise((resolve) => { - attachment.buffer.then(resolve).catch((err) => { - Logger.warn(`Failed to read attachment from storage`, { - attachmentId: attachment.id, - teamId: attachment.teamId, - error: err.message, - }); - resolve(Buffer.from("")); - }); - }), - { - date: attachment.updatedAt, - createFolders: true, - } - ); + let buffer: Buffer; + try { + buffer = await attachment.buffer; + } catch (err) { + Logger.warn(`Failed to read attachment from storage`, { + attachmentId: attachment.id, + teamId: attachment.teamId, + error: err.message, + }); + buffer = Buffer.from(""); + } + zip.addBuffer(buffer, location, { mtime: attachment.updatedAt }); content = content.replace( new RegExp(escapeRegExp(attachment.redirectUrl), "g"), location ); - }) - ); + } - zip.file(`${fileName}.${extension}`, content, { - date: document.updatedAt, + zip.addBuffer(Buffer.from(content), `${fileName}.${extension}`, { + mtime: document.updatedAt, + }); }); - - ctx.set("Content-Type", "application/zip"); - ctx.set( - "Content-Disposition", - contentDisposition(`${fileName}.zip`, { - type: "attachment", - }) - ); - ctx.body = zip.generateNodeStream(ZipHelper.defaultStreamOptions); } ); diff --git a/server/routes/api/middlewares/apiResponse.ts b/server/routes/api/middlewares/apiResponse.ts index 9ab5fe88ed..afcc0a0c07 100644 --- a/server/routes/api/middlewares/apiResponse.ts +++ b/server/routes/api/middlewares/apiResponse.ts @@ -11,10 +11,7 @@ export default function apiResponse() { typeof ctx.body === "object" && !(ctx.body instanceof Readable) && !(ctx.body instanceof stream.Readable) && - !(ctx.body instanceof Buffer) && - // JSZip returns a wrapped stream instance that is not a true readable stream - // and not exported from the module either, so we must identify it like so. - !(ctx.body && "_readableState" in ctx.body) + !(ctx.body instanceof Buffer) ) { ctx.body = { ...ctx.body, diff --git a/server/routes/api/revisions/revisions.ts b/server/routes/api/revisions/revisions.ts index b914ad83e6..08f73afb5b 100644 --- a/server/routes/api/revisions/revisions.ts +++ b/server/routes/api/revisions/revisions.ts @@ -1,7 +1,6 @@ import path from "node:path"; import Router from "koa-router"; import contentDisposition from "content-disposition"; -import JSZip from "jszip"; import { escapeRegExp } from "es-toolkit/compat"; import mime from "mime-types"; import { UserRole } from "@shared/types"; @@ -20,7 +19,7 @@ import { authorize } from "@server/policies"; import { presentPolicies, presentRevision } from "@server/presenters"; import type { APIContext } from "@server/types"; import { RateLimiterStrategy } from "@server/utils/RateLimiter"; -import ZipHelper from "@server/utils/ZipHelper"; +import { streamZipResponse } from "@server/utils/koa"; import pagination from "../middlewares/pagination"; import * as T from "./schema"; @@ -195,51 +194,35 @@ router.post( return; } - const zip = new JSZip(); - - await Promise.all( - attachments.map(async (attachment) => { + await streamZipResponse(ctx, `${fileName}.zip`, async (zip) => { + for (const attachment of attachments) { const location = path.join( "attachments", `${attachment.id}.${mime.extension(attachment.contentType)}` ); - zip.file( - location, - new Promise((resolve) => { - attachment.buffer.then(resolve).catch((err) => { - Logger.warn(`Failed to read attachment from storage`, { - attachmentId: attachment.id, - teamId: attachment.teamId, - error: err.message, - }); - resolve(Buffer.from("")); - }); - }), - { - date: attachment.updatedAt, - createFolders: true, - } - ); + let buffer: Buffer; + try { + buffer = await attachment.buffer; + } catch (err) { + Logger.warn(`Failed to read attachment from storage`, { + attachmentId: attachment.id, + teamId: attachment.teamId, + error: err.message, + }); + buffer = Buffer.from(""); + } + zip.addBuffer(buffer, location, { mtime: attachment.updatedAt }); content = content.replace( new RegExp(escapeRegExp(attachment.redirectUrl), "g"), location ); - }) - ); + } - zip.file(`${fileName}.${extension}`, content, { - date: revision.updatedAt, + zip.addBuffer(Buffer.from(content), `${fileName}.${extension}`, { + mtime: revision.updatedAt, + }); }); - - ctx.set("Content-Type", "application/zip"); - ctx.set( - "Content-Disposition", - contentDisposition(`${fileName}.zip`, { - type: "attachment", - }) - ); - ctx.body = zip.generateNodeStream(ZipHelper.defaultStreamOptions); } ); diff --git a/server/typings/yazl.d.ts b/server/typings/yazl.d.ts new file mode 100644 index 0000000000..7e97ce9622 --- /dev/null +++ b/server/typings/yazl.d.ts @@ -0,0 +1,7 @@ +import "yazl"; + +declare module "yazl" { + interface Options { + fileComment: string | Buffer; + } +} diff --git a/server/utils/ZipHelper.test.ts b/server/utils/ZipHelper.test.ts index c2f6bdcdf1..a6fa0e5338 100644 --- a/server/utils/ZipHelper.test.ts +++ b/server/utils/ZipHelper.test.ts @@ -1,20 +1,26 @@ import path from "node:path"; import fs from "fs-extra"; -import JSZip from "jszip"; import tmp from "tmp"; +import { ZipFile } from "yazl"; import ZipHelper from "./ZipHelper"; async function writeZip( entries: Record, postfix = ".zip" ): Promise { - const zip = new JSZip(); + const zip = new ZipFile(); for (const [name, content] of Object.entries(entries)) { - zip.file(name, content); + zip.addBuffer(Buffer.from(content), name); } - const buffer = await zip.generateAsync({ type: "nodebuffer" }); const zipPath = tmp.fileSync({ postfix }).name; - await fs.writeFile(zipPath, buffer); + await new Promise((resolve, reject) => { + const dest = fs + .createWriteStream(zipPath) + .on("finish", () => resolve()) + .on("error", reject); + zip.outputStream.on("error", reject).pipe(dest); + zip.end(); + }); return zipPath; } diff --git a/server/utils/ZipHelper.ts b/server/utils/ZipHelper.ts index 8f6c373840..f54faebef2 100644 --- a/server/utils/ZipHelper.ts +++ b/server/utils/ZipHelper.ts @@ -1,9 +1,9 @@ import path from "node:path"; import fs from "fs-extra"; -import type JSZip from "jszip"; import tmp from "tmp"; import type { Entry } from "yauzl"; import yauzl, { validateFileName } from "yauzl"; +import type { ZipFile } from "yazl"; import { bytesToHumanReadable } from "@shared/utils/files"; import { ValidationError } from "@server/errors"; import Logger from "@server/logging/Logger"; @@ -41,27 +41,16 @@ export interface ZipTreeNode { @trace() export default class ZipHelper { - public static defaultStreamOptions: JSZip.JSZipGeneratorOptions<"nodebuffer"> = - { - type: "nodebuffer", - streamFiles: true, - compression: "DEFLATE", - compressionOptions: { - level: 5, - }, - }; - /** - * Write a zip file to a temporary disk location + * Write a zip file to a temporary disk location. * - * @deprecated Use `extract` instead - * @param zip JSZip object - * @returns pathname of the temporary file where the zip was written to disk + * The caller is responsible for adding entries to the `ZipFile`; this method + * calls `end()` and waits for the output stream to drain to disk. + * + * @param zip yazl ZipFile object with entries already added. + * @returns pathname of the temporary file where the zip was written to disk. */ - public static async toTmpFile( - zip: JSZip, - options?: JSZip.JSZipGeneratorOptions<"nodebuffer"> - ): Promise { + public static async toTmpFile(zip: ZipFile): Promise { Logger.debug("utils", "Creating tmp file…"); return new Promise((resolve, reject) => { tmp.file( @@ -74,11 +63,6 @@ export default class ZipHelper { return reject(err); } - let previousMetadata: JSZip.JSZipMetadata = { - percent: 0, - currentFile: null, - }; - const handleError = (error: Error) => { dest.destroy(); fs.remove(filePath) @@ -98,35 +82,8 @@ export default class ZipHelper { }) .on("error", handleError); - zip - .generateNodeStream( - { - ...this.defaultStreamOptions, - ...options, - }, - (metadata) => { - if (metadata.currentFile !== previousMetadata.currentFile) { - const percent = Math.round(metadata.percent); - const memory = process.memoryUsage(); - - previousMetadata = { - currentFile: metadata.currentFile, - percent, - }; - Logger.debug( - "utils", - `Writing zip file progress… ${percent}%`, - { - currentFile: metadata.currentFile, - memory: bytesToHumanReadable(memory.rss), - } - ); - } - } - ) - .on("error", handleError) - .pipe(dest) - .on("error", handleError); + zip.outputStream.on("error", handleError).pipe(dest); + zip.end(); } ); }); diff --git a/server/utils/koa.ts b/server/utils/koa.ts index 75c21dd493..5a0a908e19 100644 --- a/server/utils/koa.ts +++ b/server/utils/koa.ts @@ -1,6 +1,8 @@ +import contentDisposition from "content-disposition"; import type formidable from "formidable"; -import type { Request } from "koa"; +import type { Context, Request } from "koa"; import { isArray } from "es-toolkit/compat"; +import { ZipFile } from "yazl"; /** * Get the first file from an incoming koa request @@ -23,3 +25,35 @@ export const getFileFromRequest = ( return isArray(file) ? file[0] : file; }; + +/** + * Stream a freshly-built zip archive as the response body. The supplied + * `build` callback receives a yazl ZipFile to populate with entries; the + * helper handles response headers, error forwarding, and finalizing the + * archive once `build` resolves. + * + * @param ctx The koa context to write the response to. + * @param fileName The filename to advertise in the Content-Disposition header. + * @param build Callback that adds entries to the provided ZipFile. + */ +export const streamZipResponse = async ( + ctx: Context, + fileName: string, + build: (zip: ZipFile) => void | Promise +): Promise => { + const zip = new ZipFile(); + await build(zip); + + ctx.set("Content-Type", "application/zip"); + ctx.set( + "Content-Disposition", + contentDisposition(fileName, { type: "attachment" }) + ); + + zip.outputStream.on("error", (err) => { + ctx.app.emit("error", err, ctx); + ctx.res.destroy(err); + }); + ctx.body = zip.outputStream; + zip.end(); +}; diff --git a/yarn.lock b/yarn.lock index 4ba507079f..33089381cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8252,6 +8252,15 @@ __metadata: languageName: node linkType: hard +"@types/yazl@npm:^2.4.6": + version: 2.4.6 + resolution: "@types/yazl@npm:2.4.6" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/6145fd1025e592747ed1331d88edb6d57dfd0f514812420889ecc880e0728b22c4d4214d5062b36164c4c40ca271266968a1571118ba6521796c76ac0b18c99c + languageName: node + linkType: hard + "@upsetjs/venn.js@npm:^2.0.0": version: 2.0.0 resolution: "@upsetjs/venn.js@npm:2.0.0" @@ -9157,6 +9166,13 @@ __metadata: languageName: node linkType: hard +"buffer-crc32@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-crc32@npm:1.0.0" + checksum: 10c0/8b86e161cee4bb48d5fa622cbae4c18f25e4857e5203b89e23de59e627ab26beb82d9d7999f2b8de02580165f61f83f997beaf02980cdf06affd175b651921ab + languageName: node + linkType: hard + "buffer-crc32@npm:~0.2.3": version: 0.2.13 resolution: "buffer-crc32@npm:0.2.13" @@ -13846,7 +13862,7 @@ __metadata: languageName: node linkType: hard -"jszip@npm:^3.10.1, jszip@npm:^3.7.1": +"jszip@npm:^3.7.1": version: 3.10.1 resolution: "jszip@npm:3.10.1" dependencies: @@ -15858,6 +15874,7 @@ __metadata: "@types/utf8": "npm:^3.0.3" "@types/validator": "npm:^13.15.10" "@types/yauzl": "npm:^2.10.3" + "@types/yazl": "npm:^2.4.6" "@vitejs/plugin-react-oxc": "npm:^0.2.3" "@vitest/ui": "npm:^4.1.6" addressparser: "npm:^1.0.1" @@ -15912,7 +15929,6 @@ __metadata: js-yaml: "npm:^4.1.1" jsdom: "npm:^22.1.0" jsonwebtoken: "npm:^9.0.3" - jszip: "npm:^3.10.1" katex: "npm:^0.16.45" kbar: "npm:0.1.0-beta.48" koa: "npm:^3.0.3" @@ -16047,6 +16063,7 @@ __metadata: y-prosemirror: "npm:^1.3.7" y-protocols: "npm:^1.0.7" yauzl: "npm:^3.2.1" + yazl: "npm:^3.3.1" yjs: "npm:^13.6.30" zod: "npm:^4.3.6" languageName: unknown @@ -21547,6 +21564,15 @@ __metadata: languageName: node linkType: hard +"yazl@npm:^3.3.1": + version: 3.3.1 + resolution: "yazl@npm:3.3.1" + dependencies: + buffer-crc32: "npm:^1.0.0" + checksum: 10c0/8290dffd8afc4ea32459c0ce5d7e22af250515aa0170f289cbc896530d3cd40c5be4788b5e3e7aa5cd21889778071f424e1824faadbd257dbd5ff80fb60d1fa6 + languageName: node + linkType: hard + "yjs@npm:^13.6.30": version: 13.6.30 resolution: "yjs@npm:13.6.30"