From 285d812f8e9cf923407fd11bd33db91f7cd9bfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 13:49:32 +0200 Subject: [PATCH 01/14] fix(converters): resolve reference mutation and discovery priority in main.ts --- src/converters/main.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/converters/main.ts b/src/converters/main.ts index 8377011b..9c063e18 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -246,8 +246,8 @@ async function mainConverter( converterFunc = properties[converterName]?.converter; } else { // Iterate over each converter in properties - for (converterName in properties) { - const converterObj = properties[converterName]; + for (const name in properties) { + const converterObj = properties[name]; if (!converterObj) { break; @@ -262,6 +262,9 @@ async function mainConverter( break; } } + if (converterFunc) { + break; + } } } @@ -349,7 +352,7 @@ for (const converterName in properties) { if (allTargets[converterName]) { allTargets[converterName].push(...(converterProperties.to[key] || [])); } else { - allTargets[converterName] = converterProperties.to[key] || []; + allTargets[converterName] = [...(converterProperties.to[key] || [])]; } } } From a95effbc835e3e1807d73db1d470ccff60afdb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 13:51:27 +0200 Subject: [PATCH 02/14] fix(converters): improve CSV header generation and FFMPEG argument parsing --- src/converters/ffmpeg.ts | 4 ++-- src/converters/vcf.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/converters/ffmpeg.ts b/src/converters/ffmpeg.ts index 8d5709a6..926b62d3 100644 --- a/src/converters/ffmpeg.ts +++ b/src/converters/ffmpeg.ts @@ -770,9 +770,9 @@ export async function convert( } // Parse FFMPEG_ARGS environment variable into array - const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/) : []; + const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/).filter(Boolean) : []; const ffmpegOutputArgs = process.env.FFMPEG_OUTPUT_ARGS - ? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/) + ? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/).filter(Boolean) : []; return new Promise((resolve, reject) => { diff --git a/src/converters/vcf.ts b/src/converters/vcf.ts index 2fff8ed8..d84094c0 100644 --- a/src/converters/vcf.ts +++ b/src/converters/vcf.ts @@ -46,12 +46,14 @@ export function parseVCF(data: string): Record[] { export function toCSV(data: Record[]): string { if (!data.length) return ""; - const first = data[0]; - if (!first) return ""; - const headers = Object.keys(first); + const headers = new Set(); + for (const row of data) { + Object.keys(row).forEach((key) => headers.add(key)); + } + const headerArray = Array.from(headers); const escape = (str: string) => `"${str.replace(/"/g, '""')}"`; - const rows = data.map((row) => headers.map((h) => escape(row[h] || "")).join(",")); - return [headers.join(","), ...rows].join("\n"); + const rows = data.map((row) => headerArray.map((h) => escape(row[h] || "")).join(",")); + return [headerArray.join(","), ...rows].join("\n"); } export async function convert( From cc1689a2efeed37d62756f9900adcf74ac7e6397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 13:51:53 +0200 Subject: [PATCH 03/14] fix(pages): improve request handling, session persistence, and file operations --- src/pages/convert.tsx | 13 ++++++--- src/pages/deleteFile.tsx | 10 ++++++- src/pages/download.tsx | 4 +-- src/pages/root.tsx | 58 +++++++++++++++++++++++++--------------- 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/pages/convert.tsx b/src/pages/convert.tsx index ed367b23..c181e516 100644 --- a/src/pages/convert.tsx +++ b/src/pages/convert.tsx @@ -56,16 +56,21 @@ export const convert = new Elysia().use(userService).post( return redirect(`${WEBROOT}/`, 302); } - const fileNames = JSON.parse(body.file_names) as string[]; - - for (let i = 0; i < fileNames.length; i++) { - fileNames[i] = sanitize(fileNames[i] || ""); + let fileNames: string[]; + try { + fileNames = JSON.parse(body.file_names) as string[]; + } catch (e) { + return redirect(`${WEBROOT}/`, 302); } if (!Array.isArray(fileNames) || fileNames.length === 0) { return redirect(`${WEBROOT}/`, 302); } + for (let i = 0; i < fileNames.length; i++) { + fileNames[i] = sanitize(fileNames[i] || ""); + } + db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run( fileNames.length, jobId.value, diff --git a/src/pages/deleteFile.tsx b/src/pages/deleteFile.tsx index e18caef3..a43fd596 100644 --- a/src/pages/deleteFile.tsx +++ b/src/pages/deleteFile.tsx @@ -27,7 +27,15 @@ export const deleteFile = new Elysia().use(userService).post( const sanitized = sanitize(body.filename); const targetPath = path.join(userUploadsDir, sanitized); - await unlink(targetPath); + try { + await unlink(targetPath); + } catch (error) { + if (error.code === "ENOENT") { + return { message: "File not found." }; + } + console.error(`Failed to unlink ${targetPath}:`, error); + throw error; + } return { message: "File deleted successfully.", diff --git a/src/pages/download.tsx b/src/pages/download.tsx index ffc64e9b..f4dc7cf4 100644 --- a/src/pages/download.tsx +++ b/src/pages/download.tsx @@ -21,8 +21,8 @@ export const download = new Elysia() return redirect(`${WEBROOT}/results`, 302); } // parse from URL encoded string - const jobId = decodeURIComponent(params.jobId); - const fileName = sanitize(decodeURIComponent(params.fileName)); + const jobId = params.jobId; + const fileName = sanitize(params.fileName); const filePath = `${outputDir}${userId}/${jobId}/${fileName}`; const file = Bun.file(filePath); diff --git a/src/pages/root.tsx b/src/pages/root.tsx index eba32384..b0ea02f0 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -33,30 +33,44 @@ export const root = new Elysia().use(userService).get( // validate jwt let user: ({ id: string } & JWTPayloadSpec) | false = false; if (ALLOW_UNAUTHENTICATED) { - const newUserId = String( - UNAUTHENTICATED_USER_SHARING - ? 0 - : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), - ); - const accessToken = await jwt.sign({ - id: newUserId, - }); - - user = { id: newUserId }; - if (!auth) { - return { - message: "No auth cookie, perhaps your browser is blocking cookies.", - }; + // Check if the current cookie is already a valid session + if (auth?.value) { + try { + const verifiedUser = await jwt.verify(auth.value); + if (verifiedUser && verifiedUser.id) { + user = verifiedUser; + } + } catch (e) { + // Invalid cookie, fall through to anonymous creation + } } - // set cookie - auth.set({ - value: accessToken, - httpOnly: true, - secure: !HTTP_ALLOWED, - maxAge: 24 * 60 * 60, - sameSite: "strict", - }); + // If no valid session found, create an anonymous user + if (!user) { + const newUserId = String( + UNAUTHENTICATED_USER_SHARING + ? 0 + : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), + ); + const accessToken = await jwt.sign({ + id: newUserId, + }); + + user = { id: newUserId }; + if (!auth) { + return { + message: "No auth cookie, perhaps your browser is blocking cookies.", + }; + } + + auth.set({ + value: accessToken, + httpOnly: true, + secure: !HTTP_ALLOWED, + maxAge: 24 * 60 * 60, + sameSite: "strict", + }); + } } else if (auth?.value) { user = await jwt.verify(auth.value); From 5cdce6b4b4ed3772650d5b754edcd144a3f57015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 16:55:41 +0200 Subject: [PATCH 04/14] fix(converters): fix converter discovery and array sharing bugs --- src/converters/main.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/converters/main.ts b/src/converters/main.ts index 9c063e18..6abedfdc 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -259,6 +259,7 @@ async function mainConverter( converterObj?.properties?.to[key]?.includes(convertTo) ) { converterFunc = converterObj.converter; + converterName = name; break; } } @@ -299,6 +300,7 @@ const possibleTargets: Record> = {}; for (const converterName in properties) { const converterProperties = properties[converterName]?.properties; + if (!converterProperties) continue; for (const key in converterProperties.from) { @@ -310,7 +312,7 @@ for (const converterName in properties) { for (const ext of fromList) { if (!possibleTargets[ext]) possibleTargets[ext] = {}; - possibleTargets[ext][converterName] = toList; + possibleTargets[ext][converterName] = [...toList]; } } } @@ -319,15 +321,13 @@ export const getPossibleTargets = (from: string): Record => { const fromClean = normalizeFiletype(from); return possibleTargets[fromClean] || {}; -}; +} const possibleInputs: string[] = []; for (const converterName in properties) { const converterProperties = properties[converterName]?.properties; - if (!converterProperties) { - continue; - } + if (!converterProperties) continue; for (const key in converterProperties.from) { for (const extension of converterProperties.from[key] ?? []) { @@ -344,13 +344,11 @@ const allTargets: Record = {}; for (const converterName in properties) { const converterProperties = properties[converterName]?.properties; - if (!converterProperties) { - continue; - } + if (!converterProperties) continue; for (const key in converterProperties.to) { if (allTargets[converterName]) { - allTargets[converterName].push(...(converterProperties.to[key] || [])); + allTargets[converterName] = [...allTargets[converterName], ...(converterProperties.to[key] || [])]; } else { allTargets[converterName] = [...(converterProperties.to[key] || [])]; } @@ -365,13 +363,11 @@ const allInputs: Record = {}; for (const converterName in properties) { const converterProperties = properties[converterName]?.properties; - if (!converterProperties) { - continue; - } + if (!converterProperties) continue; for (const key in converterProperties.from) { if (allInputs[converterName]) { - allInputs[converterName].push(...(converterProperties.from[key] || [])); + allInputs[converterName] = [...allInputs[converterName], ...(converterProperties.from[key] || [])]; } else { allInputs[converterName] = converterProperties.from[key] || []; } @@ -386,4 +382,4 @@ export const getAllInputs = (converter: string) => { * @internal For testing only. Do not use in production. * Tests need direct access to cover all branches of converter discovery and chunking logic. */ -export { chunks, mainConverter }; +export { chunks, mainConverter }; \ No newline at end of file From e17f29552c0dfbe09a9bbfcc9c914535ad923be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 16:56:13 +0200 Subject: [PATCH 05/14] fix(pages): fix session preservation on unauthenticated route --- src/pages/root.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pages/root.tsx b/src/pages/root.tsx index b0ea02f0..4aacd258 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -57,19 +57,21 @@ export const root = new Elysia().use(userService).get( }); user = { id: newUserId }; - if (!auth) { + + // Use existing cookie if it exists, otherwise set a new one + if (auth) { + auth.set({ + value: accessToken, + httpOnly: true, + secure: !HTTP_ALLOWED, + maxAge: 24 * 60 * 60, + sameSite: "strict", + }); + } else { return { message: "No auth cookie, perhaps your browser is blocking cookies.", }; } - - auth.set({ - value: accessToken, - httpOnly: true, - secure: !HTTP_ALLOWED, - maxAge: 24 * 60 * 60, - sameSite: "strict", - }); } } else if (auth?.value) { user = await jwt.verify(auth.value); From f570d53ad48cd77afee206e0b41cc903c8481420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 16:56:38 +0200 Subject: [PATCH 06/14] fix(tests): remove failing annotations after bugfixes --- tests/converters/ffmpeg.test.ts | 5 +---- tests/converters/main.test.ts | 17 +++-------------- tests/converters/vcf.test.ts | 5 +---- tests/pages/convert.test.ts | 5 +---- tests/pages/deleteFile.test.ts | 6 +----- tests/pages/download.test.ts | 6 +----- tests/pages/root.test.ts | 7 +------ 7 files changed, 9 insertions(+), 42 deletions(-) diff --git a/tests/converters/ffmpeg.test.ts b/tests/converters/ffmpeg.test.ts index 955e2a0a..54c1b210 100644 --- a/tests/converters/ffmpeg.test.ts +++ b/tests/converters/ffmpeg.test.ts @@ -181,10 +181,7 @@ test("splits FFMPEG_ARGS on any whitespace", async () => { expect(calls[0]?.slice(0, 3)).toEqual(["-hide_banner", "-y", "-nostdin"]); }); -// BUG: split(/\s+/) keeps empty strings for leading/trailing whitespace -// (" -y ".split(/\s+/) is ["", "-y", ""]), so ffmpeg receives "" as an argument -// and fails. Remove `.failing` once src/converters/ffmpeg.ts drops empty entries. -test.failing("ignores leading and trailing whitespace in FFMPEG_ARGS", async () => { +test("ignores leading and trailing whitespace in FFMPEG_ARGS", async () => { process.env.FFMPEG_ARGS = " -y "; process.env.FFMPEG_OUTPUT_ARGS = " -preset veryfast "; diff --git a/tests/converters/main.test.ts b/tests/converters/main.test.ts index e8d7f936..96e05e50 100644 --- a/tests/converters/main.test.ts +++ b/tests/converters/main.test.ts @@ -148,11 +148,7 @@ describe("converter registry", () => { expect(getAllInputs("doesnotexist")).toEqual([]); }); - // BUG: building allTargets/allInputs stores the first category's array by - // reference and then pushes the other categories into it. That mutates the - // converters' own properties, so e.g. LibreOffice offers docx -> xlsx and - // libjxl offers jxl -> jxl. Remove `.failing` once main.ts copies the arrays. - test.failing("listing all targets and inputs does not leak formats across categories", () => { + test("listing all targets and inputs does not leak formats across categories", () => { expect(getPossibleTargets("docx").libreoffice).not.toContain("xlsx"); expect(getPossibleTargets("jxl").libjxl).not.toContain("jxl"); expect(libreofficeProperties.from.text).not.toContain("xlsx"); @@ -251,10 +247,7 @@ describe("mainConverter", () => { }, ); - // BUG: discovery loops with `for (converterName in properties)` and only breaks - // the inner loop, so the variable always ends on the last registered converter. - // Remove `.failing` once mainConverter stops at the first match. - test.failing("logs the name of the auto-discovered converter", async () => { + test("logs the name of the auto-discovered converter", async () => { const inputPath = `${uploadsDir}named.vcf`; await writeFile(inputPath, vcard("Named")); @@ -263,11 +256,7 @@ describe("mainConverter", () => { expect(output.logs.join("\n")).toContain("successfully using vcf"); }); - // BUG: same root cause - later matches overwrite earlier ones, so emf -> png - // uses ImageMagick even though Inkscape is registered first on purpose - // ("Prioritize Inkscape for EMF files"). The input does not exist, so whichever - // converter is picked fails fast; only the logged converter name matters. - test.failing("auto-discovery prefers the first registered converter", async () => { + test("auto-discovery prefers the first registered converter", async () => { await mainConverter(`${uploadsDir}missing.emf`, "emf", "png", `${outputDir}missing.png`); expect(output.errors.join("\n")).toContain("using inkscape"); diff --git a/tests/converters/vcf.test.ts b/tests/converters/vcf.test.ts index 06f89535..e4d7f571 100644 --- a/tests/converters/vcf.test.ts +++ b/tests/converters/vcf.test.ts @@ -181,10 +181,7 @@ describe("toCSV", () => { expect(toCSV(contacts)).toBe('Full Name,Email\n"John Doe","john@example.com"\n"Jane Smith",""'); }); - // BUG: the header is built from the first contact only, so fields that only - // later contacts have are silently dropped from the export. Remove `.failing` - // once toCSV builds the header from all contacts. - test.failing("should not drop fields that only later contacts have", () => { + test("should not drop fields that only later contacts have", () => { const contacts = [{ "Full Name": "John Doe" }, { "Full Name": "Jane Smith", Email: "j@x.com" }]; expect(toCSV(contacts)).toContain("j@x.com"); diff --git a/tests/pages/convert.test.ts b/tests/pages/convert.test.ts index 44756a6f..ddcb4001 100644 --- a/tests/pages/convert.test.ts +++ b/tests/pages/convert.test.ts @@ -154,10 +154,7 @@ describe("POST /convert", () => { ]); }); - // BUG: file_names is parsed and indexed before it is validated, so a value that is not a - // JSON array ends in a 500 instead of the redirect used for every other invalid input. - // Remove `.failing` once convert.tsx validates the parsed value first. - test.failing.each([ + test.each([ ["is not JSON", "not json"], ["is a JSON string", '"a.vcf"'], ])("redirects home when file_names %s", async (_, fileNames) => { diff --git a/tests/pages/deleteFile.test.ts b/tests/pages/deleteFile.test.ts index 1da8f61f..5fe8c542 100644 --- a/tests/pages/deleteFile.test.ts +++ b/tests/pages/deleteFile.test.ts @@ -79,11 +79,7 @@ describe("POST /delete", () => { expect(existsSync(`${other.dirs.uploads}keep.txt`)).toBe(true); }); - // BUG: unlink errors are not handled, so deleting a file that does not exist answers - // with a 500 whose body contains the absolute upload path on the server. This also - // happens for every sanitized traversal attempt. Remove `.failing` once deleteFile.tsx - // handles the missing file. - test.failing.each(["missing.txt", "../../1/1/keep.txt"])( + test.each(["missing.txt", "../../1/1/keep.txt"])( "answers a missing file (%s) without a server error or server paths", async (filename) => { const { user, job } = await userWithUploads(); diff --git a/tests/pages/download.test.ts b/tests/pages/download.test.ts index 9827b8ec..f6a2e75e 100644 --- a/tests/pages/download.test.ts +++ b/tests/pages/download.test.ts @@ -111,11 +111,7 @@ describe("GET /download/:userId/:jobId/:fileName", () => { expect(response.status).toBe(404); }); - // BUG: Elysia already decodes path parameters and download.tsx decodes them a second - // time, so file names containing "%" fail with a 500 "URI error" (and a literal "%20" - // would be turned into a space). The results page links to such files. Remove - // `.failing` once the second decodeURIComponent is dropped. - test.failing("serves file names containing a percent sign", async () => { + test("serves file names containing a percent sign", async () => { const { user, job } = await userWithOutputs({ "100%.csv": "percent" }); const response = await request(`/download/${user.id}/${job}/100%25.csv`, { diff --git a/tests/pages/root.test.ts b/tests/pages/root.test.ts index 9fcc11a8..b4f39b3d 100644 --- a/tests/pages/root.test.ts +++ b/tests/pages/root.test.ts @@ -142,12 +142,7 @@ describe("GET / without accounts (ALLOW_UNAUTHENTICATED)", () => { expect(body).not.toContain('href="/logoff"'); }); - // BUG: the ALLOW_UNAUTHENTICATED branch runs before the session check, so every visit - // replaces the existing session (even a registered account's) with a new anonymous id - // and the user loses access to their previous jobs. This regressed the fix for #114 - // ("keep unauthenticated user logged in if allowed", bc4ad49) in 538c5b6. Remove - // `.failing` once root.tsx verifies an existing session first. - test.failing("keeps an existing session", async () => { + test("keeps an existing session", async () => { setEnv({ ALLOW_UNAUTHENTICATED: true }); const user = await createUser(); From f7976fa55da164447f81abdc6a231bc20eb5cc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 18:10:23 +0200 Subject: [PATCH 07/14] fix(linting): fixed some linting and prettier issues --- src/converters/ffmpeg.ts | 10 ++++++++-- src/converters/main.ts | 14 ++++++++++---- src/pages/convert.tsx | 2 +- src/pages/deleteFile.tsx | 7 +++---- src/pages/root.tsx | 2 +- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/converters/ffmpeg.ts b/src/converters/ffmpeg.ts index 926b62d3..adf48b45 100644 --- a/src/converters/ffmpeg.ts +++ b/src/converters/ffmpeg.ts @@ -770,9 +770,15 @@ export async function convert( } // Parse FFMPEG_ARGS environment variable into array - const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/).filter(Boolean) : []; + const ffmpegArgs = process.env.FFMPEG_ARGS + ? process.env.FFMPEG_ARGS.split(/\s+/) + .filter(Boolean) + .map((arg) => arg.trim()) + : []; const ffmpegOutputArgs = process.env.FFMPEG_OUTPUT_ARGS - ? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/).filter(Boolean) + ? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/) + .filter(Boolean) + .map((arg) => arg.trim()) : []; return new Promise((resolve, reject) => { diff --git a/src/converters/main.ts b/src/converters/main.ts index 6abedfdc..2c8e6b8c 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -321,7 +321,7 @@ export const getPossibleTargets = (from: string): Record => { const fromClean = normalizeFiletype(from); return possibleTargets[fromClean] || {}; -} +}; const possibleInputs: string[] = []; for (const converterName in properties) { @@ -348,7 +348,10 @@ for (const converterName in properties) { for (const key in converterProperties.to) { if (allTargets[converterName]) { - allTargets[converterName] = [...allTargets[converterName], ...(converterProperties.to[key] || [])]; + allTargets[converterName] = [ + ...allTargets[converterName], + ...(converterProperties.to[key] || []), + ]; } else { allTargets[converterName] = [...(converterProperties.to[key] || [])]; } @@ -367,7 +370,10 @@ for (const converterName in properties) { for (const key in converterProperties.from) { if (allInputs[converterName]) { - allInputs[converterName] = [...allInputs[converterName], ...(converterProperties.from[key] || [])]; + allInputs[converterName] = [ + ...allInputs[converterName], + ...(converterProperties.from[key] || []), + ]; } else { allInputs[converterName] = converterProperties.from[key] || []; } @@ -382,4 +388,4 @@ export const getAllInputs = (converter: string) => { * @internal For testing only. Do not use in production. * Tests need direct access to cover all branches of converter discovery and chunking logic. */ -export { chunks, mainConverter }; \ No newline at end of file +export { chunks, mainConverter }; diff --git a/src/pages/convert.tsx b/src/pages/convert.tsx index c181e516..e28890cf 100644 --- a/src/pages/convert.tsx +++ b/src/pages/convert.tsx @@ -59,7 +59,7 @@ export const convert = new Elysia().use(userService).post( let fileNames: string[]; try { fileNames = JSON.parse(body.file_names) as string[]; - } catch (e) { + } catch { return redirect(`${WEBROOT}/`, 302); } diff --git a/src/pages/deleteFile.tsx b/src/pages/deleteFile.tsx index a43fd596..ed35550b 100644 --- a/src/pages/deleteFile.tsx +++ b/src/pages/deleteFile.tsx @@ -29,12 +29,11 @@ export const deleteFile = new Elysia().use(userService).post( try { await unlink(targetPath); - } catch (error) { - if (error.code === "ENOENT") { + } catch (e) { + if ((e as NodeJS.ErrnoException).code === "ENOENT") { return { message: "File not found." }; } - console.error(`Failed to unlink ${targetPath}:`, error); - throw error; + throw e; } return { diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 4aacd258..99b1f5ae 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -40,7 +40,7 @@ export const root = new Elysia().use(userService).get( if (verifiedUser && verifiedUser.id) { user = verifiedUser; } - } catch (e) { + } catch { // Invalid cookie, fall through to anonymous creation } } From eb5e484042823dfcd44ac2cc410b19bb380a07db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 19:08:32 +0200 Subject: [PATCH 08/14] fix(review): fix issues based on review comments --- src/converters/main.ts | 9 +++++---- src/pages/convert.tsx | 4 ++++ src/pages/deleteFile.tsx | 3 ++- src/pages/root.tsx | 10 +++++++++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/converters/main.ts b/src/converters/main.ts index 2c8e6b8c..9a74a12a 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -85,6 +85,11 @@ const properties: Record< properties: propertiesxelatex, converter: convertxelatex, }, + // djvu before calibre so the dedicated ddjvu converter wins for DjVu files + djvu: { + properties: propertiesDjvu, + converter: convertDjvu, + }, calibre: { properties: propertiesCalibre, converter: convertCalibre, @@ -93,10 +98,6 @@ const properties: Record< properties: propertiesDasel, converter: convertDasel, }, - djvu: { - properties: propertiesDjvu, - converter: convertDjvu, - }, libreoffice: { properties: propertiesLibreOffice, converter: convertLibreOffice, diff --git a/src/pages/convert.tsx b/src/pages/convert.tsx index e28890cf..27d7c0e8 100644 --- a/src/pages/convert.tsx +++ b/src/pages/convert.tsx @@ -67,6 +67,10 @@ export const convert = new Elysia().use(userService).post( return redirect(`${WEBROOT}/`, 302); } + if (!fileNames.every((name) => typeof name === "string")) { + return redirect(`${WEBROOT}/`, 302); + } + for (let i = 0; i < fileNames.length; i++) { fileNames[i] = sanitize(fileNames[i] || ""); } diff --git a/src/pages/deleteFile.tsx b/src/pages/deleteFile.tsx index ed35550b..6302db9b 100644 --- a/src/pages/deleteFile.tsx +++ b/src/pages/deleteFile.tsx @@ -9,7 +9,7 @@ import path from "node:path"; export const deleteFile = new Elysia().use(userService).post( "/delete", - async ({ body, redirect, cookie: { jobId }, user }) => { + async ({ body, redirect, set, cookie: { jobId }, user }) => { if (!jobId?.value) { return redirect(`${WEBROOT}/`, 302); } @@ -31,6 +31,7 @@ export const deleteFile = new Elysia().use(userService).post( await unlink(targetPath); } catch (e) { if ((e as NodeJS.ErrnoException).code === "ENOENT") { + set.status = 404; return { message: "File not found." }; } throw e; diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 99b1f5ae..0f85f5cf 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -38,7 +38,15 @@ export const root = new Elysia().use(userService).get( try { const verifiedUser = await jwt.verify(auth.value); if (verifiedUser && verifiedUser.id) { - user = verifiedUser; + const numericId = Number.parseInt(verifiedUser.id); + // IDs below the anonymous range (excluding shared 0) are registered users + const isRegistered = numericId > 0 && numericId < 2 ** 24; + if ( + !isRegistered || + db.query("SELECT * FROM users WHERE id = ?").as(User).get(verifiedUser.id) + ) { + user = verifiedUser; + } } } catch { // Invalid cookie, fall through to anonymous creation From e607bb5f1cc02e0fa883022af496b70265fb268b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:14:36 +0200 Subject: [PATCH 09/14] chore(deps): update dependency @types/bun to v1.4.4 (#686) --- bun.lock | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 0f5967b0..746d677d 100644 --- a/bun.lock +++ b/bun.lock @@ -18,7 +18,7 @@ "@kitajs/ts-html-plugin": "^4.1.4", "@tailwindcss/cli": "^4.3.3", "@tailwindcss/postcss": "^4.3.3", - "@types/bun": "1.4.3", + "@types/bun": "1.4.4", "@types/node": "^24.19.0", "eslint": "^10.11.0", "eslint-plugin-better-tailwindcss": "^4.7.0", @@ -260,7 +260,7 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], - "@types/bun": ["@types/bun@1.4.3", "", { "dependencies": { "bun-types": "1.4.3" } }, "sha512-N67Pqy8JXbAY/j0tNraS3V5mubGvc6rrrNDmDCtZLlFzccbUJPDb0tvgUBC5q4m8ybkP93Jcn1QB+RYQ6PvBhw=="], + "@types/bun": ["@types/bun@1.4.4", "", { "dependencies": { "bun-types": "1.4.4" } }, "sha512-9wkRDXUXK124pEnAutE6n/l18j+i9GC5IAHK8TmX/2nlySETmky0OU6yL5U8pW1AoKh++AN9auDCr647BfurjQ=="], "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], @@ -310,7 +310,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bun-types": ["bun-types@1.4.3", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="], + "bun-types": ["bun-types@1.4.4", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-yjrJh5oGX7mMPKGd19YXyIjmEuj4G4zNC30bs7PM9Az7NQ3ndF3j6hbeRdvJTRQEhJ/FYQwW0a2moweEi2hMxQ=="], "cacheable": ["cacheable@2.5.0", "", { "dependencies": { "@cacheable/memory": "^2.2.0", "@cacheable/utils": "^2.5.0", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.10.1" } }, "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g=="], diff --git a/package.json b/package.json index 0ca95a75..c18cd013 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@kitajs/ts-html-plugin": "^4.1.4", "@tailwindcss/cli": "^4.3.3", "@tailwindcss/postcss": "^4.3.3", - "@types/bun": "1.4.3", + "@types/bun": "1.4.4", "@types/node": "^24.19.0", "eslint": "^10.11.0", "eslint-plugin-better-tailwindcss": "^4.7.0", From 2135c6b9f1bed5c4fdf9d2b1cfb7cdf54e9e2bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emrik=20=C3=96stling?= Date: Fri, 23 Oct 2026 18:23:49 +0200 Subject: [PATCH 10/14] chore: use bun fmt and bun check (#685) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .vscode/settings.json | 4 ++-- bun.lock | 3 --- bunfig.toml | 15 ++++++++++++--- knip.json | 3 ++- mise.toml | 12 ++++++------ package.json | 17 ++++++++--------- prettier.config.js | 4 +--- src/converters/assimp.ts | 1 + src/converters/calibre.ts | 1 + src/converters/dasel.ts | 1 + src/converters/djvu.ts | 1 + src/converters/dvisvgm.ts | 1 + src/converters/ffmpeg.ts | 2 +- src/converters/graphicsmagick.ts | 1 + src/converters/imagemagick.ts | 1 + src/converters/inkscape.ts | 1 + src/converters/libheif.ts | 1 + src/converters/libjxl.ts | 1 + src/converters/libreoffice.ts | 1 + src/converters/main.ts | 10 ++++++---- src/converters/markitdown.ts | 1 + src/converters/msgconvert.ts | 1 + src/converters/pandoc.ts | 1 + src/converters/pdftops.ts | 1 + src/converters/potrace.ts | 1 + src/converters/resvg.ts | 1 + src/converters/vips.ts | 1 + src/converters/vtracer.ts | 1 + src/converters/xelatex.ts | 1 + src/helpers/printVersions.ts | 1 + src/index.tsx | 4 +++- src/main.css | 6 +++--- src/pages/chooseConverter.tsx | 1 + src/pages/convert.tsx | 2 ++ src/pages/deleteFile.tsx | 6 ++++-- src/pages/deleteJob.tsx | 4 +++- src/pages/download.tsx | 2 ++ src/pages/healthcheck.tsx | 1 + src/pages/history.tsx | 7 ++++--- src/pages/listConverters.tsx | 3 ++- src/pages/results.tsx | 5 +++-- src/pages/root.tsx | 4 +++- src/pages/upload.tsx | 3 ++- src/pages/user.tsx | 1 + src/services/user.ts | 2 ++ tests/converters/assimp.test.ts | 1 + tests/converters/calibre.test.ts | 1 + tests/converters/dasel.test.ts | 3 ++- tests/converters/djvu.test.ts | 3 ++- tests/converters/dvisvgm.test.ts | 1 + tests/converters/ffmpeg.test.ts | 3 ++- tests/converters/graphicsmagick.test.ts | 1 + tests/converters/helpers/commonTests.ts | 1 + tests/converters/helpers/converters.ts | 9 +++++---- tests/converters/imagemagick.test.ts | 1 + tests/converters/inkscape.test.ts | 1 + tests/converters/libheif.test.ts | 1 + tests/converters/libjxl.test.ts | 1 + tests/converters/libreoffice.test.ts | 4 ++-- tests/converters/main.test.ts | 2 ++ tests/converters/markitdown.test.ts | 1 + tests/converters/msgconvert.test.ts | 1 + tests/converters/pandoc.test.ts | 3 ++- tests/converters/pdftops.test.ts | 1 + tests/converters/potrace.test.ts | 1 + tests/converters/resvg.test.ts | 1 + tests/converters/vcf.test.ts | 3 ++- tests/converters/vips.test.ts | 1 + tests/converters/vtracer.test.ts | 3 ++- tests/converters/xelatex.test.ts | 1 + tests/db/db.test.ts | 4 ++-- tests/helpers/normalizeFiletype.test.ts | 3 ++- tests/pages/auth.test.ts | 2 ++ tests/pages/chooseConverter.test.ts | 1 + tests/pages/convert.test.ts | 1 + tests/pages/deleteFile.test.ts | 1 + tests/pages/deleteJob.test.ts | 1 + tests/pages/download.test.ts | 2 ++ tests/pages/firstRun.test.ts | 1 + tests/pages/healthcheck.test.ts | 1 + tests/pages/helpers/app.ts | 2 ++ tests/pages/history.test.ts | 1 + tests/pages/listConverters.test.ts | 1 + tests/pages/results.test.ts | 1 + tests/pages/root.test.ts | 1 + tests/pages/upload.test.ts | 1 + tests/pages/user.test.ts | 1 + tests/services/user.test.ts | 2 ++ tsconfig.json | 1 - 89 files changed, 156 insertions(+), 63 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fae8e3d8..9519b6f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true } diff --git a/bun.lock b/bun.lock index 746d677d..5dcaf6e1 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,6 @@ "knip": "^6.38.0", "npm-run-all2": "^9.0.3", "postcss": "^8.5.28", - "prettier": "^3.9.9", "tailwind-scrollbar": "^4.0.2", "tailwindcss": "^4.3.3", "typescript": "^5.9.3", @@ -530,8 +529,6 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.9.10", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA=="], - "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], diff --git a/bunfig.toml b/bunfig.toml index d073e330..deb6d9b4 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,6 +1,15 @@ [test] preload = ["./tests/preload.ts"] coverage = true -coveragePathIgnorePatterns = [ - "tests/converters/helpers/commonTests.ts" -] +coveragePathIgnorePatterns = ["tests/converters/helpers/commonTests.ts"] + +[fmt] +arrowParens = "always" +printWidth = 100 +singleQuote = false +semi = true +tabWidth = 2 +sortImports = true +sortTailwindcss = false +sortPackageJson = true +jsdoc = true diff --git a/knip.json b/knip.json index c9e38040..472bad85 100644 --- a/knip.json +++ b/knip.json @@ -19,7 +19,8 @@ "potrace", "soffice", "perl", - "markitdown" + "markitdown", + "fmt" ], "tailwind": { "entry": ["src/main.css"] diff --git a/mise.toml b/mise.toml index 40a490fb..b29723e2 100644 --- a/mise.toml +++ b/mise.toml @@ -1,6 +1,6 @@ -[tools] -bun = "1.4.4" - -[env] -JWT_SECRET = "aLongAndSecretStringUsedToSignTheJSONWebToken1234" -FFMPEG_OUTPUT_ARGS = "-preset veryfast -threads 2" +[tools] +bun = "1.4.4" + +[env] +JWT_SECRET = "aLongAndSecretStringUsedToSignTheJSONWebToken1234" +FFMPEG_OUTPUT_ARGS = "-preset veryfast -threads 2" diff --git a/package.json b/package.json index c18cd013..7c6f6aaf 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,24 @@ { "name": "convertx-frontend", "version": "0.20.0", + "type": "module", + "module": "src/index.tsx", "scripts": { "dev": "bun run --watch src/index.tsx", "hot": "bun run --hot src/index.tsx", "format": "npm-run-all 'format:*'", "format:eslint": "eslint --fix .", - "format:prettier": "prettier --write .", + "format:prettier": "bun fmt", "build:css": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css", "build:js": "tsc", "build:bin": "bun run build:css && bun build --compile --minify --sourcemap --bytecode --format=esm --define 'process.env.NODE_ENV=\"production\"' src/index.tsx --outfile dist/convertx", "build:docker": "docker compose build", "build": "bun run build:css && bun run build:js", "lint": "npm-run-all 'lint:*'", - "lint:tsc": "tsc --noEmit", + "lint:tsc": "bun check", "lint:knip": "knip", "lint:eslint": "eslint .", - "lint:prettier": "prettier --check .", + "lint:prettier": "bun fmt --check", "lint:xss": "xss-scan", "test": "bun test --path-ignore-patterns=tests/integration", "test:integration": "docker build --target test -t convertx:test . && docker run --rm -t -v ./tests:/app/tests -v ./src:/app/src -v ./package.json:/app/package.json convertx:test bun test tests/integration" @@ -30,11 +32,6 @@ "sanitize-filename": "^1.6.4", "tar": "^7.5.22" }, - "module": "src/index.tsx", - "type": "module", - "bun-create": { - "start": "bun run src/index.tsx" - }, "devDependencies": { "@eslint/js": "^10.0.1", "@kitajs/ts-html-plugin": "^4.1.4", @@ -48,12 +45,14 @@ "knip": "^6.38.0", "npm-run-all2": "^9.0.3", "postcss": "^8.5.28", - "prettier": "^3.9.9", "tailwind-scrollbar": "^4.0.2", "tailwindcss": "^4.3.3", "typescript": "^5.9.3", "typescript-eslint": "^8.70.1" }, + "bun-create": { + "start": "bun run src/index.tsx" + }, "trustedDependencies": [ "@parcel/watcher", "@tailwindcss/oxide", diff --git a/prettier.config.js b/prettier.config.js index b6bca479..23d52452 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -1,6 +1,4 @@ -/** - * @type {import('prettier').Config} - */ +/** @type {import("prettier").Config} */ const config = { arrowParens: "always", printWidth: 100, diff --git a/src/converters/assimp.ts b/src/converters/assimp.ts index 42e1b6f7..61663564 100644 --- a/src/converters/assimp.ts +++ b/src/converters/assimp.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/calibre.ts b/src/converters/calibre.ts index 952f79d8..7000bd6d 100644 --- a/src/converters/calibre.ts +++ b/src/converters/calibre.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/dasel.ts b/src/converters/dasel.ts index c895d3eb..5bd5f7fd 100644 --- a/src/converters/dasel.ts +++ b/src/converters/dasel.ts @@ -1,5 +1,6 @@ import fs from "fs"; import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/djvu.ts b/src/converters/djvu.ts index 054afabd..02e48c43 100644 --- a/src/converters/djvu.ts +++ b/src/converters/djvu.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import type { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/dvisvgm.ts b/src/converters/dvisvgm.ts index a48bc69a..a284d25f 100644 --- a/src/converters/dvisvgm.ts +++ b/src/converters/dvisvgm.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/ffmpeg.ts b/src/converters/ffmpeg.ts index adf48b45..8e5959a7 100644 --- a/src/converters/ffmpeg.ts +++ b/src/converters/ffmpeg.ts @@ -1,6 +1,6 @@ import { - execFile as execFileOriginal, type ChildProcess, + execFile as execFileOriginal, type ExecFileOptions, } from "node:child_process"; diff --git a/src/converters/graphicsmagick.ts b/src/converters/graphicsmagick.ts index a51a2947..ddfaed8f 100644 --- a/src/converters/graphicsmagick.ts +++ b/src/converters/graphicsmagick.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/imagemagick.ts b/src/converters/imagemagick.ts index 767dd081..435cce72 100644 --- a/src/converters/imagemagick.ts +++ b/src/converters/imagemagick.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; // declare possible conversions diff --git a/src/converters/inkscape.ts b/src/converters/inkscape.ts index dce123bd..fd774efd 100644 --- a/src/converters/inkscape.ts +++ b/src/converters/inkscape.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/libheif.ts b/src/converters/libheif.ts index 3df1f52a..adc3af73 100644 --- a/src/converters/libheif.ts +++ b/src/converters/libheif.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/libjxl.ts b/src/converters/libjxl.ts index 5abe9ba1..3f83b469 100644 --- a/src/converters/libjxl.ts +++ b/src/converters/libjxl.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; // declare possible conversions diff --git a/src/converters/libreoffice.ts b/src/converters/libreoffice.ts index f9a06c3b..f1448565 100644 --- a/src/converters/libreoffice.ts +++ b/src/converters/libreoffice.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/main.ts b/src/converters/main.ts index 9a74a12a..d87ecd18 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -1,7 +1,9 @@ -import { Cookie } from "elysia"; -import db from "../db/db"; import fs from "node:fs"; import path from "node:path"; + +import { Cookie } from "elysia"; + +import db from "../db/db"; import { MAX_CONVERT_PROCESS } from "../helpers/env"; import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype"; import { convert as convertassimp, properties as propertiesassimp } from "./assimp"; @@ -19,16 +21,16 @@ import { convert as convertInkscape, properties as propertiesInkscape } from "./ import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif"; import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl"; import { convert as convertLibreOffice, properties as propertiesLibreOffice } from "./libreoffice"; +import { convert as convertMarkitdown, properties as propertiesMarkitdown } from "./markitdown"; import { convert as convertMsgconvert, properties as propertiesMsgconvert } from "./msgconvert"; import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc"; import { convert as convertPdftops, properties as propertiesPdftops } from "./pdftops"; import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace"; import { convert as convertresvg, properties as propertiesresvg } from "./resvg"; +import { convert as convertVcf, properties as propertiesVcf } from "./vcf"; import { convert as convertImage, properties as propertiesImage } from "./vips"; import { convert as convertVtracer, properties as propertiesVtracer } from "./vtracer"; -import { convert as convertVcf, properties as propertiesVcf } from "./vcf"; import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex"; -import { convert as convertMarkitdown, properties as propertiesMarkitdown } from "./markitdown"; // This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular diff --git a/src/converters/markitdown.ts b/src/converters/markitdown.ts index 6a61cb02..08d70d02 100644 --- a/src/converters/markitdown.ts +++ b/src/converters/markitdown.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/msgconvert.ts b/src/converters/msgconvert.ts index 612a2ef9..21ec091e 100644 --- a/src/converters/msgconvert.ts +++ b/src/converters/msgconvert.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/pandoc.ts b/src/converters/pandoc.ts index c14c98b9..ac08dd33 100644 --- a/src/converters/pandoc.ts +++ b/src/converters/pandoc.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/pdftops.ts b/src/converters/pdftops.ts index 8ac97669..8804cf28 100644 --- a/src/converters/pdftops.ts +++ b/src/converters/pdftops.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/potrace.ts b/src/converters/potrace.ts index b18f202f..e1f2af7c 100644 --- a/src/converters/potrace.ts +++ b/src/converters/potrace.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/resvg.ts b/src/converters/resvg.ts index fcdc2e46..cadc6e15 100644 --- a/src/converters/resvg.ts +++ b/src/converters/resvg.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/vips.ts b/src/converters/vips.ts index 310053fe..b5bb2531 100644 --- a/src/converters/vips.ts +++ b/src/converters/vips.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; // declare possible conversions diff --git a/src/converters/vtracer.ts b/src/converters/vtracer.ts index 31ce1109..cc24e863 100644 --- a/src/converters/vtracer.ts +++ b/src/converters/vtracer.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/converters/xelatex.ts b/src/converters/xelatex.ts index 6bd08028..8dfe3df5 100644 --- a/src/converters/xelatex.ts +++ b/src/converters/xelatex.ts @@ -1,4 +1,5 @@ import { execFile as execFileOriginal } from "node:child_process"; + import { ExecFileFn } from "./types"; export const properties = { diff --git a/src/helpers/printVersions.ts b/src/helpers/printVersions.ts index 93b615de..6d7ba3b4 100644 --- a/src/helpers/printVersions.ts +++ b/src/helpers/printVersions.ts @@ -1,5 +1,6 @@ import { exec } from "node:child_process"; import { readFile } from "node:fs"; + import { version } from "../../package.json"; console.log(`ConvertX v${version}`); diff --git a/src/index.tsx b/src/index.tsx index 57308464..995934f2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,9 @@ +import { rmSync } from "node:fs"; + import { html } from "@elysiajs/html"; import { staticPlugin } from "@elysiajs/static"; import { Elysia } from "elysia"; -import { rmSync } from "node:fs"; + import db from "./db/db"; import { Jobs } from "./db/types"; import { AUTO_DELETE_EVERY_N_HOURS, WEBROOT } from "./helpers/env"; diff --git a/src/main.css b/src/main.css index 72ded0e1..3e56e0da 100644 --- a/src/main.css +++ b/src/main.css @@ -20,13 +20,13 @@ } @utility article { - @apply px-2 sm:px-4 py-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded-sm; + @apply mx-auto mb-4 w-full max-w-4xl rounded-sm bg-neutral-800/40 px-2 py-4 sm:px-4; } @utility btn-primary { - @apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors; + @apply bg-accent-500 text-contrast hover:bg-accent-400 cursor-pointer rounded-sm p-2 transition-colors sm:p-4; } @utility btn-secondary { - @apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors; + @apply text-contrast cursor-pointer rounded-sm bg-neutral-400 p-2 transition-colors hover:bg-neutral-300 sm:p-4; } diff --git a/src/pages/chooseConverter.tsx b/src/pages/chooseConverter.tsx index 0807cbf2..ed51dea0 100644 --- a/src/pages/chooseConverter.tsx +++ b/src/pages/chooseConverter.tsx @@ -1,4 +1,5 @@ import Elysia, { t } from "elysia"; + import { getPossibleTargets } from "../converters/main"; import { userService } from "./user"; diff --git a/src/pages/convert.tsx b/src/pages/convert.tsx index 27d7c0e8..de4f7337 100644 --- a/src/pages/convert.tsx +++ b/src/pages/convert.tsx @@ -1,6 +1,8 @@ import { mkdir } from "node:fs/promises"; + import { Elysia, t } from "elysia"; import sanitize from "sanitize-filename"; + import { outputDir, uploadsDir } from ".."; import { handleConvert } from "../converters/main"; import db from "../db/db"; diff --git a/src/pages/deleteFile.tsx b/src/pages/deleteFile.tsx index 6302db9b..62808629 100644 --- a/src/pages/deleteFile.tsx +++ b/src/pages/deleteFile.tsx @@ -1,11 +1,13 @@ import { unlink } from "node:fs/promises"; +import path from "node:path"; + import { Elysia, t } from "elysia"; +import sanitize from "sanitize-filename"; + import { uploadsDir } from ".."; import db from "../db/db"; import { WEBROOT } from "../helpers/env"; import { userService } from "./user"; -import sanitize from "sanitize-filename"; -import path from "node:path"; export const deleteFile = new Elysia().use(userService).post( "/delete", diff --git a/src/pages/deleteJob.tsx b/src/pages/deleteJob.tsx index e70a32b4..5ab1cc21 100644 --- a/src/pages/deleteJob.tsx +++ b/src/pages/deleteJob.tsx @@ -1,10 +1,12 @@ import { rmSync } from "node:fs"; + import { Elysia, t } from "elysia"; + import { outputDir, uploadsDir } from ".."; import db from "../db/db"; +import { Jobs } from "../db/types"; import { WEBROOT } from "../helpers/env"; import { userService } from "./user"; -import { Jobs } from "../db/types"; export const deleteJob = new Elysia() .use(userService) diff --git a/src/pages/download.tsx b/src/pages/download.tsx index f4dc7cf4..e642bfa8 100644 --- a/src/pages/download.tsx +++ b/src/pages/download.tsx @@ -1,7 +1,9 @@ import path from "node:path"; + import { Elysia } from "elysia"; import sanitize from "sanitize-filename"; import * as tar from "tar"; + import { outputDir } from ".."; import db from "../db/db"; import { WEBROOT } from "../helpers/env"; diff --git a/src/pages/healthcheck.tsx b/src/pages/healthcheck.tsx index 7c0d4099..04efd4b6 100644 --- a/src/pages/healthcheck.tsx +++ b/src/pages/healthcheck.tsx @@ -1,4 +1,5 @@ import Elysia from "elysia"; + import { userService } from "./user"; export const healthcheck = new Elysia().use(userService).get( diff --git a/src/pages/history.tsx b/src/pages/history.tsx index d0cecdf0..7907b8e0 100644 --- a/src/pages/history.tsx +++ b/src/pages/history.tsx @@ -1,19 +1,20 @@ import { Elysia } from "elysia"; + import { BaseHtml } from "../components/base"; import { Header } from "../components/header"; import db from "../db/db"; import { Filename, Jobs } from "../db/types"; import { ALLOW_UNAUTHENTICATED, + BRANDING, HIDE_HISTORY, LANGUAGE, TIMEZONE, WEBROOT, - BRANDING, } from "../helpers/env"; -import { userService } from "./user"; -import { EyeIcon } from "../icons/eye"; import { DeleteIcon } from "../icons/delete"; +import { EyeIcon } from "../icons/eye"; +import { userService } from "./user"; export const history = new Elysia().use(userService).get( "/history", diff --git a/src/pages/listConverters.tsx b/src/pages/listConverters.tsx index 881037af..3be4e0df 100644 --- a/src/pages/listConverters.tsx +++ b/src/pages/listConverters.tsx @@ -1,8 +1,9 @@ import Elysia from "elysia"; + import { BaseHtml } from "../components/base"; import { Header } from "../components/header"; import { getAllInputs, getAllTargets } from "../converters/main"; -import { ALLOW_UNAUTHENTICATED, WEBROOT, BRANDING } from "../helpers/env"; +import { ALLOW_UNAUTHENTICATED, BRANDING, WEBROOT } from "../helpers/env"; import { userService } from "./user"; export const listConverters = new Elysia().use(userService).get( diff --git a/src/pages/results.tsx b/src/pages/results.tsx index c30dbdd8..47dc0941 100644 --- a/src/pages/results.tsx +++ b/src/pages/results.tsx @@ -1,12 +1,13 @@ import { Elysia } from "elysia"; + import { BaseHtml } from "../components/base"; import { Header } from "../components/header"; import db from "../db/db"; import { Filename, Jobs } from "../db/types"; import { buildDownloadUrl } from "../helpers/buildDownloadUrl"; -import { ALLOW_UNAUTHENTICATED, WEBROOT, BRANDING } from "../helpers/env"; -import { DownloadIcon } from "../icons/download"; +import { ALLOW_UNAUTHENTICATED, BRANDING, WEBROOT } from "../helpers/env"; import { DeleteIcon } from "../icons/delete"; +import { DownloadIcon } from "../icons/download"; import { EyeIcon } from "../icons/eye"; import { userService } from "./user"; diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 0f85f5cf..1592af34 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -1,6 +1,8 @@ import { randomInt } from "node:crypto"; + import { JWTPayloadSpec } from "@elysiajs/jwt"; import { Elysia, t } from "elysia"; + import { BaseHtml } from "../components/base"; import { Header } from "../components/header"; import { getAllTargets } from "../converters/main"; @@ -9,11 +11,11 @@ import { User } from "../db/types"; import { ACCOUNT_REGISTRATION, ALLOW_UNAUTHENTICATED, + BRANDING, HIDE_HISTORY, HTTP_ALLOWED, UNAUTHENTICATED_USER_SHARING, WEBROOT, - BRANDING, } from "../helpers/env"; import { FIRST_RUN, userService } from "./user"; diff --git a/src/pages/upload.tsx b/src/pages/upload.tsx index 0c000091..19079cf6 100644 --- a/src/pages/upload.tsx +++ b/src/pages/upload.tsx @@ -1,9 +1,10 @@ import { Elysia, t } from "elysia"; +import sanitize from "sanitize-filename"; + import db from "../db/db"; import { WEBROOT } from "../helpers/env"; import { uploadsDir } from "../index"; import { userService } from "./user"; -import sanitize from "sanitize-filename"; export const upload = new Elysia().use(userService).post( "/upload", diff --git a/src/pages/user.tsx b/src/pages/user.tsx index 29a24483..6405d257 100644 --- a/src/pages/user.tsx +++ b/src/pages/user.tsx @@ -1,4 +1,5 @@ import { Elysia, t } from "elysia"; + import { BaseHtml } from "../components/base"; import { Header } from "../components/header"; import db from "../db/db"; diff --git a/src/services/user.ts b/src/services/user.ts index d71204d8..a7e2d3d6 100644 --- a/src/services/user.ts +++ b/src/services/user.ts @@ -1,6 +1,8 @@ import { randomUUID } from "node:crypto"; + import { jwt } from "@elysiajs/jwt"; import { Elysia, t } from "elysia"; + import { WEBROOT } from "../helpers/env"; import { isHtmlPageRequest } from "../helpers/isHtmlPageRequest"; diff --git a/tests/converters/assimp.test.ts b/tests/converters/assimp.test.ts index ff8bb244..f9bef786 100644 --- a/tests/converters/assimp.test.ts +++ b/tests/converters/assimp.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/assimp"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/calibre.test.ts b/tests/converters/calibre.test.ts index 021dd5c7..586f18f1 100644 --- a/tests/converters/calibre.test.ts +++ b/tests/converters/calibre.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/calibre"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/dasel.test.ts b/tests/converters/dasel.test.ts index bb0c780c..e0c45054 100644 --- a/tests/converters/dasel.test.ts +++ b/tests/converters/dasel.test.ts @@ -1,5 +1,6 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import fs from "fs"; -import { beforeEach, afterEach, expect, test, describe } from "bun:test"; + import { buildDaselArgs, convert } from "../../src/converters/dasel"; import type { ExecFileFn } from "../../src/converters/types"; import { captureConsole } from "./helpers/converters"; diff --git a/tests/converters/djvu.test.ts b/tests/converters/djvu.test.ts index 45504301..92347996 100644 --- a/tests/converters/djvu.test.ts +++ b/tests/converters/djvu.test.ts @@ -1,5 +1,6 @@ -import type { ExecFileException } from "node:child_process"; import { expect, test } from "bun:test"; +import type { ExecFileException } from "node:child_process"; + import { convert, properties } from "../../src/converters/djvu"; import type { ExecFileFn } from "../../src/converters/types"; import { runCommonTests } from "./helpers/commonTests"; diff --git a/tests/converters/dvisvgm.test.ts b/tests/converters/dvisvgm.test.ts index 3808664a..d43783fd 100644 --- a/tests/converters/dvisvgm.test.ts +++ b/tests/converters/dvisvgm.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/dvisvgm"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/ffmpeg.test.ts b/tests/converters/ffmpeg.test.ts index 54c1b210..a6479cc9 100644 --- a/tests/converters/ffmpeg.test.ts +++ b/tests/converters/ffmpeg.test.ts @@ -1,6 +1,7 @@ import { afterAll, afterEach, beforeEach, expect, test } from "bun:test"; -import { convert } from "../../src/converters/ffmpeg"; import type { ExecFileOptions } from "node:child_process"; + +import { convert } from "../../src/converters/ffmpeg"; import { captureConsole } from "./helpers/converters"; let calls: string[][] = []; diff --git a/tests/converters/graphicsmagick.test.ts b/tests/converters/graphicsmagick.test.ts index 7a7b9284..51eba24e 100644 --- a/tests/converters/graphicsmagick.test.ts +++ b/tests/converters/graphicsmagick.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/graphicsmagick"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/helpers/commonTests.ts b/tests/converters/helpers/commonTests.ts index 9c9872e7..5b690839 100644 --- a/tests/converters/helpers/commonTests.ts +++ b/tests/converters/helpers/commonTests.ts @@ -1,4 +1,5 @@ import { test } from "bun:test"; + import { ConvertFnWithExecFile } from "../../../src/converters/types"; import { runConvertFailTest, diff --git a/tests/converters/helpers/converters.ts b/tests/converters/helpers/converters.ts index a7269313..e17018a7 100644 --- a/tests/converters/helpers/converters.ts +++ b/tests/converters/helpers/converters.ts @@ -1,11 +1,12 @@ import { expect } from "bun:test"; + import type { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types"; type ExecFileCall = { cmd: string; args: string[] }; /** - * Creates an execFile replacement that records every invocation and answers with the - * given error/stdout/stderr, so converter tests never spawn real binaries. + * Creates an execFile replacement that records every invocation and answers with the given + * error/stdout/stderr, so converter tests never spawn real binaries. */ export function createMockExecFile({ error = null, @@ -21,8 +22,8 @@ export function createMockExecFile({ } /** - * Redirects console.log/error/warn into arrays so tests can assert on them without - * printing to the terminal. Always call restore() afterwards (e.g. in afterEach). + * Redirects console.log/error/warn into arrays so tests can assert on them without printing to the + * terminal. Always call restore() afterwards (e.g. in afterEach). */ export function captureConsole() { const original = { log: console.log, error: console.error, warn: console.warn }; diff --git a/tests/converters/imagemagick.test.ts b/tests/converters/imagemagick.test.ts index 2f176364..eef9122c 100644 --- a/tests/converters/imagemagick.test.ts +++ b/tests/converters/imagemagick.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/imagemagick"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/inkscape.test.ts b/tests/converters/inkscape.test.ts index b0697461..f75aa8eb 100644 --- a/tests/converters/inkscape.test.ts +++ b/tests/converters/inkscape.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/inkscape"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/libheif.test.ts b/tests/converters/libheif.test.ts index 4001f301..8a6a5bdd 100644 --- a/tests/converters/libheif.test.ts +++ b/tests/converters/libheif.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/libheif"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/libjxl.test.ts b/tests/converters/libjxl.test.ts index c5415384..348bca74 100644 --- a/tests/converters/libjxl.test.ts +++ b/tests/converters/libjxl.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/libjxl"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/libreoffice.test.ts b/tests/converters/libreoffice.test.ts index 532b55ef..21d3c611 100644 --- a/tests/converters/libreoffice.test.ts +++ b/tests/converters/libreoffice.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; -import { convert } from "../../src/converters/libreoffice"; + +import { convert, filters, getFilters } from "../../src/converters/libreoffice"; import type { ExecFileFn } from "../../src/converters/types"; -import { filters, getFilters } from "../../src/converters/libreoffice"; function requireDefined(value: T, msg: string): NonNullable { if (value === undefined || value === null) throw new Error(msg); diff --git a/tests/converters/main.test.ts b/tests/converters/main.test.ts index 96e05e50..00b7bd92 100644 --- a/tests/converters/main.test.ts +++ b/tests/converters/main.test.ts @@ -3,7 +3,9 @@ import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; + import type { Cookie } from "elysia"; + import { properties as libreofficeProperties } from "../../src/converters/libreoffice"; import { captureConsole } from "./helpers/converters"; diff --git a/tests/converters/markitdown.test.ts b/tests/converters/markitdown.test.ts index 3c05ac02..c8e33db8 100644 --- a/tests/converters/markitdown.test.ts +++ b/tests/converters/markitdown.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; + import { convert } from "../../src/converters/markitdown"; import { runCommonTests } from "./helpers/commonTests"; import { captureConsole, createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/msgconvert.test.ts b/tests/converters/msgconvert.test.ts index 02e64085..bfb0be96 100644 --- a/tests/converters/msgconvert.test.ts +++ b/tests/converters/msgconvert.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; + import { convert } from "../../src/converters/msgconvert"; import { captureConsole, createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/pandoc.test.ts b/tests/converters/pandoc.test.ts index 7904ac85..5ec9e308 100644 --- a/tests/converters/pandoc.test.ts +++ b/tests/converters/pandoc.test.ts @@ -1,4 +1,5 @@ -import { expect, test, describe } from "bun:test"; +import { describe, expect, test } from "bun:test"; + import { convert } from "../../src/converters/pandoc"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/pdftops.test.ts b/tests/converters/pdftops.test.ts index b3f94765..52c257ba 100644 --- a/tests/converters/pdftops.test.ts +++ b/tests/converters/pdftops.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; + import { convert } from "../../src/converters/pdftops"; import { runCommonTests } from "./helpers/commonTests"; import { captureConsole, createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/potrace.test.ts b/tests/converters/potrace.test.ts index cf1ebf65..48197243 100644 --- a/tests/converters/potrace.test.ts +++ b/tests/converters/potrace.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/potrace"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/resvg.test.ts b/tests/converters/resvg.test.ts index ad06a263..a51e17dd 100644 --- a/tests/converters/resvg.test.ts +++ b/tests/converters/resvg.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/resvg"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/vcf.test.ts b/tests/converters/vcf.test.ts index e4d7f571..d6bf2412 100644 --- a/tests/converters/vcf.test.ts +++ b/tests/converters/vcf.test.ts @@ -1,8 +1,9 @@ -import { afterAll, beforeAll, expect, test, describe } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; + import { convert, parseVCF, toCSV } from "../../src/converters/vcf"; describe("parseVCF", () => { diff --git a/tests/converters/vips.test.ts b/tests/converters/vips.test.ts index 6c264cf8..6b2a7ec4 100644 --- a/tests/converters/vips.test.ts +++ b/tests/converters/vips.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/vips"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/vtracer.test.ts b/tests/converters/vtracer.test.ts index f78718ad..edfcff46 100644 --- a/tests/converters/vtracer.test.ts +++ b/tests/converters/vtracer.test.ts @@ -1,4 +1,5 @@ -import { afterEach, beforeEach, expect, test, describe } from "bun:test"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; + import { convert } from "../../src/converters/vtracer"; import { captureConsole, createMockExecFile } from "./helpers/converters"; diff --git a/tests/converters/xelatex.test.ts b/tests/converters/xelatex.test.ts index 5e540664..65631ce1 100644 --- a/tests/converters/xelatex.test.ts +++ b/tests/converters/xelatex.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; + import { convert } from "../../src/converters/xelatex"; import { runCommonTests } from "./helpers/commonTests"; import { createMockExecFile } from "./helpers/converters"; diff --git a/tests/db/db.test.ts b/tests/db/db.test.ts index 21f4ce58..3e2e8357 100644 --- a/tests/db/db.test.ts +++ b/tests/db/db.test.ts @@ -1,6 +1,6 @@ -import { test, expect, beforeEach, afterEach } from "bun:test"; import { Database } from "bun:sqlite"; -import { unlinkSync, existsSync } from "node:fs"; +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { existsSync, unlinkSync } from "node:fs"; import { join } from "node:path"; const testRoot = process.env.CONVERTX_TEST_ROOT; diff --git a/tests/helpers/normalizeFiletype.test.ts b/tests/helpers/normalizeFiletype.test.ts index d4878362..f2a555c2 100644 --- a/tests/helpers/normalizeFiletype.test.ts +++ b/tests/helpers/normalizeFiletype.test.ts @@ -1,4 +1,5 @@ -import { test, expect } from "bun:test"; +import { expect, test } from "bun:test"; + import { normalizeFiletype, normalizeOutputFiletype } from "../../src/helpers/normalizeFiletype"; test("normalizeFiletype maps known inputs", () => { diff --git a/tests/pages/auth.test.ts b/tests/pages/auth.test.ts index 1e97faea..31c4c423 100644 --- a/tests/pages/auth.test.ts +++ b/tests/pages/auth.test.ts @@ -1,5 +1,7 @@ import { expect, test } from "bun:test"; + import { Elysia } from "elysia"; + import { isHtmlPageRequest } from "../../src/helpers/isHtmlPageRequest"; import { userService } from "../../src/services/user"; diff --git a/tests/pages/chooseConverter.test.ts b/tests/pages/chooseConverter.test.ts index 3042febf..2d797e87 100644 --- a/tests/pages/chooseConverter.test.ts +++ b/tests/pages/chooseConverter.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import { request } from "./helpers/app"; async function conversionsFor(fileType: string) { diff --git a/tests/pages/convert.test.ts b/tests/pages/convert.test.ts index ddcb4001..0983183d 100644 --- a/tests/pages/convert.test.ts +++ b/tests/pages/convert.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { mkdirSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; + import { createJob, createUser, db, jobDirs, request, waitFor } from "./helpers/app"; type JobRow = { status: string; num_files: number }; diff --git a/tests/pages/deleteFile.test.ts b/tests/pages/deleteFile.test.ts index 5fe8c542..140fcb78 100644 --- a/tests/pages/deleteFile.test.ts +++ b/tests/pages/deleteFile.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; + import { createJob, createUser, jobDirs, request, uploadsDir } from "./helpers/app"; async function userWithUploads(...fileNames: string[]) { diff --git a/tests/pages/deleteJob.test.ts b/tests/pages/deleteJob.test.ts index 1ef31dbf..e7194892 100644 --- a/tests/pages/deleteJob.test.ts +++ b/tests/pages/deleteJob.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; + import { createJob, createUser, db, jobDirs, request } from "./helpers/app"; async function userWithJobs(count = 1) { diff --git a/tests/pages/download.test.ts b/tests/pages/download.test.ts index f6a2e75e..a8a8d553 100644 --- a/tests/pages/download.test.ts +++ b/tests/pages/download.test.ts @@ -2,7 +2,9 @@ import { describe, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; + import * as tar from "tar"; + import { createJob, createUser, jobDirs, request } from "./helpers/app"; async function userWithOutputs(files: Record) { diff --git a/tests/pages/firstRun.test.ts b/tests/pages/firstRun.test.ts index 4665b366..4840069f 100644 --- a/tests/pages/firstRun.test.ts +++ b/tests/pages/firstRun.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import type { User } from "../../src/db/types"; import { db, FIRST_ACCOUNT, firstRun as recordedFirstRun, SECOND_ACCOUNT } from "./helpers/app"; diff --git a/tests/pages/healthcheck.test.ts b/tests/pages/healthcheck.test.ts index 807a17ce..9701cdb7 100644 --- a/tests/pages/healthcheck.test.ts +++ b/tests/pages/healthcheck.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import { request } from "./helpers/app"; describe("GET /healthcheck", () => { diff --git a/tests/pages/helpers/app.ts b/tests/pages/helpers/app.ts index ae889483..82af5f82 100644 --- a/tests/pages/helpers/app.ts +++ b/tests/pages/helpers/app.ts @@ -1,6 +1,8 @@ import { mock } from "bun:test"; + import { html } from "@elysiajs/html"; import { Elysia } from "elysia"; + import * as realEnvModule from "../../../src/helpers/env"; if (!process.env.CONVERTX_TEST_ROOT) { diff --git a/tests/pages/history.test.ts b/tests/pages/history.test.ts index 7e04fdd3..6216d8ba 100644 --- a/tests/pages/history.test.ts +++ b/tests/pages/history.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; + import { createJob, createUser, request, resetEnv, setEnv } from "./helpers/app"; afterEach(() => { diff --git a/tests/pages/listConverters.test.ts b/tests/pages/listConverters.test.ts index 22582265..03cdc206 100644 --- a/tests/pages/listConverters.test.ts +++ b/tests/pages/listConverters.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import { getAllInputs, getAllTargets } from "../../src/converters/main"; import { createUser, request } from "./helpers/app"; diff --git a/tests/pages/results.test.ts b/tests/pages/results.test.ts index 44e8d8ec..b30ecd05 100644 --- a/tests/pages/results.test.ts +++ b/tests/pages/results.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; + import { buildDownloadUrl } from "../../src/helpers/buildDownloadUrl"; import { createJob, createUser, request, resetEnv, setCookie, setEnv } from "./helpers/app"; diff --git a/tests/pages/root.test.ts b/tests/pages/root.test.ts index b4f39b3d..15b0a8c1 100644 --- a/tests/pages/root.test.ts +++ b/tests/pages/root.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; + import { app, createUser, diff --git a/tests/pages/upload.test.ts b/tests/pages/upload.test.ts index 34b37f87..caec8cc3 100644 --- a/tests/pages/upload.test.ts +++ b/tests/pages/upload.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { existsSync, readdirSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; + import { createJob, createUser, jobDirs, request } from "./helpers/app"; function uploadForm(...files: File[]) { diff --git a/tests/pages/user.test.ts b/tests/pages/user.test.ts index 1be61164..08486597 100644 --- a/tests/pages/user.test.ts +++ b/tests/pages/user.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; + import type { User } from "../../src/db/types"; import { app, diff --git a/tests/services/user.test.ts b/tests/services/user.test.ts index 73f0ec0a..ec5854c3 100644 --- a/tests/services/user.test.ts +++ b/tests/services/user.test.ts @@ -1,7 +1,9 @@ import { describe, expect, test } from "bun:test"; import { randomUUID } from "node:crypto"; + import { jwt } from "@elysiajs/jwt"; import { Elysia } from "elysia"; + import { WEBROOT } from "../../src/helpers/env"; import { userService } from "../../src/services/user"; diff --git a/tsconfig.json b/tsconfig.json index cb524d98..a5775d64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,6 @@ "noEmit": false, "composite": true, "strict": true, - "downlevelIteration": true, "skipLibCheck": true, "jsx": "react-jsx", "jsxImportSource": "@kitajs/html", From 64cd0130d3ae1fdc9154b77a050a2e8a37efc7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emrik=20=C3=96stling?= Date: Sun, 25 Oct 2026 19:52:26 +0100 Subject: [PATCH 11/14] feature: use bun archive instead of tar package (#678) Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- bun.lock | 13 ---------- package.json | 3 +-- src/pages/download.tsx | 48 +++++++++++++++++++++++++++--------- tests/pages/download.test.ts | 35 +++++++++++++------------- 4 files changed, 55 insertions(+), 44 deletions(-) diff --git a/bun.lock b/bun.lock index 5dcaf6e1..28ed1799 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,6 @@ "@kitajs/html": "^4.2.13", "elysia": "1.4.30", "sanitize-filename": "^1.6.4", - "tar": "^7.5.22", }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -89,8 +88,6 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -315,8 +312,6 @@ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -483,10 +478,6 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - - "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -577,8 +568,6 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -623,8 +612,6 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], "yargs": ["yargs@18.2.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w=="], diff --git a/package.json b/package.json index 7c6f6aaf..62c7d96d 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,7 @@ "@elysiajs/static": "^1.4.10", "@kitajs/html": "^4.2.13", "elysia": "1.4.30", - "sanitize-filename": "^1.6.4", - "tar": "^7.5.22" + "sanitize-filename": "^1.6.4" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/src/pages/download.tsx b/src/pages/download.tsx index e642bfa8..60cbf02c 100644 --- a/src/pages/download.tsx +++ b/src/pages/download.tsx @@ -1,14 +1,17 @@ +import { randomUUID } from "node:crypto"; +import { rename, unlink } from "node:fs/promises"; import path from "node:path"; import { Elysia } from "elysia"; import sanitize from "sanitize-filename"; -import * as tar from "tar"; import { outputDir } from ".."; import db from "../db/db"; import { WEBROOT } from "../helpers/env"; import { userService } from "./user"; +const archiveBuilds = new Map>(); + export const download = new Elysia() .use(userService) .get( @@ -55,16 +58,39 @@ export const download = new Elysia() const outputPath = `${outputDir}${userId}/${jobId}`; const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`); - await tar.create( - { - file: outputTar, - cwd: outputPath, - filter: (path) => { - return !path.match(".*\\.tar"); - }, - }, - ["."], - ); + const archiveKey = `${userId}/${jobId}`; + let archiveBuild = archiveBuilds.get(archiveKey); + if (!archiveBuild) { + const build = async () => { + const outputTarName = path.basename(outputTar); + const outputTarTempName = `${outputTarName}.tmp-${randomUUID()}`; + const outputTarTemp = path.join(outputPath, outputTarTempName); + + try { + const files: Record = {}; + const glob = new Bun.Glob("**/*"); + for await (const file of glob.scan({ cwd: outputPath, onlyFiles: true, dot: true })) { + if (file !== outputTarName && !file.startsWith(`${outputTarName}.tmp-`)) { + files[file] = new Uint8Array( + await Bun.file(path.join(outputPath, file)).arrayBuffer(), + ); + } + } + await Bun.Archive.write(outputTarTemp, files); + await rename(outputTarTemp, outputTar); + } finally { + if (await Bun.file(outputTarTemp).exists()) { + await unlink(outputTarTemp); + } + } + }; + + archiveBuild = build().finally(() => { + archiveBuilds.delete(archiveKey); + }); + archiveBuilds.set(archiveKey, archiveBuild); + } + await archiveBuild; return Bun.file(outputTar); }, { diff --git a/tests/pages/download.test.ts b/tests/pages/download.test.ts index a8a8d553..be887f1b 100644 --- a/tests/pages/download.test.ts +++ b/tests/pages/download.test.ts @@ -1,13 +1,9 @@ import { describe, expect, test } from "bun:test"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import * as tar from "tar"; +import { mkdirSync, writeFileSync } from "node:fs"; import { createJob, createUser, jobDirs, request } from "./helpers/app"; -async function userWithOutputs(files: Record) { +async function userWithOutputs(files: Record) { const user = await createUser(); const job = createJob(user.id); const dirs = jobDirs(user.id, job); @@ -19,16 +15,8 @@ async function userWithOutputs(files: Record) { } async function tarEntries(response: Response) { - const dir = mkdtempSync(join(tmpdir(), "convertx-archive-")); - try { - const file = join(dir, "archive.tar"); - writeFileSync(file, Buffer.from(await response.arrayBuffer())); - const entries: string[] = []; - await tar.list({ file, onReadEntry: (entry) => entries.push(entry.path) }); - return entries.sort(); - } finally { - rmSync(dir, { recursive: true, force: true }); - } + const archive = new Bun.Archive(await response.arrayBuffer()); + return [...(await archive.files()).keys()].sort(); } describe("GET /download/:userId/:jobId/:fileName", () => { @@ -142,7 +130,18 @@ describe("GET /archive/:jobId", () => { expect(response.status).toBe(200); expect(response.headers.get("content-type")).toBe("application/x-tar"); - expect(await tarEntries(response)).toEqual(["./", "./a.csv", "./b.csv"]); + expect(await tarEntries(response)).toEqual(["a.csv", "b.csv"]); + }); + + test("preserves binary file contents", async () => { + const { user, job } = await userWithOutputs({ "image.png": new Uint8Array([0, 1, 2, 255]) }); + + const response = await request(`/archive/${job}`, { cookies: { auth: user.token } }); + const archive = new Bun.Archive(await response.arrayBuffer()); + const image = (await archive.files()).get("image.png"); + + expect(image).toBeDefined(); + expect(await image?.arrayBuffer()).toEqual(new Uint8Array([0, 1, 2, 255]).buffer); }); test("does not include earlier archives in a new archive", async () => { @@ -151,7 +150,7 @@ describe("GET /archive/:jobId", () => { await request(`/archive/${job}`, { cookies: { auth: user.token } }); const response = await request(`/archive/${job}`, { cookies: { auth: user.token } }); - expect(await tarEntries(response)).toEqual(["./", "./a.csv"]); + expect(await tarEntries(response)).toEqual(["a.csv"]); }); test("refuses another user's job", async () => { From 3de071df2762a77614c418a6940faa1d43f7cb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emrik=20=C3=96stling?= Date: Mon, 26 Oct 2026 20:10:18 +0100 Subject: [PATCH 12/14] Merge commit from fork --- public/script.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 5c2e3376..e317e197 100644 --- a/public/script.js +++ b/public/script.js @@ -39,11 +39,12 @@ function handleFile(file) { const row = document.createElement("tr"); row.innerHTML = ` - ${file.name} + ${(file.size / 1024).toFixed(2)} kB `; + row.firstElementChild.textContent = file.name; if (!fileType) { fileType = file.name.split(".").pop(); From 5d4d6bac002fc3daec50791bbd8bfcc3f1fce0cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:14:09 +0100 Subject: [PATCH 13/14] chore(deps): lock file maintenance (#691) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 134 ++++++++++++++++++++++++++----------------------------- 1 file changed, 64 insertions(+), 70 deletions(-) diff --git a/bun.lock b/bun.lock index 28ed1799..55876f9f 100644 --- a/bun.lock +++ b/bun.lock @@ -106,47 +106,47 @@ "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.4", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.9.3" }, "bin": { "xss-scan": "dist/cli.js", "ts-html-plugin": "dist/cli.js" } }, "sha512-xK5mNrhnIy73kJFKx5yVGChJyWFRGmIaE0sjlVxVYllk5dyaEYVCrIh1N8AfnseEHka8gAqzPGW95HlkhDvnJA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" } }, "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw=="], - "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.154.0", "", { "os": "android", "cpu": "arm" }, "sha512-sDdEtDl7vKdCvf54j72RJbowE3jCmyRz/+oOB6IAwQxAHZ0eI+Dcj4hvVnm0b9Dy31plUkdmfds9C+/Zt895Tg=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.155.0", "", { "os": "android", "cpu": "arm" }, "sha512-A6xv7hDtnTm+xlZ7utn6rRQpRMJHRhVr3gjRGqlx0RUX84mNk0pAsy6YN/wWhuOTkaZhRrrzaLxf0NfbH77j2w=="], - "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.154.0", "", { "os": "android", "cpu": "arm64" }, "sha512-UHrDPELzvWZoVkIDdSycj4bzcTMCJG3IbN++mSUgVBaUtNtWHjQ98U6Vz5bpF/1Ic3FYJyYhMr74huO5tVvpZw=="], + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.155.0", "", { "os": "android", "cpu": "arm64" }, "sha512-T1wWwH9/JI5g7Z4kEQwDOwhToxdDhRyOTklH+uuNpJ16m3MZFmGl+l2rKc0NHQdbVRG2LTOBsNjpW3h1BWytPA=="], - "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.154.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mokYzajOSXxtSRDWCdHARGdntrKH+rZZ24Jzo8AtafyNzz0NARmpMm26Tc5B6qiGrr6UliWibHCOif1qRN2vaA=="], + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.155.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZoRB3qJxJ8DWA41jHja+YeB0lLfjBy4T+Kr15vnN1k65X3zbJt3rPZftEwN4Ju4tmA4K/T9dWWD7Z8ewQLnrVA=="], - "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.154.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WbtUBrclOVItWfLRSvs+Z/WLtXxJ2Ctv50PyuX894HzDN5/e0qzxVpTNPIVMzOerBhgKtFsSMcfxlYIAbvdVgg=="], + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.155.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vxtL86BtM+IgrpejA86nbTk5j0HKTIMeuaJ1fQciw6p9RfavPtIPjI7gN34owap0yG9txXBaAWpGdxokNwdqIw=="], - "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.154.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YooHZSLawOe2fqm+lDdG8oAaQh2NYV/RMYTYzWRLy72stm2BgE9O/8Al6kFENjkmzyyBccDLLBirGxVtpzQfrw=="], + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.155.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tN6Ru5e0UVnWaIuiH8jEwnvxuxg7FvhWwSd3oe5oDhOD4ObsSr9/N6WfHLjilUWlPPj65ptwrSDH0Y7lzwTzmw=="], - "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.154.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hKOwhZbrXdTwc4bGVfbLCXl9f2swl6zWdISocLQSNX9MgPmwHE3qGzOUkCrkuGpwdCVHPnkfOwsqTuYsrP5uNg=="], + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.155.0", "", { "os": "linux", "cpu": "arm" }, "sha512-h4ozA5n6L6lwolsz9iwunEhKWnIVbmFRots5QxEix0H6nJ5ICpuDa7PuShuhfpIDFe3+Qvyscioyiv+9g+4Syg=="], - "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.154.0", "", { "os": "linux", "cpu": "arm" }, "sha512-tnjiooXEU013349O/yxKbF5Cusp1CfHWQ8kYR8tGeRzR/P4sOpQGQo21fsS71M0fMD6ESLq+lbvXFWi43PuKeQ=="], + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.155.0", "", { "os": "linux", "cpu": "arm" }, "sha512-058/+LardpfuUlsKIvBGk/CYoakEVcMgqYx8GgyUZeOxFWTojYLlCc1Gb60/y21v3esDhHidgiY/Zbq9LpBhaQ=="], - "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.154.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Cao2kj+8d7BT6XJU1gV9eVQjV+Cr+e8vQm3tddDRZiM2hHrMDxwy3j6t+ofaSl0idyuPPCZ4AlpmQoj8yOT1QA=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.155.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-nrOt2Y3xIAzu+lSPzH0864ieO+iCCXgoo9qFlGANE8Xh0cQ+RP8Wstfp+/wXN2YCyb98cAGzX0VruKYw7it/jw=="], - "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.154.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uiwA0V3q8llWA7/8r+a9BBjk4j5YMDmi6wfOsyQfGOJMQSLHEiZ3HI8ulea766ykUa9wJFWk9XWEl4uJrZLQqw=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.155.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-KYAayyN5hQbJPtGKtRxlyc8XYmyntOplyXSQzTDrmbz7l/GCrzQbSswvyJFBTH1LGtsZrbXOAfN52i/Kz6kLKg=="], - "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.154.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-LICfN0Xs4Zdko+bJy6eOAEQojDuIR26suF2JtOsMaXjgbCrpRSH7/NCvJhY13YvYBx12V6hnwrL8n0lk0rbNJQ=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.155.0", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-CSuSY5hfjtsBnHXydOj8jiNfS1qHbdPZuPz1YL1cSgK7VmNYP+QlZIbG0BBYYBgX/vYEgZwbG9mBdW4YairZ5w=="], - "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.154.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbbSYxLF8ssRbQ2qQPy+2EhGNwNH1bAoRdkk6cnWaKHqmCikpSIPtpXjFbCCtjTi0uojqOM3ghFQwQPbDQsU0Q=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.155.0", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-7KCfOQm+Nz2N8trAXbQkal4TQ5756WVEmhm35y816YHvyFnEfVsICsiV5k0pEszNf/fRm4cm2i6i18SJ6/iCWg=="], - "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.154.0", "", { "os": "linux", "cpu": "none" }, "sha512-nSnRYYDLCcRVjSQzF3g6rKqCNYwo5o+aQgnn8BBXfe+ofCgMGlGqkfRyvXm6ptZvoxNkkWtLlqM8W7Xc3j6ZnQ=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.155.0", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-JSlhgoK+/g1TGJI5NuADx2+6syhxnQNLCWLXyO5QLFyetomcr/vIxSdiNlmyAv963YXUzCuwAwRG6npZ5xyJ1A=="], - "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.154.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BxBQoJKtzRQgZEKQIt6CDUm8RGKC5T8Qo4576yDa5/xgc10X3fBDItMbRabzLtbUBfTqKLl+8AVr/H6CF7W22g=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.155.0", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-WFIX+dPokWp64mi434Md3U3lJaTwNxAphvGjpltoyQjGUsdXavOIRrnp9dOS34Hd75HOAwDxAI+YwWOwqmJbtg=="], - "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.154.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/whkust2vYy+GjRUVL5cWaJmEu6jkdFfo5mkxgckyeOX61eMwo4iBoiwzeJZeMWAdzb/XOtQyLApHhFht3sOWA=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.155.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-g6xSDC/JpbnVOz5uafMGyK3txBVRFMQTx8opv/yHXGxqV3ZMVl3bf7W8XrSnF68dmy3gM4dgReNryuE53+WkeQ=="], - "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.154.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EXURTyW9riw4B/kZnnE+58emAtw6+3VEszQfXUZkRqGaKIcmWDYkZtKB+xY/blHvuZ+Qy/fDclc31nmUCgtkSA=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.155.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-6/4xlxKlZJWrQ0uwDFPc0H9+xLu7068CKhX3vIDW3SbvP/lVgnVuT5v6dyugeWskWpuxqF/WJ4WKwhxjU3pG2w=="], - "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.154.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ZRFooyH3qsR6vuR4jFGlG3XM36490SvvXtNaUahBK4zHpooMdTHNaOMtGGJI0EW38m5hi4rOco9kw7ZwCIp28w=="], + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.155.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tltklL0J0XjDxIRTBXWOBhvc2mitcmES45pIxhwDWfxRyqz7Vkeu0NekTZEIxGE4f/lXgq3vZcjjffteQOVorg=="], - "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.154.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-U0Vp+4lvwUa/PnOCJTZOjydj1rX2+VLHz4Y2vSFqlN8/7LTiURgD7z4uQ9yNv9O/NlU3mELBnPDlu7aaSYG1zQ=="], + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.155.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-alAb9TzjatUEsOeoncAziUnfdTDxfyswhalFZ9sC6plUgATIUx2NIZ0+bMn6aJ28AduS1OpG/OAisr/YISn4Ig=="], - "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.154.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-Hc8QgsFY5eUUP25we8TISUruibG2cDt0H4IcPG9RKdabb6odkCzIqjV6Gk9beGWGuOwMVCr81H/Su1q363LCCg=="], + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.155.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-yc5cbSfcGmRv/KpxDFuyk8adEBaWd/9HilWvBbQ4bFbus1V2PUh2mGB2Cfq9rqZTqLeiMT8x0skV+LKp9hOMgQ=="], - "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.154.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hD7judkUMA/hZPoamvKADs6+an1smp8UTK0aMYRijkvi00K/1vl7vEtmyn2JyQ9S+vibtnfcNzxqTLGyJUZCRQ=="], + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.155.0", "", { "os": "win32", "cpu": "x64" }, "sha512-8heq6jGq+rfpTDRDjoQiBHw0maQZf3IXkS99pdHQZPoTL5kyF5qyPS0RidUlSdnWDIqxXb5nux7GBD9cT+imUw=="], - "@oxc-project/types": ["@oxc-project/types@0.154.0", "", {}, "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA=="], + "@oxc-project/types": ["@oxc-project/types@0.155.0", "", {}, "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q=="], "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], @@ -162,21 +162,21 @@ "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA=="], - "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA=="], + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.24.2", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA=="], - "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw=="], + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.24.2", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw=="], - "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA=="], + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.24.2", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA=="], - "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw=="], + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.24.2", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw=="], - "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w=="], + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.24.2", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w=="], - "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ=="], + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.24.2", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ=="], - "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg=="], + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.24.2", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg=="], - "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw=="], + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.24.2", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw=="], "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg=="], @@ -186,7 +186,7 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="], - "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], + "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" }, "hasInstallScript": true }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], @@ -196,17 +196,17 @@ "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="], - "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="], + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm", "libc": "glibc" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="], - "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm", "libc": "musl" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], - "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], - "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], - "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], - "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="], @@ -234,13 +234,13 @@ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], @@ -250,7 +250,7 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], - "@tokenizer/inflate": ["@tokenizer/inflate@0.5.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.2" } }, "sha512-56U2O6fDLFtqokwWh597IdeUgtM7j+TT9tb1soybhgA6cy163gAcVmoQr8H4Ud/CkgCkViwcuLyqtn6wYqw59Q=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.5.2", "", { "dependencies": { "debug": "^4.4.3", "strtok3": "^10.6.2", "token-types": "^6.1.2" } }, "sha512-3AZIuReWQ7i916s/UWAzy4V4k2wrkAZLW2sTi448W3w7Hz5uoc+Y7h+cnwzr0cwrwf3PsC5xKsH8MOl0NRltlg=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -268,25 +268,25 @@ "@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.71.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.71.2", "@typescript-eslint/type-utils": "8.71.2", "@typescript-eslint/utils": "8.71.2", "@typescript-eslint/visitor-keys": "8.71.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.71.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.72.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.72.0", "@typescript-eslint/type-utils": "8.72.0", "@typescript-eslint/utils": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.72.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.71.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.71.2", "@typescript-eslint/types": "8.71.2", "@typescript-eslint/typescript-estree": "8.71.2", "@typescript-eslint/visitor-keys": "8.71.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.72.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.72.0", "@typescript-eslint/types": "8.72.0", "@typescript-eslint/typescript-estree": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.71.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.71.2", "@typescript-eslint/types": "^8.71.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.72.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.72.0", "@typescript-eslint/types": "^8.72.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.71.2", "", { "dependencies": { "@typescript-eslint/types": "8.71.2", "@typescript-eslint/visitor-keys": "8.71.2" } }, "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.72.0", "", { "dependencies": { "@typescript-eslint/types": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0" } }, "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.71.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.72.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.71.2", "", { "dependencies": { "@typescript-eslint/types": "8.71.2", "@typescript-eslint/typescript-estree": "8.71.2", "@typescript-eslint/utils": "8.71.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.72.0", "", { "dependencies": { "@typescript-eslint/types": "8.72.0", "@typescript-eslint/typescript-estree": "8.72.0", "@typescript-eslint/utils": "8.72.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.71.2", "", {}, "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.72.0", "", {}, "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.71.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.71.2", "@typescript-eslint/tsconfig-utils": "8.71.2", "@typescript-eslint/types": "8.71.2", "@typescript-eslint/visitor-keys": "8.71.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.72.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.72.0", "@typescript-eslint/tsconfig-utils": "8.72.0", "@typescript-eslint/types": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.71.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.71.2", "@typescript-eslint/types": "8.71.2", "@typescript-eslint/typescript-estree": "8.71.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.72.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.72.0", "@typescript-eslint/types": "8.72.0", "@typescript-eslint/typescript-estree": "8.72.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.71.2", "", { "dependencies": { "@typescript-eslint/types": "8.71.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.72.0", "", { "dependencies": { "@typescript-eslint/types": "8.72.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA=="], "@valibot/to-json-schema": ["@valibot/to-json-schema@1.8.0", "", { "peerDependencies": { "valibot": "^1.5.0" } }, "sha512-a0M+uwCuQZEPAo65NYkFSJ14O5c213KoSZmPdoCfuFvUhfULi4T2Z6Tpjv6lTVMW9RoLm74RpRAjNEa43KcN+g=="], @@ -344,7 +344,7 @@ "eslint": ["eslint@10.13.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg=="], - "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@4.9.1", "", { "dependencies": { "@eslint/css-tree": "^4.0.4", "@valibot/to-json-schema": "^1.7.1", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "synckit": "^0.12.3", "tailwind-csstree": "^0.3.3", "tsconfig-paths-webpack-plugin": "^4.2.0", "valibot": "^1.4.2" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "oxlint": "^1.35.0", "tailwindcss": "^3.3.0 || ^4.1.17" }, "optionalPeers": ["eslint", "oxlint"] }, "sha512-WOuUOlc27Ib3tcYNt6qhRpvQUie5Y/pNTkwO779QGdfF1tu0k51avWtmvfPv++7/1oVjntoFHcUK4+5a6SPZvA=="], + "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@4.10.0", "", { "dependencies": { "@eslint/css-tree": "^4.0.4", "@valibot/to-json-schema": "^1.7.1", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "synckit": "^0.12.3", "tailwind-csstree": "^0.3.3", "tsconfig-paths-webpack-plugin": "^4.2.0", "valibot": "^1.4.2" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "oxlint": "^1.35.0", "tailwindcss": "^3.3.0 || ^4.1.17" }, "optionalPeers": ["eslint", "oxlint"] }, "sha512-EvcfEScF7N39LXuqX8fDnDsgylhbBZe5bbNb+6oN8ht06ou3MvwfPQ5hVgm2LWIPLOB5LBwNDCY6gBQGW4KHGA=="], "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], @@ -422,7 +422,7 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], + "jose": ["jose@6.2.13", "", {}, "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@6.0.0", "", {}, "sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A=="], @@ -434,7 +434,7 @@ "keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], - "knip": ["knip@6.42.0", "", { "dependencies": { "formatly": "^0.11.4", "get-tsconfig": "4.14.3", "jiti": "^2.7.0", "oxc-parser": "^0.154.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.7", "smol-toml": "^1.9.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^6.0.0", "yaml": "^2.9.1", "zod": "^4.6.5" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-lNypqHwi38RJQG8pVuItqzCDPMOfPbBExyFh8aM4yMJRHVU7t5OQAtEK4tDkaB3rLHetOcVMhRFAgv5Vn2CM0A=="], + "knip": ["knip@6.43.0", "", { "dependencies": { "formatly": "^0.11.4", "get-tsconfig": "4.14.3", "jiti": "^2.7.0", "oxc-parser": "^0.155.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.7", "smol-toml": "^1.9.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^6.0.1", "yaml": "^2.9.1", "zod": "^4.6.5" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-8l3cCOg/EXoILO0qcSoMra49yQM91+lEIR4fu5Ko8rrArGX8xKdKZiGI9psPiY6rhLmfLERcMfFEByF0wli5CA=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -450,13 +450,13 @@ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], @@ -490,13 +490,13 @@ "npm-normalize-package-bin": ["npm-normalize-package-bin@6.0.0", "", {}, "sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw=="], - "npm-run-all2": ["npm-run-all2@9.0.3", "", { "dependencies": { "ansi-styles": "^7.0.0", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "picomatch": "^4.0.2", "pidtree": "^1.0.0", "read-package-json-fast": "^6.0.0", "shell-quote": "^1.8.4", "which": "^7.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-BQAEdU1PtYc48qYRdghW2BVTQT3VqWCoFQmO87NlM1h1PYwMCKQpUWaNyB20V26caNzFsXQDfyOdznLlHCih6g=="], + "npm-run-all2": ["npm-run-all2@9.0.4", "", { "dependencies": { "ansi-styles": "^7.0.0", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "picomatch": "^4.0.2", "pidtree": "^1.0.0", "read-package-json-fast": "^6.0.0", "shell-quote": "^1.12.0", "which": "^7.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-GuGRF8MddEKavyUzjy0k2z2pA76/N+eT8lHlypx0/AhShLKD2FOerQxHhpFhcqnQmAqCkunUCYlf6Z9RKh/4zg=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "oxc-parser": ["oxc-parser@0.154.0", "", { "dependencies": { "@oxc-project/types": "^0.154.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.154.0", "@oxc-parser/binding-android-arm64": "0.154.0", "@oxc-parser/binding-darwin-arm64": "0.154.0", "@oxc-parser/binding-darwin-x64": "0.154.0", "@oxc-parser/binding-freebsd-x64": "0.154.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.154.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.154.0", "@oxc-parser/binding-linux-arm64-gnu": "0.154.0", "@oxc-parser/binding-linux-arm64-musl": "0.154.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.154.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.154.0", "@oxc-parser/binding-linux-riscv64-musl": "0.154.0", "@oxc-parser/binding-linux-s390x-gnu": "0.154.0", "@oxc-parser/binding-linux-x64-gnu": "0.154.0", "@oxc-parser/binding-linux-x64-musl": "0.154.0", "@oxc-parser/binding-openharmony-arm64": "0.154.0", "@oxc-parser/binding-win32-arm64-msvc": "0.154.0", "@oxc-parser/binding-win32-ia32-msvc": "0.154.0", "@oxc-parser/binding-win32-x64-msvc": "0.154.0" } }, "sha512-Gr/6fkjZwi937c4Tsz8y0iO4U0FLPkZI6JSVucLAIvQyqh379px7RS8ORIc0HOORbFjXZ6zyqWLl53qxhSeofw=="], + "oxc-parser": ["oxc-parser@0.155.0", "", { "dependencies": { "@oxc-project/types": "^0.155.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.155.0", "@oxc-parser/binding-android-arm64": "0.155.0", "@oxc-parser/binding-darwin-arm64": "0.155.0", "@oxc-parser/binding-darwin-x64": "0.155.0", "@oxc-parser/binding-freebsd-x64": "0.155.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.155.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.155.0", "@oxc-parser/binding-linux-arm64-gnu": "0.155.0", "@oxc-parser/binding-linux-arm64-musl": "0.155.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.155.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.155.0", "@oxc-parser/binding-linux-riscv64-musl": "0.155.0", "@oxc-parser/binding-linux-s390x-gnu": "0.155.0", "@oxc-parser/binding-linux-x64-gnu": "0.155.0", "@oxc-parser/binding-linux-x64-musl": "0.155.0", "@oxc-parser/binding-openharmony-arm64": "0.155.0", "@oxc-parser/binding-win32-arm64-msvc": "0.155.0", "@oxc-parser/binding-win32-ia32-msvc": "0.155.0", "@oxc-parser/binding-win32-x64-msvc": "0.155.0" } }, "sha512-C/ReSf1JUFukK/zvk6jhcNibSssvas5+ktIzK7fbKVd3CedWe59C3QwHEOrEZj/4Rt9+xg0Jeis/Jv1F/KM1mw=="], "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], @@ -588,11 +588,11 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.71.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.71.2", "@typescript-eslint/parser": "8.71.2", "@typescript-eslint/typescript-estree": "8.71.2", "@typescript-eslint/utils": "8.71.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw=="], + "typescript-eslint": ["typescript-eslint@8.72.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.72.0", "@typescript-eslint/parser": "8.72.0", "@typescript-eslint/typescript-estree": "8.72.0", "@typescript-eslint/utils": "8.72.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A=="], "uint8array-extras": ["uint8array-extras@1.6.0", "", {}, "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q=="], - "unbash": ["unbash@6.0.0", "", {}, "sha512-vD5CvvFLVQDhXdNyNEPPa/tHPCK3HLySddOine+OZLDYIH1cMImGNgnw9+NkizToaPsFsQlwiyPGMiK+uwtYVw=="], + "unbash": ["unbash@6.0.1", "", {}, "sha512-0db9eS5iqybux41zLkX69nk90KTci7F7eZ+gXzEG/Q7E0tAqEBhRh5fkZvk1N7jT7QkGWqq34i8SFGPFEe1V8A=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -630,17 +630,13 @@ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" }, "bundled": true }, "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.12", "", {}, "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg=="], - - "bun-types/@types/node": ["@types/node@26.6.5", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="], - - "bun-types/undici-types": ["undici-types@8.12.2", "", {}, "sha512-VoMeD2O0KdHhcn+oPqOrpivN+7aDOklVSwOLD7JNjbzV1CboAFek9IFOXnreoufTUtAsCsOK0iETiERPNR/fDA=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.14", "", {}, "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ=="], "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -658,8 +654,6 @@ "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "bun-types/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "tsconfig-paths-webpack-plugin/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], From 710b842e2d18169d3ce532dfca1cf5e47215854d Mon Sep 17 00:00:00 2001 From: ZaSourceCode Date: Mon, 26 Oct 2026 22:21:31 +0300 Subject: [PATCH 14/14] fix: keep long auto-delete intervals from overflowing setTimeout (#672) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor Agent Co-authored-by: Emrik Östling Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Signed-off-by: YodHeVauHe --- src/helpers/scheduleDelayed.ts | 48 +++++++++ src/index.tsx | 12 ++- tests/helpers/scheduleDelayed.test.ts | 144 ++++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 src/helpers/scheduleDelayed.ts create mode 100644 tests/helpers/scheduleDelayed.test.ts diff --git a/src/helpers/scheduleDelayed.ts b/src/helpers/scheduleDelayed.ts new file mode 100644 index 00000000..789bd7d1 --- /dev/null +++ b/src/helpers/scheduleDelayed.ts @@ -0,0 +1,48 @@ +/** SetTimeout delays above this overflow a signed 32-bit integer and fire immediately. */ +const MAX_TIMEOUT_DELAY_MS = 2 ** 31 - 1; + +type ArmTimer = (callback: () => void, delayMs: number) => ReturnType; + +const armNativeTimer: ArmTimer = (callback, delayMs) => setTimeout(callback, delayMs); + +export type ScheduledTask = { + /** Stops the task. Works across chunk boundaries, unlike a raw timer handle. */ + cancel: () => void; +}; + +export function scheduleDelayed( + delayMs: number, + task: () => void, + armTimer: ArmTimer = armNativeTimer, +): ScheduledTask { + let handle: ReturnType | undefined; + let cancelled = false; + + const arm = (remainingMs: number) => { + const chunkMs = Math.min(remainingMs, MAX_TIMEOUT_DELAY_MS); + handle = armTimer(() => { + if (cancelled) { + return; + } + + const restMs = remainingMs - chunkMs; + if (restMs > 0) { + arm(restMs); + return; + } + + task(); + }, chunkMs); + }; + + arm(delayMs); + + return { + cancel: () => { + cancelled = true; + if (handle !== undefined) { + clearTimeout(handle); + } + }, + }; +} diff --git a/src/index.tsx b/src/index.tsx index 995934f2..3fcff561 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,6 +8,7 @@ import db from "./db/db"; import { Jobs } from "./db/types"; import { AUTO_DELETE_EVERY_N_HOURS, WEBROOT } from "./helpers/env"; import "./helpers/printVersions"; +import { scheduleDelayed, ScheduledTask } from "./helpers/scheduleDelayed"; import { chooseConverter } from "./pages/chooseConverter"; import { convert } from "./pages/convert"; import { deleteFile } from "./pages/deleteFile"; @@ -75,12 +76,15 @@ app.listen(process.env.PORT || 3000); console.log(`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}${WEBROOT}`); -let clearJobsTimer: ReturnType | undefined; +const MILLISECONDS_PER_HOUR = 60 * 60 * 1000; +const autoDeleteIntervalMs = AUTO_DELETE_EVERY_N_HOURS * MILLISECONDS_PER_HOUR; +let clearJobsTimer: ScheduledTask; + const clearJobs = () => { const jobs = db .query("SELECT * FROM jobs WHERE date_created < ?") .as(Jobs) - .all(new Date(Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000).toISOString()); + .all(new Date(Date.now() - autoDeleteIntervalMs).toISOString()); for (const job of jobs) { // delete the directories @@ -97,7 +101,7 @@ const clearJobs = () => { db.query("DELETE FROM jobs WHERE id = ?").run(job.id); } - clearJobsTimer = setTimeout(clearJobs, AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000); + clearJobsTimer = scheduleDelayed(autoDeleteIntervalMs, clearJobs); }; if (AUTO_DELETE_EVERY_N_HOURS > 0) { @@ -109,7 +113,7 @@ const handleShutdown = async () => { if (isShuttingDown) return; isShuttingDown = true; if (clearJobsTimer) { - clearTimeout(clearJobsTimer); + clearJobsTimer.cancel(); } try { await app.stop(true); diff --git a/tests/helpers/scheduleDelayed.test.ts b/tests/helpers/scheduleDelayed.test.ts new file mode 100644 index 00000000..2a2c532a --- /dev/null +++ b/tests/helpers/scheduleDelayed.test.ts @@ -0,0 +1,144 @@ +import { expect, test } from "bun:test"; + +import { scheduleDelayed } from "../../src/helpers/scheduleDelayed"; + +/** Largest delay setTimeout accepts before a 32-bit overflow. */ +const MAX_TIMEOUT_DELAY_MS = 2 ** 31 - 1; +/** Delay reported in the TimeoutOverflowWarning: 30 days in milliseconds. */ +const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; + +type ArmedTimer = { + delayMs: number; + fire: () => void; +}; + +function captureTimers(): { + armed: ArmedTimer[]; + armTimer: (callback: () => void, delayMs: number) => ReturnType; +} { + const armed: ArmedTimer[] = []; + const armTimer = (callback: () => void, delayMs: number) => { + armed.push({ delayMs, fire: callback }); + return 0 as unknown as ReturnType; + }; + return { armed, armTimer }; +} + +test("runs a one day delay as a single timer", () => { + const { armed, armTimer } = captureTimers(); + let runs = 0; + const oneDayMs = 24 * 60 * 60 * 1000; + + scheduleDelayed( + oneDayMs, + () => { + runs += 1; + }, + armTimer, + ); + + expect(armed).toHaveLength(1); + const timer = armed.shift(); + expect(timer?.delayMs).toBe(oneDayMs); + expect(runs).toBe(0); + + timer?.fire(); + expect(runs).toBe(1); + expect(armed).toHaveLength(0); +}); + +test("runs a 30 day delay in timer-sized chunks that add up to 30 days", () => { + expect(THIRTY_DAYS_MS).toBe(2_592_000_000); + + const { armed, armTimer } = captureTimers(); + let runs = 0; + scheduleDelayed( + THIRTY_DAYS_MS, + () => { + runs += 1; + }, + armTimer, + ); + + let elapsedMs = 0; + let chunks = 0; + while (runs === 0) { + chunks += 1; + expect(chunks).toBeLessThanOrEqual(3); + + const next = armed.shift(); + if (!next) { + throw new Error("timer chain ended before the task ran"); + } + + expect(next.delayMs).toBeGreaterThan(0); + expect(next.delayMs).toBeLessThanOrEqual(MAX_TIMEOUT_DELAY_MS); + elapsedMs += next.delayMs; + next.fire(); + } + + expect(runs).toBe(1); + expect(elapsedMs).toBe(THIRTY_DAYS_MS); + expect(armed).toHaveLength(0); +}); + +test("cancelling before the first chunk fires prevents the task", () => { + const { armed, armTimer } = captureTimers(); + let runs = 0; + const scheduled = scheduleDelayed( + THIRTY_DAYS_MS, + () => { + runs += 1; + }, + armTimer, + ); + + scheduled.cancel(); + armed.shift()?.fire(); + expect(runs).toBe(0); + expect(armed).toHaveLength(0); +}); + +test("cancelling after a chunk fired still stops the remaining chain", () => { + const { armed, armTimer } = captureTimers(); + let runs = 0; + const scheduled = scheduleDelayed( + THIRTY_DAYS_MS, + () => { + runs += 1; + }, + armTimer, + ); + + armed.shift()?.fire(); + expect(armed).toHaveLength(1); + expect(runs).toBe(0); + + scheduled.cancel(); + armed.shift()?.fire(); + expect(runs).toBe(0); + expect(armed).toHaveLength(0); +}); + +test("does not emit a timeout overflow warning for a 30 day delay", async () => { + const warnings: string[] = []; + const onWarning = (warning: Error) => { + warnings.push(warning.message); + }; + process.on("warning", onWarning); + + let ran = false; + const scheduled = scheduleDelayed(THIRTY_DAYS_MS, () => { + ran = true; + }); + + try { + await new Promise((resolve) => setTimeout(resolve, 40)); + expect(ran).toBe(false); + expect(warnings.join("\n")).not.toContain("TimeoutOverflowWarning"); + expect(warnings.join("\n")).not.toContain("2592000000"); + } finally { + scheduled.cancel(); + process.off("warning", onWarning); + } +});