@open-mercato/core 0.6.7-develop.6592.1.7f69fa2454 → 0.6.7-develop.6593.1.0ecf630f1e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/attachments/api/image/%5Bid%5D/%5B%5B...slug%5D%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport sharp from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentOrganizationId } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nexport async function GET(\n req: NextRequest,\n context: { params: Promise<{ id: string; slug?: string[] | undefined }> }\n) {\n const auth = await getAuthFromRequest(req)\n const { id } = await context.params\n if (!id) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const parsedQuery = querySchema.safeParse(\n Object.fromEntries(new URL(req.url).searchParams.entries())\n )\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'Invalid size parameters' }, { status: 400 })\n }\n const { width, height, cropType } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const scopedAuth = auth\n ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) }\n : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = checkAttachmentAccess(scopedAuth, attachment, partition)\n if (!access.ok) {\n const message = access.status === 401 ? 'Unauthorized' : 'Forbidden'\n return NextResponse.json({ error: message }, { status: access.status })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions:
|
|
5
|
-
"mappings": "AAAA,SAAsB,oBAAoB;AAC1C,OAAO,
|
|
4
|
+
"sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport sharp, { type ResizeOptions } from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentOrganizationId } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nexport async function GET(\n req: NextRequest,\n context: { params: Promise<{ id: string; slug?: string[] | undefined }> }\n) {\n const auth = await getAuthFromRequest(req)\n const { id } = await context.params\n if (!id) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const parsedQuery = querySchema.safeParse(\n Object.fromEntries(new URL(req.url).searchParams.entries())\n )\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'Invalid size parameters' }, { status: 400 })\n }\n const { width, height, cropType } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const scopedAuth = auth\n ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) }\n : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = checkAttachmentAccess(scopedAuth, attachment, partition)\n if (!access.ok) {\n const message = access.status === 401 ? 'Unauthorized' : 'Forbidden'\n return NextResponse.json({ error: message }, { status: access.status })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions: ResizeOptions = {\n width: width || undefined,\n height: height || undefined,\n fit: cropType === 'contain' ? 'contain' : 'cover',\n }\n if (cropType === 'contain') {\n resizeOptions.background = { r: 0, g: 0, b: 0, alpha: 0 }\n }\n transformer = transformer.resize(resizeOptions)\n }\n buffer = await transformer.toBuffer()\n if (cacheKey) {\n void writeThumbnailCache(attachment.partitionCode, attachment.id, cacheKey, buffer).catch((cacheError) => {\n logger.error('Thumbnail cache write failed', { err: cacheError })\n })\n }\n }\n if (!buffer) {\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n const responseBody = new Uint8Array(buffer)\n\n return new NextResponse(responseBody, {\n headers: {\n 'Content-Type': attachment.mimeType || 'image/jpeg',\n 'Cache-Control': partition.isPublic ? 'public, max-age=3600' : 'private, max-age=60',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (error) {\n logger.error('Image read failed', { err: error })\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Serve resized images',\n methods: {\n GET: {\n summary: 'Serve image with optional resizing',\n description: 'Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).',\n responses: [\n {\n status: 200,\n description: 'Binary image content (Content-Type: image/jpeg, image/png, etc.)',\n schema: z.any().describe('Binary image content - actual Content-Type header set to image MIME type, not application/json'),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },\n { status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },\n { status: 404, description: 'Image not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Partition misconfigured or image rendering failed', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/imageSafety.ts"],
|
|
4
|
-
"sourcesContent": ["import sharp from 'sharp'\n\nexport const MAX_IMAGE_SOURCE_BYTES = 25 * 1024 * 1024\nexport const MAX_IMAGE_SOURCE_PIXELS = 40_000_000\n\nconst allowedImageMimeTypes = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/gif',\n 'image/webp',\n])\n\nconst mimeAliases = new Map([\n ['image/jpg', 'image/jpeg'],\n ['image/pjpeg', 'image/jpeg'],\n ['image/x-png', 'image/png'],\n])\n\nexport type ImageSafetyResult =\n | { ok: true; mimeType: string }\n | { ok: false; status: 400 | 413; error: string }\n\nfunction normalizeMimeType(mimeType: string | null | undefined): string | null {\n if (typeof mimeType !== 'string') return null\n const normalized = mimeType.split(';')[0]?.trim().toLowerCase() ?? ''\n if (!normalized) return null\n return mimeAliases.get(normalized) ?? normalized\n}\n\nexport function detectImageMimeType(buffer: Buffer): string | null {\n if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {\n return 'image/jpeg'\n }\n\n if (\n buffer.length >= 8 &&\n buffer[0] === 0x89 &&\n buffer[1] === 0x50 &&\n buffer[2] === 0x4e &&\n buffer[3] === 0x47 &&\n buffer[4] === 0x0d &&\n buffer[5] === 0x0a &&\n buffer[6] === 0x1a &&\n buffer[7] === 0x0a\n ) {\n return 'image/png'\n }\n\n if (\n buffer.length >= 6 &&\n buffer[0] === 0x47 &&\n buffer[1] === 0x49 &&\n buffer[2] === 0x46 &&\n buffer[3] === 0x38 &&\n (buffer[4] === 0x37 || buffer[4] === 0x39) &&\n buffer[5] === 0x61\n ) {\n return 'image/gif'\n }\n\n if (\n buffer.length >= 12 &&\n buffer.toString('ascii', 0, 4) === 'RIFF' &&\n buffer.toString('ascii', 8, 12) === 'WEBP'\n ) {\n return 'image/webp'\n }\n\n return null\n}\n\nexport function validateImageMagicBytes(buffer: Buffer, declaredMimeType: string | null | undefined): ImageSafetyResult {\n if (buffer.length > MAX_IMAGE_SOURCE_BYTES) {\n return { ok: false, status: 413, error: 'Image exceeds source byte limit' }\n }\n\n const detectedMimeType = detectImageMimeType(buffer)\n if (!detectedMimeType || !allowedImageMimeTypes.has(detectedMimeType)) {\n return { ok: false, status: 400, error: 'Unsupported image format' }\n }\n\n const normalizedDeclaredMimeType = normalizeMimeType(declaredMimeType)\n if (!normalizedDeclaredMimeType || !allowedImageMimeTypes.has(normalizedDeclaredMimeType)) {\n return { ok: false, status: 400, error: 'Unsupported media type' }\n }\n\n if (normalizedDeclaredMimeType !== detectedMimeType) {\n return { ok: false, status: 400, error: 'Image MIME type does not match file content' }\n }\n\n return { ok: true, mimeType: detectedMimeType }\n}\n\nexport async function validateImageDimensions(buffer: Buffer): Promise<ImageSafetyResult> {\n let metadata:
|
|
5
|
-
"mappings": "AAAA,OAAO,
|
|
4
|
+
"sourcesContent": ["import sharp, { type Metadata } from 'sharp'\n\nexport const MAX_IMAGE_SOURCE_BYTES = 25 * 1024 * 1024\nexport const MAX_IMAGE_SOURCE_PIXELS = 40_000_000\n\nconst allowedImageMimeTypes = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/gif',\n 'image/webp',\n])\n\nconst mimeAliases = new Map([\n ['image/jpg', 'image/jpeg'],\n ['image/pjpeg', 'image/jpeg'],\n ['image/x-png', 'image/png'],\n])\n\nexport type ImageSafetyResult =\n | { ok: true; mimeType: string }\n | { ok: false; status: 400 | 413; error: string }\n\nfunction normalizeMimeType(mimeType: string | null | undefined): string | null {\n if (typeof mimeType !== 'string') return null\n const normalized = mimeType.split(';')[0]?.trim().toLowerCase() ?? ''\n if (!normalized) return null\n return mimeAliases.get(normalized) ?? normalized\n}\n\nexport function detectImageMimeType(buffer: Buffer): string | null {\n if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {\n return 'image/jpeg'\n }\n\n if (\n buffer.length >= 8 &&\n buffer[0] === 0x89 &&\n buffer[1] === 0x50 &&\n buffer[2] === 0x4e &&\n buffer[3] === 0x47 &&\n buffer[4] === 0x0d &&\n buffer[5] === 0x0a &&\n buffer[6] === 0x1a &&\n buffer[7] === 0x0a\n ) {\n return 'image/png'\n }\n\n if (\n buffer.length >= 6 &&\n buffer[0] === 0x47 &&\n buffer[1] === 0x49 &&\n buffer[2] === 0x46 &&\n buffer[3] === 0x38 &&\n (buffer[4] === 0x37 || buffer[4] === 0x39) &&\n buffer[5] === 0x61\n ) {\n return 'image/gif'\n }\n\n if (\n buffer.length >= 12 &&\n buffer.toString('ascii', 0, 4) === 'RIFF' &&\n buffer.toString('ascii', 8, 12) === 'WEBP'\n ) {\n return 'image/webp'\n }\n\n return null\n}\n\nexport function validateImageMagicBytes(buffer: Buffer, declaredMimeType: string | null | undefined): ImageSafetyResult {\n if (buffer.length > MAX_IMAGE_SOURCE_BYTES) {\n return { ok: false, status: 413, error: 'Image exceeds source byte limit' }\n }\n\n const detectedMimeType = detectImageMimeType(buffer)\n if (!detectedMimeType || !allowedImageMimeTypes.has(detectedMimeType)) {\n return { ok: false, status: 400, error: 'Unsupported image format' }\n }\n\n const normalizedDeclaredMimeType = normalizeMimeType(declaredMimeType)\n if (!normalizedDeclaredMimeType || !allowedImageMimeTypes.has(normalizedDeclaredMimeType)) {\n return { ok: false, status: 400, error: 'Unsupported media type' }\n }\n\n if (normalizedDeclaredMimeType !== detectedMimeType) {\n return { ok: false, status: 400, error: 'Image MIME type does not match file content' }\n }\n\n return { ok: true, mimeType: detectedMimeType }\n}\n\nexport async function validateImageDimensions(buffer: Buffer): Promise<ImageSafetyResult> {\n let metadata: Metadata\n try {\n metadata = await sharp(buffer, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n }).metadata()\n } catch {\n return { ok: false, status: 400, error: 'Invalid image content' }\n }\n\n const width = metadata.width ?? 0\n const height = metadata.height ?? 0\n if (width <= 0 || height <= 0) {\n return { ok: false, status: 400, error: 'Invalid image dimensions' }\n }\n\n if (width * height > MAX_IMAGE_SOURCE_PIXELS) {\n return { ok: false, status: 413, error: 'Image exceeds pixel limit' }\n }\n\n return { ok: true, mimeType: metadata.format ? `image/${metadata.format}` : 'image/jpeg' }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6593.1.0ecf630f1e",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -248,21 +248,22 @@
|
|
|
248
248
|
"resend": "^6.17.2",
|
|
249
249
|
"sanitize-html": "^2.17.5",
|
|
250
250
|
"semver": "^7.8.5",
|
|
251
|
+
"sharp": "^0.35.3",
|
|
251
252
|
"svix": "^1.96.1",
|
|
252
253
|
"ts-pattern": "^5.0.0",
|
|
253
254
|
"zod": "^4.4.3"
|
|
254
255
|
},
|
|
255
256
|
"peerDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6593.1.0ecf630f1e",
|
|
258
|
+
"@open-mercato/shared": "0.6.7-develop.6593.1.0ecf630f1e",
|
|
259
|
+
"@open-mercato/ui": "0.6.7-develop.6593.1.0ecf630f1e",
|
|
259
260
|
"react": "^19.0.0",
|
|
260
261
|
"react-dom": "^19.0.0"
|
|
261
262
|
},
|
|
262
263
|
"devDependencies": {
|
|
263
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
264
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
265
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6593.1.0ecf630f1e",
|
|
265
|
+
"@open-mercato/shared": "0.6.7-develop.6593.1.0ecf630f1e",
|
|
266
|
+
"@open-mercato/ui": "0.6.7-develop.6593.1.0ecf630f1e",
|
|
266
267
|
"@testing-library/dom": "^10.4.1",
|
|
267
268
|
"@testing-library/jest-dom": "^6.9.1",
|
|
268
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import sharp from 'sharp'
|
|
2
|
+
import sharp, { type ResizeOptions } from 'sharp'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
5
5
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
@@ -109,7 +109,7 @@ export async function GET(
|
|
|
109
109
|
limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,
|
|
110
110
|
})
|
|
111
111
|
if (width || height) {
|
|
112
|
-
const resizeOptions:
|
|
112
|
+
const resizeOptions: ResizeOptions = {
|
|
113
113
|
width: width || undefined,
|
|
114
114
|
height: height || undefined,
|
|
115
115
|
fit: cropType === 'contain' ? 'contain' : 'cover',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import sharp from 'sharp'
|
|
1
|
+
import sharp, { type Metadata } from 'sharp'
|
|
2
2
|
|
|
3
3
|
export const MAX_IMAGE_SOURCE_BYTES = 25 * 1024 * 1024
|
|
4
4
|
export const MAX_IMAGE_SOURCE_PIXELS = 40_000_000
|
|
@@ -92,7 +92,7 @@ export function validateImageMagicBytes(buffer: Buffer, declaredMimeType: string
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export async function validateImageDimensions(buffer: Buffer): Promise<ImageSafetyResult> {
|
|
95
|
-
let metadata:
|
|
95
|
+
let metadata: Metadata
|
|
96
96
|
try {
|
|
97
97
|
metadata = await sharp(buffer, {
|
|
98
98
|
failOn: 'error',
|