fix: image transforms via Sharp, model photos crash, video duration
All checks were successful
Build and Push Backend Image / build (push) Successful in 46s
Build and Push Frontend Image / build (push) Successful in 5m7s

- Backend: add Sharp image transform endpoint (/assets/:id?transform=X)
  with presets: mini(64), thumbnail(200), preview(480), medium(960), banner(1280)
  Transformed images are cached as webp next to originals
- Frontend: fix model photos crash (p.directus_files_id → p)
- Frontend: fix model banner URL (data.model.banner.id → data.model.banner)
- Frontend: fix video duration display (video.movie.duration → video.movie_file?.duration)
  across models/[slug], videos, videos/[slug], and home pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 20:56:33 +01:00
parent 273aa42510
commit 05cb6a66e3
8 changed files with 321 additions and 18 deletions

View File

@@ -18,8 +18,8 @@ COPY packages/buttplug/package.json ./packages/buttplug/package.json
RUN pnpm install --frozen-lockfile --filter @sexy.pivoine.art/backend --ignore-scripts RUN pnpm install --frozen-lockfile --filter @sexy.pivoine.art/backend --ignore-scripts
# Only rebuild the native argon2 binding — not buttplug or anything else # Rebuild native bindings (argon2, sharp)
RUN pnpm rebuild argon2 RUN pnpm rebuild argon2 sharp
COPY packages/backend ./packages/backend COPY packages/backend ./packages/backend
@@ -27,7 +27,7 @@ RUN pnpm --filter @sexy.pivoine.art/backend build
RUN CI=true pnpm install --frozen-lockfile --filter @sexy.pivoine.art/backend --prod --ignore-scripts RUN CI=true pnpm install --frozen-lockfile --filter @sexy.pivoine.art/backend --prod --ignore-scripts
RUN pnpm rebuild argon2 RUN pnpm rebuild argon2 sharp
# ============================================================================ # ============================================================================
# Runner stage # Runner stage

View File

@@ -31,6 +31,7 @@
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
"pg": "^8.16.0", "pg": "^8.16.0",
"sharp": "^0.33.5",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
@@ -38,6 +39,7 @@
"@types/fluent-ffmpeg": "^2.1.27", "@types/fluent-ffmpeg": "^2.1.27",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/pg": "^8.15.4", "@types/pg": "^8.15.4",
"@types/sharp": "^0.32.0",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"drizzle-kit": "^0.31.1", "drizzle-kit": "^0.31.1",
"tsx": "^4.19.4", "tsx": "^4.19.4",

View File

@@ -7,6 +7,8 @@ import { createYoga } from "graphql-yoga";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { files } from "./db/schema/index"; import { files } from "./db/schema/index";
import path from "path"; import path from "path";
import { existsSync } from "fs";
import sharp from "sharp";
import { schema } from "./graphql/index"; import { schema } from "./graphql/index";
import { buildContext } from "./graphql/context"; import { buildContext } from "./graphql/context";
import { db } from "./db/connection"; import { db } from "./db/connection";
@@ -67,10 +69,20 @@ async function main() {
yoga.handleNodeRequestAndResponse(req, reply, { req, reply, db, redis }), yoga.handleNodeRequestAndResponse(req, reply, { req, reply, db, redis }),
}); });
// Serve uploaded files: GET /assets/:id // Transform presets: width x height (height optional = keep aspect ratio)
const TRANSFORMS: Record<string, { width: number; height?: number }> = {
mini: { width: 64, height: 64 },
thumbnail: { width: 200, height: 200 },
preview: { width: 480, height: 270 },
medium: { width: 960 },
banner: { width: 1280, height: 400 },
};
// Serve uploaded files: GET /assets/:id?transform=<preset>
// Files are stored as <UPLOAD_DIR>/<id>/<filename> — look up filename in DB // Files are stored as <UPLOAD_DIR>/<id>/<filename> — look up filename in DB
fastify.get("/assets/:id", async (request, reply) => { fastify.get("/assets/:id", async (request, reply) => {
const { id } = request.params as { id: string }; const { id } = request.params as { id: string };
const { transform } = request.query as { transform?: string };
const result = await db const result = await db
.select({ filename: files.filename, mime_type: files.mime_type }) .select({ filename: files.filename, mime_type: files.mime_type })
@@ -81,8 +93,23 @@ async function main() {
if (!result[0]) return reply.status(404).send({ error: "File not found" }); if (!result[0]) return reply.status(404).send({ error: "File not found" });
const { filename, mime_type } = result[0]; const { filename, mime_type } = result[0];
reply.header("Content-Type", mime_type);
reply.header("Cache-Control", "public, max-age=31536000, immutable"); reply.header("Cache-Control", "public, max-age=31536000, immutable");
const preset = transform ? TRANSFORMS[transform] : null;
if (preset && mime_type?.startsWith("image/")) {
const cacheFile = path.join(UPLOAD_DIR, id, `${transform}.webp`);
if (!existsSync(cacheFile)) {
const originalPath = path.join(UPLOAD_DIR, id, filename);
await sharp(originalPath)
.resize({ width: preset.width, height: preset.height, fit: "cover", withoutEnlargement: true })
.webp({ quality: 85 })
.toFile(cacheFile);
}
reply.header("Content-Type", "image/webp");
return reply.sendFile(path.join(id, `${transform}.webp`));
}
reply.header("Content-Type", mime_type);
return reply.sendFile(path.join(id, filename)); return reply.sendFile(path.join(id, filename));
}); });

View File

@@ -144,7 +144,7 @@ const { data } = $props();
<div <div
class="absolute bottom-2 left-2 text-white text-sm font-medium" class="absolute bottom-2 left-2 text-white text-sm font-medium"
> >
{formatVideoDuration(video.movie.duration)} {#if video.movie_file?.duration}{formatVideoDuration(video.movie_file.duration)}{/if}
</div> </div>
<!-- <div <!-- <div
class="absolute top-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded-full" class="absolute top-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded-full"

View File

@@ -22,9 +22,9 @@ const { data } = $props();
let images = $derived( let images = $derived(
data.model.photos.map((p) => ({ data.model.photos.map((p) => ({
...p.directus_files_id, ...p,
url: getAssetUrl(p.directus_files_id.id), url: getAssetUrl(p.id),
thumbnail: getAssetUrl(p.directus_files_id.id, "thumbnail"), thumbnail: getAssetUrl(p.id, "thumbnail"),
})), })),
); );
@@ -52,7 +52,7 @@ let totalPlays = $derived(
<div class="relative h-64 md:h-80 overflow-hidden bg-gradient-to-br from-primary to-accent"> <div class="relative h-64 md:h-80 overflow-hidden bg-gradient-to-br from-primary to-accent">
{#if data.model.banner} {#if data.model.banner}
<img <img
src={getAssetUrl(data.model.banner.id, "banner")} src={getAssetUrl(data.model.banner, "banner")}
alt={$_(data.model.artist_name)} alt={$_(data.model.artist_name)}
class="w-full h-full object-cover" class="w-full h-full object-cover"
/> />
@@ -243,7 +243,7 @@ let totalPlays = $derived(
<div <div
class="absolute bottom-2 left-2 text-white text-sm font-medium" class="absolute bottom-2 left-2 text-white text-sm font-medium"
> >
{formatVideoDuration(video.movie.duration)} {#if video.movie_file?.duration}{formatVideoDuration(video.movie_file.duration)}{/if}
</div> </div>
<!-- <div <!-- <div
class="absolute top-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded-full" class="absolute top-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded-full"

View File

@@ -34,11 +34,11 @@ const filteredVideos = $derived(() => {
const matchesCategory = categoryFilter === "all"; const matchesCategory = categoryFilter === "all";
const matchesDuration = const matchesDuration =
durationFilter === "all" || durationFilter === "all" ||
(durationFilter === "short" && video.movie.duration < 10 * 60) || (durationFilter === "short" && (video.movie_file?.duration ?? 0) < 10 * 60) ||
(durationFilter === "medium" && (durationFilter === "medium" &&
video.movie.duration >= 10 * 60 && (video.movie_file?.duration ?? 0) >= 10 * 60 &&
video.movie.duration < 20 * 60) || (video.movie_file?.duration ?? 0) < 20 * 60) ||
(durationFilter === "long" && video.movie.duration >= 20 * 60); (durationFilter === "long" && (video.movie_file?.duration ?? 0) >= 20 * 60);
return matchesSearch && matchesCategory && matchesDuration; return matchesSearch && matchesCategory && matchesDuration;
}) })
.sort((a, b) => { .sort((a, b) => {
@@ -50,7 +50,7 @@ const filteredVideos = $derived(() => {
return (b.likes_count || 0) - (a.likes_count || 0); return (b.likes_count || 0) - (a.likes_count || 0);
if (sortBy === "most_played") if (sortBy === "most_played")
return (b.plays_count || 0) - (a.plays_count || 0); return (b.plays_count || 0) - (a.plays_count || 0);
if (sortBy === "duration") return b.movie.duration - a.movie.duration; if (sortBy === "duration") return (b.movie_file?.duration ?? 0) - (a.movie_file?.duration ?? 0);
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
}); });
}); });
@@ -220,7 +220,7 @@ const filteredVideos = $derived(() => {
<div <div
class="absolute bottom-3 left-3 bg-black/70 text-white text-sm px-2 py-1 rounded font-medium" class="absolute bottom-3 left-3 bg-black/70 text-white text-sm px-2 py-1 rounded font-medium"
> >
{formatVideoDuration(video.movie.duration)} {#if video.movie_file?.duration}{formatVideoDuration(video.movie_file.duration)}{/if}
</div> </div>
<!-- Premium Badge --> <!-- Premium Badge -->

View File

@@ -221,7 +221,7 @@ let showPlayer = $state(false);
<div <div
class="absolute bottom-4 left-4 bg-black/70 text-white px-3 py-1 rounded font-medium" class="absolute bottom-4 left-4 bg-black/70 text-white px-3 py-1 rounded font-medium"
> >
{formatVideoDuration(data.video.movie.duration)} {#if data.video.movie_file?.duration}{formatVideoDuration(data.video.movie_file.duration)}{/if}
</div> </div>
{#if data.video.premium} {#if data.video.premium}
<div <div

274
pnpm-lock.yaml generated
View File

@@ -64,6 +64,9 @@ importers:
pg: pg:
specifier: ^8.16.0 specifier: ^8.16.0
version: 8.19.0 version: 8.19.0
sharp:
specifier: ^0.33.5
version: 0.33.5
slugify: slugify:
specifier: ^1.6.6 specifier: ^1.6.6
version: 1.6.6 version: 1.6.6
@@ -80,6 +83,9 @@ importers:
'@types/pg': '@types/pg':
specifier: ^8.15.4 specifier: ^8.15.4
version: 8.18.0 version: 8.18.0
'@types/sharp':
specifier: ^0.32.0
version: 0.32.0
'@types/uuid': '@types/uuid':
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.0.0 version: 10.0.0
@@ -229,6 +235,9 @@ packages:
'@drizzle-team/brocli@0.10.2': '@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
'@emnapi/runtime@1.8.1':
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
'@envelop/core@5.5.1': '@envelop/core@5.5.1':
resolution: {integrity: sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==} resolution: {integrity: sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -961,6 +970,111 @@ packages:
'@iconify/utils@3.1.0': '@iconify/utils@3.1.0':
resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.33.5':
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.0.4':
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.0.4':
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.0.4':
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-ia32@0.33.5':
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.33.5':
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@internationalized/date@3.11.0': '@internationalized/date@3.11.0':
resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==}
@@ -1360,6 +1474,10 @@ packages:
'@types/resolve@1.20.2': '@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
'@types/sharp@0.32.0':
resolution: {integrity: sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==}
deprecated: This is a stub types definition. sharp provides its own type definitions, so you do not need this installed.
'@types/trusted-types@2.0.7': '@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -1506,9 +1624,23 @@ packages:
color-convert@1.9.3: color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.3: color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
colorette@2.0.19: colorette@2.0.19:
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
@@ -2026,6 +2158,9 @@ packages:
resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
is-arrayish@0.3.4:
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
is-core-module@2.16.1: is-core-module@2.16.1:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2562,6 +2697,10 @@ packages:
setprototypeof@1.2.0: setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2574,6 +2713,9 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'} engines: {node: '>=14'}
simple-swizzle@0.2.4:
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
sirv@3.0.2: sirv@3.0.2:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2886,6 +3028,11 @@ snapshots:
'@drizzle-team/brocli@0.10.2': {} '@drizzle-team/brocli@0.10.2': {}
'@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
optional: true
'@envelop/core@5.5.1': '@envelop/core@5.5.1':
dependencies: dependencies:
'@envelop/instrumentation': 1.0.0 '@envelop/instrumentation': 1.0.0
@@ -3393,6 +3540,81 @@ snapshots:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
mlly: 1.8.0 mlly: 1.8.0
'@img/sharp-darwin-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
optional: true
'@img/sharp-darwin-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.0.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.0.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.0.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linux-arm@1.0.5':
optional: true
'@img/sharp-libvips-linux-s390x@1.0.4':
optional: true
'@img/sharp-libvips-linux-x64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
optional: true
'@img/sharp-linux-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.0.4
optional: true
'@img/sharp-linux-arm@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.0.5
optional: true
'@img/sharp-linux-s390x@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.0.4
optional: true
'@img/sharp-linux-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.0.4
optional: true
'@img/sharp-linuxmusl-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
optional: true
'@img/sharp-linuxmusl-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
optional: true
'@img/sharp-wasm32@0.33.5':
dependencies:
'@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-win32-ia32@0.33.5':
optional: true
'@img/sharp-win32-x64@0.33.5':
optional: true
'@internationalized/date@3.11.0': '@internationalized/date@3.11.0':
dependencies: dependencies:
'@swc/helpers': 0.5.19 '@swc/helpers': 0.5.19
@@ -3723,6 +3945,10 @@ snapshots:
'@types/resolve@1.20.2': {} '@types/resolve@1.20.2': {}
'@types/sharp@0.32.0':
dependencies:
sharp: 0.33.5
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
'@types/uuid@10.0.0': {} '@types/uuid@10.0.0': {}
@@ -3874,8 +4100,24 @@ snapshots:
dependencies: dependencies:
color-name: 1.1.3 color-name: 1.1.3
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.3: {} color-name@1.1.3: {}
color-name@1.1.4: {}
color-string@1.9.1:
dependencies:
color-name: 1.1.3
simple-swizzle: 0.2.4
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
colorette@2.0.19: colorette@2.0.19:
optional: true optional: true
@@ -4415,6 +4657,8 @@ snapshots:
ipaddr.js@2.3.0: {} ipaddr.js@2.3.0: {}
is-arrayish@0.3.4: {}
is-core-module@2.16.1: is-core-module@2.16.1:
dependencies: dependencies:
hasown: 2.0.2 hasown: 2.0.2
@@ -4893,6 +5137,32 @@ snapshots:
setprototypeof@1.2.0: {} setprototypeof@1.2.0: {}
sharp@0.33.5:
dependencies:
color: 4.2.3
detect-libc: 2.1.2
semver: 7.7.3
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
'@img/sharp-libvips-darwin-arm64': 1.0.4
'@img/sharp-libvips-darwin-x64': 1.0.4
'@img/sharp-libvips-linux-arm': 1.0.5
'@img/sharp-libvips-linux-arm64': 1.0.4
'@img/sharp-libvips-linux-s390x': 1.0.4
'@img/sharp-libvips-linux-x64': 1.0.4
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
'@img/sharp-linux-arm': 0.33.5
'@img/sharp-linux-arm64': 0.33.5
'@img/sharp-linux-s390x': 0.33.5
'@img/sharp-linux-x64': 0.33.5
'@img/sharp-linuxmusl-arm64': 0.33.5
'@img/sharp-linuxmusl-x64': 0.33.5
'@img/sharp-wasm32': 0.33.5
'@img/sharp-win32-ia32': 0.33.5
'@img/sharp-win32-x64': 0.33.5
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
@@ -4901,6 +5171,10 @@ snapshots:
signal-exit@4.1.0: {} signal-exit@4.1.0: {}
simple-swizzle@0.2.4:
dependencies:
is-arrayish: 0.3.4
sirv@3.0.2: sirv@3.0.2:
dependencies: dependencies:
'@polka/url': 1.0.0-next.29 '@polka/url': 1.0.0-next.29