diff --git a/bin/quak.ts b/bin/quak.ts index c89092e..dd3c087 100644 --- a/bin/quak.ts +++ b/bin/quak.ts @@ -9,6 +9,10 @@ import envPaths from "env-paths"; import { Client, type ClientSnapshot } from "../src/client.js"; import { init } from "../src/crypto/index.js"; import { runBackup } from "../src/backup.js"; +import { + listMissingThumbnails, + fixMissingThumbnails, +} from "../src/thumbnails.js"; const paths = envPaths("quak", { suffix: "" }); const sessionPath = join(paths.data, "session.json"); @@ -366,4 +370,90 @@ program process.exit(result.failed > 0 ? 1 : 0); }); +const helper = program + .command("helper") + .description("Maintenance and repair utilities"); + +helper + .command("list-missing-thumbnails") + .description("List files whose thumbnails are missing or empty") + .option("--json", "Output as JSON array") + .action(async (opts: { json?: boolean }) => { + await init(); + const client = requireSession(); + const missing = await listMissingThumbnails(client, (msg) => { + if (!opts.json) stderr.write(msg + "\n"); + }); + + if (opts.json) { + stdout.write(JSON.stringify(missing, null, 2) + "\n"); + } else { + if (missing.length === 0) { + stderr.write("No missing thumbnails found.\n"); + } else { + stderr.write( + `\n${missing.length} file(s) with missing thumbnails:\n`, + ); + for (const m of missing) { + stdout.write( + `${m.fileID}\t${m.title}\t${m.collection}\t${m.reason}\n`, + ); + } + } + } + }); + +helper + .command("fix-missing-thumbnails") + .description( + "Generate and upload thumbnails for files that are missing them", + ) + .option( + "--file ", + "Specific file IDs to fix (default: fix all missing)", + ) + .option("--json", "Output as JSON") + .action(async (opts: { file?: string[]; json?: boolean }) => { + await init(); + const client = requireSession(); + + let fileIDs: number[]; + if (opts.file && opts.file.length > 0) { + fileIDs = opts.file.map(Number).filter(Number.isFinite); + } else { + stderr.write("Scanning for missing thumbnails...\n"); + const missing = await listMissingThumbnails(client, (msg) => { + if (!opts.json) stderr.write(msg + "\n"); + }); + fileIDs = missing.map((m) => m.fileID); + if (fileIDs.length === 0) { + stderr.write("No missing thumbnails found.\n"); + return; + } + stderr.write(`Found ${fileIDs.length} file(s) to fix.\n`); + } + + const results = await fixMissingThumbnails(client, fileIDs, (msg) => { + if (!opts.json) stderr.write(msg + "\n"); + }); + + if (opts.json) { + stdout.write(JSON.stringify(results, null, 2) + "\n"); + } else { + const ok = results.filter((r) => r.success).length; + const fail = results.filter((r) => !r.success).length; + stderr.write(`\n--- Done ---\n`); + stderr.write(` Fixed: ${ok}\n`); + stderr.write(` Failed: ${fail}\n`); + if (fail > 0) { + stderr.write("\nFailed files:\n"); + for (const r of results.filter((r) => !r.success)) { + stderr.write(` ${r.fileID}\t${r.title}\t${r.error}\n`); + } + } + } + + process.exit(results.some((r) => !r.success) ? 1 : 0); + }); + program.parse(); diff --git a/package.json b/package.json index c967ef3..836f04d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@eslint/js": "9.38.0", "@types/libsodium-wrappers-sumo": "0.8.2", "@types/node": "22.18.13", + "@types/sharp": "0.32.0", "eslint": "9.38.0", "prettier": "3.8.1", "typescript": "5.9.3", @@ -41,6 +42,7 @@ "commander": "14.0.3", "env-paths": "4.0.0", "fast-srp-hap": "2.0.4", - "libsodium-wrappers-sumo": "0.8.4" + "libsodium-wrappers-sumo": "0.8.4", + "sharp": "0.34.5" } } diff --git a/src/api/client.ts b/src/api/client.ts index fc5e877..32b1ae7 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -162,6 +162,52 @@ export class ApiClient { return resp.body; } + async getUploadURL( + contentLength: number, + contentMD5: string, + ): Promise<{ objectKey: string; url: string }> { + return this.postJSON("/files/upload-url", { + contentLength, + contentMD5, + }); + } + + async putFile(presignedURL: string, data: Uint8Array): Promise { + const resp = await this._fetch(presignedURL, { + method: "PUT", + headers: { + "Content-Type": "application/octet-stream", + "Content-Length": String(data.length), + }, + body: data, + }); + if (!resp.ok) { + throw new Error(`PUT to presigned URL failed: HTTP ${resp.status}`); + } + } + + async putJSON(path: string, body: unknown): Promise { + const url = `${this.apiOrigin}${path}`; + const resp = await this._fetch(url, { + method: "PUT", + headers: this.headers({ "Content-Type": "application/json" }), + body: JSON.stringify(body), + }); + await this.throwIfError(resp); + return (await resp.json()) as T; + } + + async updateThumbnail( + fileID: number, + objectKey: string, + decryptionHeader: string, + ): Promise { + await this.putJSON("/files/thumbnail", { + fileID, + thumbnail: { objectKey, decryptionHeader }, + }); + } + async getThumbnailStream( fileID: number, ): Promise> { diff --git a/src/client.ts b/src/client.ts index 8223f87..3f145d7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -119,6 +119,11 @@ export class Client { ); } + getApiClient(): ApiClient { + this.assertLoggedIn(); + return this.api; + } + private assertLoggedIn(): void { if (this.loggedOut) throw new Error("Client has been logged out"); } diff --git a/src/crypto/index.ts b/src/crypto/index.ts index a8c6658..ef129f7 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -9,6 +9,7 @@ export { deriveKEK, deriveLoginSubkey } from "./kdf.js"; export { decryptBox, decryptSealed } from "./box.js"; export { decryptBlob, + encryptBlob, initStreamPull, pullStreamChunk, STREAM_CHUNK_OVERHEAD, diff --git a/src/crypto/stream.ts b/src/crypto/stream.ts index f0c4757..f255c59 100644 --- a/src/crypto/stream.ts +++ b/src/crypto/stream.ts @@ -8,6 +8,23 @@ export const STREAM_CHUNK_SIZE = 4 * 1024 * 1024; // 16 bytes of Poly1305 tag plus 1 byte of secretstream tag. export const STREAM_CHUNK_OVERHEAD = 17; +// Encrypt a small blob as a single secretstream chunk with TAG_FINAL. +// Returns the header and ciphertext. Used for encrypting thumbnails +// and metadata before upload. +export const encryptBlob = ( + plaintext: Uint8Array, + key: Uint8Array, +): { header: Uint8Array; ciphertext: Uint8Array } => { + const push = sodium.crypto_secretstream_xchacha20poly1305_init_push(key); + const ciphertext = sodium.crypto_secretstream_xchacha20poly1305_push( + push.state, + plaintext, + null, + sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL, + ); + return { header: push.header, ciphertext }; +}; + // Opaque handle to libsodium's secretstream pull state. Threaded through // successive pullStreamChunk calls. export type StreamPullState = sodium.StateAddress; diff --git a/src/thumbnails.ts b/src/thumbnails.ts new file mode 100644 index 0000000..efdc84d --- /dev/null +++ b/src/thumbnails.ts @@ -0,0 +1,183 @@ +import { createHash } from "node:crypto"; +import sharp from "sharp"; +import type { Client } from "./client.js"; +import { encryptBlob, toBase64 } from "./crypto/index.js"; +import { downloadFile } from "./download/index.js"; +import type { EnteFile } from "./model/types.js"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +const THUMB_MAX_DIMENSION = 720; +const THUMB_JPEG_QUALITY = 50; + +export interface MissingThumbnailInfo { + fileID: number; + title: string; + collection: string; + reason: string; +} + +export interface ThumbnailFixResult { + fileID: number; + title: string; + collection: string; + success: boolean; + error?: string; +} + +export type ProgressCallback = (message: string) => void; + +export const listMissingThumbnails = async ( + client: Client, + onProgress?: ProgressCallback, +): Promise => { + const log = onProgress ?? (() => {}); + const missing: MissingThumbnailInfo[] = []; + const seen = new Set(); + + const collections = await client.listCollections(); + for (const col of collections) { + log(`[${col.name}] Checking thumbnails...`); + const files = await client.listFiles(col.id, col.key); + for (const file of files) { + if (seen.has(file.id)) continue; + seen.add(file.id); + try { + const api = client.getApiClient(); + const stream = await api.getThumbnailStream(file.id); + const reader = stream.getReader(); + let totalBytes = 0; + for (;;) { + const { done, value } = await reader.read(); + if (value) totalBytes += value.length; + if (done) break; + } + if (totalBytes === 0) { + missing.push({ + fileID: file.id, + title: file.metadata.title, + collection: col.name, + reason: "empty thumbnail (0 bytes)", + }); + } + } catch { + missing.push({ + fileID: file.id, + title: file.metadata.title, + collection: col.name, + reason: "thumbnail fetch failed", + }); + } + } + } + return missing; +}; + +const generateThumbnail = async (originalPath: string): Promise => { + const result = await sharp(originalPath) + .rotate() + .resize(THUMB_MAX_DIMENSION, THUMB_MAX_DIMENSION, { + fit: "inside", + withoutEnlargement: true, + }) + .jpeg({ quality: THUMB_JPEG_QUALITY }) + .toBuffer(); + return new Uint8Array(result); +}; + +export const fixMissingThumbnails = async ( + client: Client, + fileIDs: number[], + onProgress?: ProgressCallback, +): Promise => { + const log = onProgress ?? (() => {}); + const results: ThumbnailFixResult[] = []; + const api = client.getApiClient(); + + const collections = await client.listCollections(); + const fileMap = new Map< + number, + { file: EnteFile; collectionName: string } + >(); + + for (const col of collections) { + const files = await client.listFiles(col.id, col.key); + for (const file of files) { + if (fileIDs.includes(file.id) && !fileMap.has(file.id)) { + fileMap.set(file.id, { + file, + collectionName: col.name, + }); + } + } + } + + for (const fileID of fileIDs) { + const entry = fileMap.get(fileID); + if (!entry) { + results.push({ + fileID, + title: "unknown", + collection: "unknown", + success: false, + error: "file not found in any collection", + }); + continue; + } + + const { file, collectionName } = entry; + const tmpDir = mkdtempSync(join(tmpdir(), "quak-thumb-")); + + try { + log( + `[${collectionName}] Downloading ${file.metadata.title} for thumbnail generation...`, + ); + const origPath = join(tmpDir, "original"); + await downloadFile(api, file, origPath); + + log( + `[${collectionName}] Generating thumbnail for ${file.metadata.title}...`, + ); + const thumbJpeg = await generateThumbnail(origPath); + + log( + `[${collectionName}] Encrypting and uploading thumbnail (${thumbJpeg.length} bytes)...`, + ); + const { header, ciphertext } = encryptBlob(thumbJpeg, file.key); + + const md5 = createHash("md5").update(ciphertext).digest("base64"); + const { objectKey, url } = await api.getUploadURL( + ciphertext.length, + md5, + ); + await api.putFile(url, ciphertext); + await api.updateThumbnail(file.id, objectKey, toBase64(header)); + + log( + `[${collectionName}] Thumbnail uploaded for ${file.metadata.title}`, + ); + results.push({ + fileID, + title: file.metadata.title, + collection: collectionName, + success: true, + }); + } catch (err) { + log( + `[${collectionName}] FAILED ${file.metadata.title}: ${err instanceof Error ? err.message : err}`, + ); + results.push({ + fileID, + title: file.metadata.title, + collection: collectionName, + success: false, + error: err instanceof Error ? err.message : String(err), + }); + } finally { + rmSync(tmpDir, { recursive: true, force: true }); + } + } + + return results; +}; diff --git a/yarn.lock b/yarn.lock index 6398ef0..f16f660 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@emnapi/runtime@^1.7.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" + integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== + dependencies: + tslib "^2.4.0" + "@esbuild/aix-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" @@ -223,6 +230,153 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== +"@img/colour@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.1.0.tgz#b0c2c2fa661adf75effd6b4964497cd80010bb9d" + integrity sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ== + +"@img/sharp-darwin-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86" + integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.2.4" + +"@img/sharp-darwin-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b" + integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.2.4" + +"@img/sharp-libvips-darwin-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43" + integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g== + +"@img/sharp-libvips-darwin-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc" + integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg== + +"@img/sharp-libvips-linux-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318" + integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw== + +"@img/sharp-libvips-linux-arm@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d" + integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A== + +"@img/sharp-libvips-linux-ppc64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7" + integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA== + +"@img/sharp-libvips-linux-riscv64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de" + integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA== + +"@img/sharp-libvips-linux-s390x@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec" + integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ== + +"@img/sharp-libvips-linux-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce" + integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw== + +"@img/sharp-libvips-linuxmusl-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06" + integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw== + +"@img/sharp-libvips-linuxmusl-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75" + integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg== + +"@img/sharp-linux-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc" + integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.2.4" + +"@img/sharp-linux-arm@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d" + integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.2.4" + +"@img/sharp-linux-ppc64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813" + integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA== + optionalDependencies: + "@img/sharp-libvips-linux-ppc64" "1.2.4" + +"@img/sharp-linux-riscv64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60" + integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw== + optionalDependencies: + "@img/sharp-libvips-linux-riscv64" "1.2.4" + +"@img/sharp-linux-s390x@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7" + integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.2.4" + +"@img/sharp-linux-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8" + integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.2.4" + +"@img/sharp-linuxmusl-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086" + integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + +"@img/sharp-linuxmusl-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f" + integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + +"@img/sharp-wasm32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0" + integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw== + dependencies: + "@emnapi/runtime" "^1.7.0" + +"@img/sharp-win32-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a" + integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g== + +"@img/sharp-win32-ia32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de" + integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg== + +"@img/sharp-win32-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8" + integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw== + "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" @@ -403,6 +557,13 @@ dependencies: undici-types "~6.21.0" +"@types/sharp@^0.32.0": + version "0.32.0" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.32.0.tgz#fc3ac6df6b456319bae807c3d24efdc6631cdd6f" + integrity sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw== + dependencies: + sharp "*" + "@typescript-eslint/eslint-plugin@8.46.2": version "8.46.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz#dc4ab93ee3d7e6c8e38820a0d6c7c93c7183e2dc" @@ -716,6 +877,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +detect-libc@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + env-paths@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-4.0.0.tgz#d0bb1f84a81d2542581bf7b7e8085d0683b39097" @@ -1280,11 +1446,45 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -semver@^7.6.0: +semver@^7.6.0, semver@^7.7.3: version "7.8.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.0.tgz#ed0661039fcbcda2ce71f01fa6adbefaa77040df" integrity sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA== +sharp@*, sharp@^0.34.5: + version "0.34.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0" + integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg== + dependencies: + "@img/colour" "^1.0.0" + detect-libc "^2.1.2" + semver "^7.7.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.34.5" + "@img/sharp-darwin-x64" "0.34.5" + "@img/sharp-libvips-darwin-arm64" "1.2.4" + "@img/sharp-libvips-darwin-x64" "1.2.4" + "@img/sharp-libvips-linux-arm" "1.2.4" + "@img/sharp-libvips-linux-arm64" "1.2.4" + "@img/sharp-libvips-linux-ppc64" "1.2.4" + "@img/sharp-libvips-linux-riscv64" "1.2.4" + "@img/sharp-libvips-linux-s390x" "1.2.4" + "@img/sharp-libvips-linux-x64" "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + "@img/sharp-linux-arm" "0.34.5" + "@img/sharp-linux-arm64" "0.34.5" + "@img/sharp-linux-ppc64" "0.34.5" + "@img/sharp-linux-riscv64" "0.34.5" + "@img/sharp-linux-s390x" "0.34.5" + "@img/sharp-linux-x64" "0.34.5" + "@img/sharp-linuxmusl-arm64" "0.34.5" + "@img/sharp-linuxmusl-x64" "0.34.5" + "@img/sharp-wasm32" "0.34.5" + "@img/sharp-win32-arm64" "0.34.5" + "@img/sharp-win32-ia32" "0.34.5" + "@img/sharp-win32-x64" "0.34.5" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1366,6 +1566,11 @@ ts-api-utils@^2.1.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== +tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"