Compare commits

...

4 Commits

Author SHA1 Message Date
Paul Nothaft c5907a0833 fix(backend): set multer's fieldArrayIndexLimit to actually close CVE-2026-82333
The advisory is explicit that the 2.3.0 version bump alone doesn't
remediate the array-index DoS — an app must also set
limits.fieldArrayIndexLimit. Set it on every multer instance, sized to
what each route's form actually needs.
2026-09-10 23:37:03 +02:00
Paul Nothaft f13d163c74 fix(backend): bump sharp, nodemailer, multer, js-yaml, joi for security fixes
Resolves 12 open code-scanning alerts (#589-600): sharp libheif RCE,
nodemailer address-parser ReDoS + domain-validation bypasses, multer
upload DoS/race conditions, js-yaml parsing DoS, and joi prototype
pollution. All patch/minor bumps within the currently used major
version.
2026-09-10 17:44:21 +02:00
Paul Nothaft c4d89c9d64 chore(main): release 3.131.3-beta.0 (#1373)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Waiting to run
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Build and Push Docker Images / merge-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Waiting to run
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Build and Push Docker Images / merge-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-aio (linux/amd64, ubuntu-latest) (push) Waiting to run
Build and Push Docker Images / build-aio (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Build and Push Docker Images / merge-aio (push) Blocked by required conditions
Build and Push Docker Images / smoke-aio (push) Waiting to run
Build and Push Docker Images / build-ml (linux/amd64, ubuntu-latest) (push) Waiting to run
Build and Push Docker Images / build-ml (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Build and Push Docker Images / merge-ml (push) Blocked by required conditions
Build and Push Docker Images / dockerhub-descriptions (push) Blocked by required conditions
Build and Push Docker Images / summary (push) Blocked by required conditions
2026-09-10 11:54:50 +00:00
Paul Nothaft a2bf1f644c fix(video): try metadata extraction and thumbnail generation independently (#1371)
* fix(video): try metadata extraction and thumbnail generation independently

processUploadedVideo() gated everything behind isValidVideo(), which
rejects the whole video if ffprobe can't read even one of
duration/width/height -- common on some iPhone/Lightroom-exported
MP4s (issue 1370). Callers (photoProcessor.js's processPhoto and
processUploadedPhotos) already catch that throw and fall back to a
static placeholder thumbnail plus a metadata-only retry (codex
review of #845), but that fallback never got a REAL thumbnail even
when generateVideoThumbnail() would have succeeded on its own --
thumbnailing doesn't need valid duration/width/height, it just seeks
and grabs a frame.

processUploadedVideo now tries metadata extraction and thumbnail
generation independently, keeping whichever succeeds instead of
discarding both on a single failed field. The callers' existing
throw handling stays as a backstop.

Also: extractVideoMetadata stored duration as 0 (not null) whenever
ffprobe had no duration field, masking "unknown" as a fake real
zero-second clip and defeating downstream `duration != null` checks
meant to skip an untrustworthy value.

Relates to issue 1370

* fix(video): fall back to the SVG placeholder when thumbnail generation fails

processUploadedVideo could return success with thumbnailKey: null
when only thumbnail generation failed. The gallery grid
(GridGalleryLayout/JustifiedGalleryLayout) falls back to
`photo.thumbnail_url || photo.url` when there's no thumbnail, so
AuthenticatedImage downloaded the full original video and tried to
render it as an <img> -- a broken tile and a potentially huge
fetch just from opening the gallery.

Falls back to the same ffmpeg-free SVG placeholder the callers
already generate for a total processing failure, so a bare
thumbnail-generation failure degrades to that placeholder too,
never to "no thumbnail at all".

Found by codex review.

* fix(video): avoid a SQLite connection deadlock in the placeholder fallback

generateVideoPlaceholder() unconditionally called getThumbnailSettings(),
which queries the database directly (not through any active transaction).
videoProcessor.js's new placeholder fallback can run from inside
processUploadedPhotos' open per-file SQLite transaction (chunked video
upload) -- knex's default SQLite pool has exactly one connection, so
that second, un-transacted query deadlocks against the transaction
holding it, timing out after acquireConnectionTimeout (60s). Reproduced
directly against an isolated SQLite db.

generateVideoPlaceholder now skips the settings lookup entirely when
the caller supplies explicit width/height, and the video fallback
passes the same DEFAULT_THUMBNAIL_WIDTH/HEIGHT the settings lookup
would have fallen back to anyway (now exported for reuse).

Found by codex review.

* fix(video): throw when neither a real thumbnail nor the placeholder can be produced

processUploadedVideo returned success with thumbnailKey: null when
both the real thumbnail AND the SVG placeholder failed -- a total,
systemic failure (storage backend down, disk full), not a quirk of
one file. On stable, which doesn't have the #845 call-site fallback,
this silently completed the video with no thumbnail at all instead
of the retryable 'failed' status a throw here produces. On main,
the pre-existing #845 fallback already absorbed this exact case
(no behavior change there) -- verified against codex's own
git-blame check of the pre-PR stable code before applying this.

Now throws in that case, restoring the pre-existing "let the caller
mark it failed and retryable" behavior for a genuinely unrecoverable
video, while keeping every partial-failure case (the vast majority)
resolving with whatever succeeded.

Found by codex review.

---------

Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
2026-09-10 13:48:48 +02:00
26 changed files with 576 additions and 185 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "3.131.2-beta.0"
".": "3.131.3-beta.0"
}
+7
View File
@@ -5,6 +5,13 @@ All notable changes to PicPeak will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.131.3-beta.0](https://github.com/PicPeak/picpeak/compare/v3.131.2-beta.0...v3.131.3-beta.0) (2026-09-10)
### Bug Fixes
* **video:** try metadata extraction and thumbnail generation independently ([#1371](https://github.com/PicPeak/picpeak/issues/1371)) ([a2bf1f6](https://github.com/PicPeak/picpeak/commit/a2bf1f644c78734fc9a86a24d441b7a70bdb38fb))
## [3.131.2-beta.0](https://github.com/PicPeak/picpeak/compare/v3.131.1-beta.0...v3.131.2-beta.0) (2026-09-09)
@@ -25,14 +25,18 @@ process.env.STORAGE_PATH = path.join(tmpDir, 'storage');
fs.mkdirSync(process.env.STORAGE_PATH, { recursive: true });
process.env.JWT_SECRET = process.env.JWT_SECRET || 'crm-route-test-secret';
const express = require('express');
const cookieParser = require('cookie-parser');
const request = require('supertest');
const { bootCrmDb, seedMinimal, createPublicToken, buildRouteApp } = require('../integration/helpers/crmDb');
const tokenGuards = require('../../src/utils/publicTokenGuards');
const { errorHandler } = require('../../src/middleware/errorHandler');
describe('publicContracts routes', () => {
let db;
let cleanup;
let app;
let appWithErrorHandler;
let customerId;
let contractId;
@@ -51,6 +55,17 @@ describe('publicContracts routes', () => {
contractId = inserted[0]?.id ?? inserted[0];
app = buildRouteApp('/api/public/contracts', require('../../src/routes/publicContracts'));
// A second app instance wired to the REAL production error handler
// (buildRouteApp's is a simplified stand-in that only reads
// err.statusCode/err.status, which a bare MulterError doesn't set).
// Used below to verify the actual 4xx contract end-to-end, not just
// that multer aborted the request.
appWithErrorHandler = express();
appWithErrorHandler.use(express.json());
appWithErrorHandler.use(cookieParser());
appWithErrorHandler.use('/api/public/contracts', require('../../src/routes/publicContracts'));
appWithErrorHandler.use(errorHandler);
}, 120000);
afterAll(async () => {
@@ -131,6 +146,40 @@ describe('publicContracts routes', () => {
.attach('file', Buffer.from('%PDF-1.4 fake'), 'signed.pdf');
expect(res.status).toBe(404);
});
// CVE-2026-82333 regression (#1374 follow-up): multer 2.3.0 added an
// opt-in `fieldArrayIndexLimit` that must be set to actually close the
// field-parser DoS — the version bump alone does nothing. This route is
// unauthenticated (token-in-URL only), so it's the sharpest place to
// prove a crafted request with an oversized array-index field name
// (`evil[999999999]`) is rejected rather than accepted or left to hang.
it('rejects a multipart request with an oversized array-index field name', async () => {
const token = await createPublicToken(db, 'contract_action_tokens', {
contract_id: contractId,
});
const res = await request(app)
.post(`/api/public/contracts/${token}/upload-signed-pdf`)
.field('evil[999999999]', 'x')
.attach('file', Buffer.from('%PDF-1.4 fake'), 'signed.pdf');
// multer aborts the request before the handler runs; buildRouteApp's
// generic error handler falls back to 500 for a bare MulterError
// (see appWithErrorHandler test below for the real 4xx contract), so
// here we only assert the upload was NOT accepted/processed.
expect(res.status).toBeGreaterThanOrEqual(400);
expect(res.body.error).not.toBe(undefined);
});
it('maps the oversized array-index rejection to a 400 through the real error handler', async () => {
const token = await createPublicToken(db, 'contract_action_tokens', {
contract_id: contractId,
});
const res = await request(appWithErrorHandler)
.post(`/api/public/contracts/${token}/upload-signed-pdf`)
.field('evil[999999999]', 'x')
.attach('file', Buffer.from('%PDF-1.4 fake'), 'signed.pdf');
expect(res.status).toBe(400);
expect(res.body.code).toBe('VALIDATION_ERROR');
});
});
describe('GET /:token/pdf', () => {
@@ -0,0 +1,62 @@
/**
* generateVideoPlaceholder() must not touch the database when the caller
* already supplies width/height (videoProcessor.js's thumbnail-generation
* fallback does exactly this).
*
* Why it matters: processUploadedPhotos() (chunked video upload) holds a
* per-file SQLite transaction open across thumbnail generation. SQLite's
* knex pool defaults to a single connection, so any second, un-transacted
* db() query made while that transaction is open blocks until
* acquireConnectionTimeout (60s in production) — verified directly against
* an isolated SQLite db (codex review of #1371/#1372). Passing explicit
* dimensions must skip getThumbnailSettings()'s db() call entirely, not
* just tolerate its failure.
*/
const path = require('path');
const fs = require('fs').promises;
const os = require('os');
const mockDbSpy = jest.fn(() => {
throw new Error('db() must not be called when width/height are supplied');
});
jest.mock('../../src/database/db', () => ({ db: (...args) => mockDbSpy(...args) }));
const storageModule = require('../../src/services/storage');
const LocalFsStorage = require('../../src/services/storage/LocalFsStorage');
describe('generateVideoPlaceholder skips the settings DB lookup given explicit dimensions', () => {
let storage;
let root;
let imageProcessor;
beforeAll(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'picpeak-vidplaceholder-'));
storage = new LocalFsStorage({ root });
await storage.init();
storageModule.setStorageForTesting(storage);
imageProcessor = require('../../src/services/imageProcessor');
}, 30000);
afterAll(async () => {
storageModule.resetStorage();
await fs.rm(root, { recursive: true, force: true }).catch(() => {});
});
afterEach(() => mockDbSpy.mockClear());
it('never calls db() when width/height are provided', async () => {
const key = await imageProcessor.generateVideoPlaceholder('demo.mp4', { width: 300, height: 300 });
expect(key).toBe('thumbnails/thumb_demo.jpg');
expect(await storage.exists(key)).toBe(true);
expect(mockDbSpy).not.toHaveBeenCalled();
});
it('falls through to defaults (not a throw) when db() fails and no dimensions were given', async () => {
const key = await imageProcessor.generateVideoPlaceholder('demo2.mp4');
expect(key).toBe('thumbnails/thumb_demo2.jpg');
expect(mockDbSpy).toHaveBeenCalled();
});
});
+135 -135
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-backend",
"version": "3.123.0-beta.0",
"version": "3.131.3-beta.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-backend",
"version": "3.123.0-beta.0",
"version": "3.131.3-beta.0",
"dependencies": {
"@aws-sdk/client-s3": "^3.850.0",
"@aws-sdk/lib-storage": "^3.850.0",
@@ -39,7 +39,7 @@
"knex": "^2.4.2",
"mailparser": "^3.9.9",
"mime-types": "^3.0.1",
"multer": "2.2.0",
"multer": "2.3.0",
"node-cron": "^3.0.2",
"node-stream-zip": "^1.15.0",
"nodemailer": "^9.0.1",
@@ -53,7 +53,7 @@
"qrcode": "^1.5.4",
"react-i18next": "^15.6.0",
"sanitize-html": "2.17.7",
"sharp": "0.35.3",
"sharp": "0.35.4",
"sqlite3": "^5.1.6",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
@@ -1716,9 +1716,9 @@
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
"integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz",
"integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==",
"cpu": [
"arm64"
],
@@ -1734,13 +1734,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.3.2"
"@img/sharp-libvips-darwin-arm64": "1.3.3"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
"integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz",
"integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==",
"cpu": [
"x64"
],
@@ -1756,20 +1756,20 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.3.2"
"@img/sharp-libvips-darwin-x64": "1.3.3"
}
},
"node_modules/@img/sharp-freebsd-wasm32": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
"integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz",
"integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==",
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"dependencies": {
"@img/sharp-wasm32": "0.35.3"
"@img/sharp-wasm32": "0.35.4"
},
"engines": {
"node": ">=20.9.0"
@@ -1779,9 +1779,9 @@
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
"integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz",
"integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==",
"cpu": [
"arm64"
],
@@ -1795,9 +1795,9 @@
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
"integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz",
"integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==",
"cpu": [
"x64"
],
@@ -1811,9 +1811,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
"integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz",
"integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==",
"cpu": [
"arm"
],
@@ -1827,9 +1827,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
"integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz",
"integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==",
"cpu": [
"arm64"
],
@@ -1843,9 +1843,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
"integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz",
"integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==",
"cpu": [
"ppc64"
],
@@ -1859,9 +1859,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
"integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz",
"integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==",
"cpu": [
"riscv64"
],
@@ -1875,9 +1875,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
"integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz",
"integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==",
"cpu": [
"s390x"
],
@@ -1891,9 +1891,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
"integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz",
"integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==",
"cpu": [
"x64"
],
@@ -1907,9 +1907,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
"integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz",
"integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==",
"cpu": [
"arm64"
],
@@ -1923,9 +1923,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
"integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz",
"integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==",
"cpu": [
"x64"
],
@@ -1939,9 +1939,9 @@
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
"integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz",
"integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==",
"cpu": [
"arm"
],
@@ -1957,13 +1957,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.3.2"
"@img/sharp-libvips-linux-arm": "1.3.3"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
"integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz",
"integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==",
"cpu": [
"arm64"
],
@@ -1979,13 +1979,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.3.2"
"@img/sharp-libvips-linux-arm64": "1.3.3"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
"integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz",
"integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==",
"cpu": [
"ppc64"
],
@@ -2001,13 +2001,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.3.2"
"@img/sharp-libvips-linux-ppc64": "1.3.3"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
"integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz",
"integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==",
"cpu": [
"riscv64"
],
@@ -2023,13 +2023,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.3.2"
"@img/sharp-libvips-linux-riscv64": "1.3.3"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
"integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz",
"integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==",
"cpu": [
"s390x"
],
@@ -2045,13 +2045,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.3.2"
"@img/sharp-libvips-linux-s390x": "1.3.3"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
"integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz",
"integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==",
"cpu": [
"x64"
],
@@ -2067,13 +2067,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.3.2"
"@img/sharp-libvips-linux-x64": "1.3.3"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
"integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz",
"integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==",
"cpu": [
"arm64"
],
@@ -2089,13 +2089,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
"@img/sharp-libvips-linuxmusl-arm64": "1.3.3"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
"integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz",
"integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==",
"cpu": [
"x64"
],
@@ -2111,17 +2111,17 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.3.2"
"@img/sharp-libvips-linuxmusl-x64": "1.3.3"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
"integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz",
"integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.11.1"
"@emnapi/runtime": "^1.11.3"
},
"engines": {
"node": ">=20.9.0"
@@ -2131,16 +2131,16 @@
}
},
"node_modules/@img/sharp-webcontainers-wasm32": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
"integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz",
"integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/sharp-wasm32": "0.35.3"
"@img/sharp-wasm32": "0.35.4"
},
"engines": {
"node": ">=20.9.0"
@@ -2150,9 +2150,9 @@
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
"integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz",
"integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==",
"cpu": [
"arm64"
],
@@ -2169,9 +2169,9 @@
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
"integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz",
"integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==",
"cpu": [
"ia32"
],
@@ -2188,9 +2188,9 @@
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
"integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz",
"integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==",
"cpu": [
"x64"
],
@@ -8084,9 +8084,9 @@
}
},
"node_modules/joi": {
"version": "17.13.4",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz",
"integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==",
"version": "17.13.7",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.7.tgz",
"integrity": "sha512-MF80Dm5Y2veNy8QWVx9Bj3ui4mo7+VPSPsR1M+oaHXV0Gx6zGX9a2F+OZG3Blby9tOlzU9Rs5FUimlEhbKtfnQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.3.0",
@@ -8120,9 +8120,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"funding": [
{
"type": "github",
@@ -9195,9 +9195,9 @@
"license": "MIT"
},
"node_modules/multer": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.3.0.tgz",
"integrity": "sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
@@ -9398,9 +9398,9 @@
}
},
"node_modules/nodemailer": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz",
"integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.1.1.tgz",
"integrity": "sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
@@ -11258,9 +11258,9 @@
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.35.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
"integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
"integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.1.0",
@@ -11274,31 +11274,31 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.35.3",
"@img/sharp-darwin-x64": "0.35.3",
"@img/sharp-freebsd-wasm32": "0.35.3",
"@img/sharp-libvips-darwin-arm64": "1.3.2",
"@img/sharp-libvips-darwin-x64": "1.3.2",
"@img/sharp-libvips-linux-arm": "1.3.2",
"@img/sharp-libvips-linux-arm64": "1.3.2",
"@img/sharp-libvips-linux-ppc64": "1.3.2",
"@img/sharp-libvips-linux-riscv64": "1.3.2",
"@img/sharp-libvips-linux-s390x": "1.3.2",
"@img/sharp-libvips-linux-x64": "1.3.2",
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
"@img/sharp-libvips-linuxmusl-x64": "1.3.2",
"@img/sharp-linux-arm": "0.35.3",
"@img/sharp-linux-arm64": "0.35.3",
"@img/sharp-linux-ppc64": "0.35.3",
"@img/sharp-linux-riscv64": "0.35.3",
"@img/sharp-linux-s390x": "0.35.3",
"@img/sharp-linux-x64": "0.35.3",
"@img/sharp-linuxmusl-arm64": "0.35.3",
"@img/sharp-linuxmusl-x64": "0.35.3",
"@img/sharp-webcontainers-wasm32": "0.35.3",
"@img/sharp-win32-arm64": "0.35.3",
"@img/sharp-win32-ia32": "0.35.3",
"@img/sharp-win32-x64": "0.35.3"
"@img/sharp-darwin-arm64": "0.35.4",
"@img/sharp-darwin-x64": "0.35.4",
"@img/sharp-freebsd-wasm32": "0.35.4",
"@img/sharp-libvips-darwin-arm64": "1.3.3",
"@img/sharp-libvips-darwin-x64": "1.3.3",
"@img/sharp-libvips-linux-arm": "1.3.3",
"@img/sharp-libvips-linux-arm64": "1.3.3",
"@img/sharp-libvips-linux-ppc64": "1.3.3",
"@img/sharp-libvips-linux-riscv64": "1.3.3",
"@img/sharp-libvips-linux-s390x": "1.3.3",
"@img/sharp-libvips-linux-x64": "1.3.3",
"@img/sharp-libvips-linuxmusl-arm64": "1.3.3",
"@img/sharp-libvips-linuxmusl-x64": "1.3.3",
"@img/sharp-linux-arm": "0.35.4",
"@img/sharp-linux-arm64": "0.35.4",
"@img/sharp-linux-ppc64": "0.35.4",
"@img/sharp-linux-riscv64": "0.35.4",
"@img/sharp-linux-s390x": "0.35.4",
"@img/sharp-linux-x64": "0.35.4",
"@img/sharp-linuxmusl-arm64": "0.35.4",
"@img/sharp-linuxmusl-x64": "0.35.4",
"@img/sharp-webcontainers-wasm32": "0.35.4",
"@img/sharp-win32-arm64": "0.35.4",
"@img/sharp-win32-ia32": "0.35.4",
"@img/sharp-win32-x64": "0.35.4"
},
"peerDependenciesMeta": {
"@types/node": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "3.131.2-beta.0",
"version": "3.131.3-beta.0",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"engines": {
@@ -49,7 +49,7 @@
"knex": "^2.4.2",
"mailparser": "^3.9.9",
"mime-types": "^3.0.1",
"multer": "2.2.0",
"multer": "2.3.0",
"node-cron": "^3.0.2",
"node-stream-zip": "^1.15.0",
"nodemailer": "^9.0.1",
@@ -63,7 +63,7 @@
"qrcode": "^1.5.4",
"react-i18next": "^15.6.0",
"sanitize-html": "2.17.7",
"sharp": "0.35.3",
"sharp": "0.35.4",
"sqlite3": "^5.1.6",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
+14 -4
View File
@@ -120,7 +120,14 @@ const createPhotoUploader = (options = {}) => {
files: options.maxFiles || 2000,
fieldSize: 10 * 1024 * 1024,
parts: 10000,
headerPairs: 2000
headerPairs: 2000,
// CVE-2026-82333: no preset in this factory is currently wired up to
// a route (nothing imports createPhotoUploader et al. — routes build
// their own multer instances directly), but every preset gets the
// limit anyway so it can't be adopted later without it. None of the
// uploaders this factory builds have a legitimate use for
// array-indexed field names.
fieldArrayIndexLimit: 0
},
fileFilter: createFileFilter(ALLOWED_TYPES.media, {
validateMagicNumbers: true
@@ -146,7 +153,8 @@ const createLogoUploader = (options = {}) => {
}
}),
limits: {
fileSize: options.maxSize || SIZE_LIMITS.medium
fileSize: options.maxSize || SIZE_LIMITS.medium,
fieldArrayIndexLimit: 0 // CVE-2026-82333 — see createPhotoUploader comment
},
fileFilter: createFileFilter(ALLOWED_TYPES.logos, {
skipMagicValidation: ['image/svg+xml']
@@ -172,7 +180,8 @@ const createFaviconUploader = (options = {}) => {
}
}),
limits: {
fileSize: options.maxSize || SIZE_LIMITS.small
fileSize: options.maxSize || SIZE_LIMITS.small,
fieldArrayIndexLimit: 0 // CVE-2026-82333 — see createPhotoUploader comment
},
fileFilter: createFileFilter(ALLOWED_TYPES.favicons, {
skipMagicValidation: ['image/x-icon', 'image/vnd.microsoft.icon']
@@ -194,7 +203,8 @@ const createGalleryUploader = (destDir, options = {}) => {
dest: destDir,
limits: {
fileSize: options.maxSize || SIZE_LIMITS.large,
files: options.maxFiles || 10
files: options.maxFiles || 10,
fieldArrayIndexLimit: 0 // CVE-2026-82333 — see createPhotoUploader comment
},
fileFilter: createFileFilter(ALLOWED_TYPES.photos)
};
+10
View File
@@ -93,6 +93,16 @@ const handleKnownErrors = (err) => {
return new ValidationError('Unexpected file field');
}
// CVE-2026-82333: multer 2.3.0's fieldArrayIndexLimit rejects multipart
// field names with an oversized bracket array index (e.g. `a[99999999]`)
// before the DoS-prone field parser runs. Without this mapping the
// resulting MulterError has no .statusCode/.status and falls through to
// a 500 here, so map it to a proper 400 like the other multer limits.
if (err.code === 'LIMIT_FIELD_ARRAY_INDEX') {
const { ValidationError } = require('../utils/errors');
return new ValidationError('Field name array index too large');
}
return err;
};
+5 -1
View File
@@ -202,7 +202,11 @@ const picpeakUpload = multer({
destination: (req, file, cb) => cb(null, os.tmpdir()),
filename: (req, file, cb) => cb(null, `picpeak-upload-${Date.now()}-${crypto.randomBytes(6).toString('hex')}.picpeak`),
}),
limits: { fileSize: 5 * 1024 * 1024 * 1024 }, // 5 GB — .picpeak with photos can be large
// CVE-2026-82333: this route only ever consumes a single unnamed file
// field (`backup`) — no legitimate bracket-indexed field name (e.g.
// `a[0]`) exists in its form. fieldArrayIndexLimit: 0 rejects any field
// name using array-index syntax at all, closing multer's field-parser DoS.
limits: { fileSize: 5 * 1024 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 5 GB — .picpeak with photos can be large
});
// Upload + restore a .picpeak onto THIS instance. DESTRUCTIVE: full override of
+3 -1
View File
@@ -109,7 +109,9 @@ const pdfLogoStorage = multer.diskStorage({
const pdfLogoUpload = multer({
storage: pdfLogoStorage,
limits: { fileSize: 5 * 1024 * 1024 },
// CVE-2026-82333: single unnamed `logo` field only — no legitimate
// array-indexed field names, so reject any bracket-index field name.
limits: { fileSize: 5 * 1024 * 1024, fieldArrayIndexLimit: 0 },
fileFilter: (_req, file, cb) => {
const allowed = ['image/png', 'image/jpeg', 'image/svg+xml'];
if (allowed.includes(file.mimetype)) cb(null, true);
+3 -1
View File
@@ -32,7 +32,9 @@ const pageLogoStorage = multer.diskStorage({
const pageLogoUpload = multer({
storage: pageLogoStorage,
limits: { fileSize: 5 * 1024 * 1024 },
// CVE-2026-82333: single unnamed `logo` field only — no legitimate
// array-indexed field names, so reject any bracket-index field name.
limits: { fileSize: 5 * 1024 * 1024, fieldArrayIndexLimit: 0 },
fileFilter: (_req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
if (validateFileType(file.originalname, file.mimetype, allowed)) cb(null, true);
+3 -1
View File
@@ -73,7 +73,9 @@ const signedPdfStorage = multer.diskStorage({
const signedPdfUpload = multer({
storage: signedPdfStorage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
// CVE-2026-82333: single unnamed `file` field only — no legitimate
// array-indexed field names, so reject any bracket-index field name.
limits: { fileSize: 10 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 10 MB
fileFilter: (req, file, cb) => {
const allowed = ['application/pdf'];
if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true);
+3 -1
View File
@@ -31,7 +31,9 @@ const eventLogoStorage = multer.diskStorage({
const eventLogoUpload = multer({
storage: eventLogoStorage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
// CVE-2026-82333: single unnamed `logo` field only — no legitimate
// array-indexed field names, so reject any bracket-index field name.
limits: { fileSize: 5 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 5MB
fileFilter: (req, file, cb) => {
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) {
+4 -1
View File
@@ -41,7 +41,10 @@ function diskUpload(subdir) {
},
filename: (_req, file, cb) => cb(null, `${subdir.split('/').pop()}-${Date.now()}${path.extname(file.originalname) || ''}`),
}),
limits: { fileSize: 15 * 1024 * 1024 },
// CVE-2026-82333: both callers (`inboundUpload` → 'file', `proofUpload`
// → 'proof') take a single unnamed field — no legitimate array-indexed
// field names, so reject any bracket-index field name.
limits: { fileSize: 15 * 1024 * 1024, fieldArrayIndexLimit: 0 },
fileFilter: (_req, file, cb) => (ALLOWED_MIME.includes(file.mimetype) ? cb(null, true) : cb(new Error('Only PDF, JPEG or PNG files are allowed'))),
});
}
+3 -1
View File
@@ -72,7 +72,9 @@ const importedInvoiceStorage = multer.diskStorage({
});
const importedInvoiceUpload = multer({
storage: importedInvoiceStorage,
limits: { fileSize: 10 * 1024 * 1024 },
// CVE-2026-82333: single unnamed `pdf` field only — no legitimate
// array-indexed field names, so reject any bracket-index field name.
limits: { fileSize: 10 * 1024 * 1024, fieldArrayIndexLimit: 0 },
fileFilter: (_req, file, cb) => {
if (file.mimetype === 'application/pdf') cb(null, true);
else cb(new Error('Only PDF files are allowed for imported invoices'));
+6 -1
View File
@@ -112,7 +112,12 @@ const createUpload = (maxFileSizeBytes) => multer({
files: 2000, // Hard safety ceiling; actual limit enforced dynamically
fieldSize: 10 * 1024 * 1024, // 10MB for non-file fields
parts: 10000,
headerPairs: 2000
headerPairs: 2000,
// CVE-2026-82333: files arrive as repeated `photos` parts via
// multer's own .array('photos', N) — not bracket-indexed field names
// like `photos[0]` — so no legitimate field name uses array-index
// syntax at all. Reject any that do.
fieldArrayIndexLimit: 0
},
fileFilter: (req, file, cb) => {
// req.allowedMimeTypes is populated by the middleware that runs before multer
+7 -2
View File
@@ -153,7 +153,10 @@ const { validateFileType } = require('../utils/fileSecurityUtils');
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
// CVE-2026-82333: single unnamed field (`logo` or `watermarkLogo`) per
// route — no legitimate array-indexed field names, so reject any
// bracket-index field name.
limits: { fileSize: 5 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 5MB
fileFilter: (req, file, cb) => {
// Note: SVG files are excluded from magic number validation for logos
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];
@@ -181,7 +184,9 @@ const faviconStorage = multer.diskStorage({
const faviconUpload = multer({
storage: faviconStorage,
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB — roomy enough for a 512×512+ square PNG
// CVE-2026-82333: single unnamed `favicon` field only — no legitimate
// array-indexed field names, so reject any bracket-index field name.
limits: { fileSize: 2 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 2MB — roomy enough for a 512×512+ square PNG
fileFilter: (req, file, cb) => {
const allowedMimeTypes = ['image/png', 'image/x-icon', 'image/vnd.microsoft.icon'];
const name = file.originalname.toLowerCase();
+4 -1
View File
@@ -50,7 +50,10 @@ const tempStorage = multer.diskStorage({
function buildAdminUploader(maxSizeBytes, allowed) {
return multer({
storage: tempStorage,
limits: { fileSize: maxSizeBytes, files: ADMIN_MAX_FILES },
// CVE-2026-82333: files arrive as repeated `files` parts via .array(),
// not bracket-indexed field names like `files[0]` — no legitimate
// field name uses array-index syntax at all. Reject any that do.
limits: { fileSize: maxSizeBytes, files: ADMIN_MAX_FILES, fieldArrayIndexLimit: 0 },
fileFilter: (req, file, cb) => {
if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true);
return cb(new Error('This file type is not allowed'));
+6 -1
View File
@@ -77,7 +77,12 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
dest: tempUploadDir,
limits: {
fileSize: maxFileSizeBytes,
files: maxFilesPerUpload
files: maxFilesPerUpload,
// CVE-2026-82333: files arrive as repeated `photos` parts via
// .array(), not bracket-indexed field names like `photos[0]` — no
// legitimate field name uses array-index syntax at all. Reject any
// that do.
fieldArrayIndexLimit: 0
},
fileFilter: (req, file, cb) => {
if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) {
+4 -1
View File
@@ -62,7 +62,10 @@ const signedPdfStorage = multer.diskStorage({
const signedPdfUpload = multer({
storage: signedPdfStorage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
// CVE-2026-82333: single unnamed `file` field only, and this route is
// unauthenticated (token-only) — no legitimate array-indexed field
// names, so reject any bracket-index field name.
limits: { fileSize: 10 * 1024 * 1024, fieldArrayIndexLimit: 0 }, // 10 MB
fileFilter: (req, file, cb) => {
if (validateFileType(file.originalname, file.mimetype, ['application/pdf'])) return cb(null, true);
return cb(new Error('Only PDF files are allowed'));
+5 -1
View File
@@ -122,7 +122,11 @@ const tempStorage = multer.diskStorage({
function buildUploader(maxSizeBytes, allowed) {
return multer({
storage: tempStorage,
limits: { fileSize: maxSizeBytes, files: MAX_FILES_PER_UPLOAD },
// CVE-2026-82333: files arrive as repeated `files` parts via .array(),
// not bracket-indexed field names like `files[0]`, and this route is
// unauthenticated (token-only) — no legitimate field name uses
// array-index syntax at all. Reject any that do.
limits: { fileSize: maxSizeBytes, files: MAX_FILES_PER_UPLOAD, fieldArrayIndexLimit: 0 },
fileFilter: (req, file, cb) => {
if (validateFileType(file.originalname, file.mimetype, allowed)) return cb(null, true);
return cb(new Error('This file type is not allowed'));
+3 -1
View File
@@ -69,7 +69,9 @@ const photoStorage = multer.diskStorage({
});
const buildPhotoUpload = (maxFileSizeBytes) => multer({
storage: photoStorage,
limits: { fileSize: maxFileSizeBytes },
// CVE-2026-82333: single unnamed `photo` field only — no legitimate
// array-indexed field names, so reject any bracket-index field name.
limits: { fileSize: maxFileSizeBytes, fieldArrayIndexLimit: 0 },
fileFilter: (_req, file, cb) => {
if (/^image\//.test(file.mimetype)) cb(null, true);
else cb(new Error('Only image uploads are accepted on this endpoint'));
@@ -0,0 +1,125 @@
jest.mock('../../utils/logger');
jest.mock('fluent-ffmpeg');
jest.mock('../storage', () => ({
getStorage: jest.fn()
}));
jest.mock('../imageProcessor', () => ({
generateVideoPlaceholder: jest.fn(),
DEFAULT_THUMBNAIL_WIDTH: 300,
DEFAULT_THUMBNAIL_HEIGHT: 300
}));
const ffmpeg = require('fluent-ffmpeg');
const { getStorage } = require('../storage');
const { generateVideoPlaceholder } = require('../imageProcessor');
const {
extractVideoMetadata,
processUploadedVideo
} = require('../videoProcessor');
describe('extractVideoMetadata (#1370)', () => {
afterEach(() => jest.clearAllMocks());
it('returns null duration rather than 0 when ffprobe has none, so "unknown" and "a real 0s clip" stay distinguishable', async () => {
ffmpeg.ffprobe = jest.fn((videoPath, cb) => {
cb(null, {
streams: [{ codec_type: 'video', width: 1920, height: 1080, codec_name: 'hevc' }],
format: {} // no duration field at all
});
});
const metadata = await extractVideoMetadata('/tmp/video.mp4');
expect(metadata.duration).toBeNull();
expect(metadata.width).toBe(1920);
expect(metadata.videoCodec).toBe('hevc');
});
it('floors a real duration', async () => {
ffmpeg.ffprobe = jest.fn((videoPath, cb) => {
cb(null, { streams: [], format: { duration: 12.9 } });
});
const metadata = await extractVideoMetadata('/tmp/video.mp4');
expect(metadata.duration).toBe(12);
});
});
describe('processUploadedVideo degrades gracefully instead of rejecting the whole video (#1370)', () => {
let storage;
beforeEach(() => {
storage = { putFromFile: jest.fn().mockResolvedValue(undefined), exists: jest.fn().mockResolvedValue(true) };
getStorage.mockReturnValue(storage);
generateVideoPlaceholder.mockResolvedValue('thumbnails/thumb_placeholder.jpg');
});
afterEach(() => jest.clearAllMocks());
it('keeps the thumbnail when only metadata extraction fails', async () => {
ffmpeg.ffprobe = jest.fn((videoPath, cb) => cb(new Error('moov atom not found')));
ffmpeg.mockImplementation(() => ({
screenshots: jest.fn(function screenshots({ filename, folder }) {
require('fs').writeFileSync(require('path').join(folder, filename), 'jpeg-bytes');
return this;
}),
on(event, handler) {
if (event === 'end') setImmediate(handler);
return this;
}
}));
const result = await processUploadedVideo('/tmp/video.mp4', 'thumbnails/thumb_video.jpg');
expect(result.success).toBe(true);
expect(result.metadata).toBeNull();
expect(result.thumbnailKey).toBe('thumbnails/thumb_video.jpg');
// A real thumbnail already succeeded — never touch the placeholder path.
expect(generateVideoPlaceholder).not.toHaveBeenCalled();
});
it('falls back to the SVG placeholder when thumbnail generation fails, so the gallery never falls back to rendering the raw video as an <img> (codex review)', async () => {
ffmpeg.ffprobe = jest.fn((videoPath, cb) => {
cb(null, {
streams: [{ codec_type: 'video', width: 1080, height: 1920, codec_name: 'h264' }],
format: { duration: 5.4 }
});
});
ffmpeg.mockImplementation(() => ({
screenshots() { return this; },
on(event, handler) {
if (event === 'error') setImmediate(() => handler(new Error('ffmpeg seek failed')));
return this;
}
}));
const result = await processUploadedVideo('/tmp/video.mp4', 'thumbnails/thumb_wedding_001.jpg');
expect(result.success).toBe(true);
expect(result.metadata).toEqual(expect.objectContaining({ duration: 5, videoCodec: 'h264' }));
// thumbnailKey is always thumbnails/thumb_<name>.jpg — strip the prefix
// back to a filename so generateVideoPlaceholder recomputes the same key.
// Explicit width/height so generateVideoPlaceholder skips its DB-backed
// settings lookup — this can run inside an open per-file SQLite
// transaction (chunked video upload), where that lookup deadlocks.
expect(generateVideoPlaceholder).toHaveBeenCalledWith('wedding_001.jpg', { width: 300, height: 300 });
expect(result.thumbnailKey).toBe('thumbnails/thumb_placeholder.jpg');
expect(storage.putFromFile).not.toHaveBeenCalled();
});
it('throws when metadata, thumbnail generation, AND the placeholder all fail, so the caller surfaces a retryable failure instead of completing with nothing to show (codex review)', async () => {
ffmpeg.ffprobe = jest.fn((videoPath, cb) => cb(new Error('Invalid data found when processing input')));
ffmpeg.mockImplementation(() => ({
screenshots() { return this; },
on(event, handler) {
if (event === 'error') setImmediate(() => handler(new Error('ffmpeg seek failed')));
return this;
}
}));
generateVideoPlaceholder.mockRejectedValue(new Error('sharp render failed'));
await expect(processUploadedVideo('/tmp/corrupt.mp4', 'thumbnails/thumb_corrupt.jpg'))
.rejects.toThrow('Unable to generate any thumbnail');
});
});
+11 -3
View File
@@ -610,9 +610,15 @@ async function generateVideoPlaceholder(originalFilename, options = {}) {
const thumbnailRelKey = path.posix.join('thumbnails', thumbnailFilename);
const storage = getStorage();
const settings = await getThumbnailSettings();
const width = settings.width || DEFAULT_THUMBNAIL_WIDTH;
const height = settings.height || DEFAULT_THUMBNAIL_HEIGHT;
// Skip the settings lookup when the caller already supplies dimensions.
// This can run from inside an open per-file SQLite transaction (chunked
// video upload's fallback path in videoProcessor.js) — a second,
// un-transacted db() query for settings there deadlocks against SQLite's
// single-connection pool until acquireConnectionTimeout (60s), reproduced
// directly against an isolated SQLite db (codex review of #1371/#1372).
const settings = (options.width && options.height) ? {} : await getThumbnailSettings();
const width = options.width || settings.width || DEFAULT_THUMBNAIL_WIDTH;
const height = options.height || settings.height || DEFAULT_THUMBNAIL_HEIGHT;
if (options.regenerate) {
await storage.delete(thumbnailRelKey).catch(() => {});
@@ -1495,4 +1501,6 @@ module.exports = {
extractRawPreview,
withProcessableImage,
RAW_EXTENSIONS,
DEFAULT_THUMBNAIL_WIDTH,
DEFAULT_THUMBNAIL_HEIGHT,
};
+99 -23
View File
@@ -32,7 +32,10 @@ async function extractVideoMetadata(videoPath) {
const audioStream = metadata.streams.find(s => s.codec_type === 'audio');
const result = {
duration: Math.floor(metadata.format.duration || 0),
// null (not 0) when ffprobe genuinely has no duration — a real
// 0-second clip and "unknown" must stay distinguishable, since
// downstream code treats `duration != null` as "trust this value".
duration: metadata.format.duration != null ? Math.floor(metadata.format.duration) : null,
width: videoStream?.width || null,
height: videoStream?.height || null,
videoCodec: videoStream?.codec_name || null,
@@ -130,35 +133,108 @@ async function getVideoDuration(videoPath) {
* Process an uploaded video: extract metadata and produce a thumbnail through
* the storage backend.
*
* Metadata extraction and thumbnail generation are independent, best-effort
* steps — mirroring how the image pipeline treats thumbnail/dimension/EXIF
* failures (log a warning, keep the upload). This used to gate everything
* behind isValidVideo(), which rejects the whole video if ffprobe can't read
* even one of duration/width/height — common on some iPhone/Lightroom-
* exported MP4s (#1370). Callers (photoProcessor.js's processPhoto and
* processUploadedPhotos) already catch that throw and fall back to a static
* placeholder thumbnail plus a metadata-only retry (codex review of #845),
* but that fallback never got a REAL thumbnail even when
* generateVideoThumbnail() would have succeeded on its own — thumbnailing
* doesn't need valid duration/width/height, it just seeks and grabs a frame.
* Trying both steps independently means a real thumbnail (and whatever
* metadata ffprobe *can* read) survives far more often. metadata is still
* allowed to come back null (ffprobe failed) — a video with no thumbnail
* would fall back to rendering the raw video as an <img> in the gallery
* grid (`photo.thumbnail_url || photo.url`), so this only resolves when a
* real thumbnail or the SVG placeholder produced *something*; if both fail
* (storage backend down, disk full — not a quirk of one file) it throws
* instead, so the caller surfaces a retryable failure rather than silently
* completing with nothing to show.
*
* @param {string} videoPath - Local path to the source video (ffmpeg requires fs).
* @param {string} thumbnailKey - Relative storage key for the thumbnail.
* @returns {Promise<{success: boolean, metadata: Object, thumbnailKey: string}>}
* @returns {Promise<{success: boolean, metadata: Object|null, thumbnailKey: string}>}
*/
async function processUploadedVideo(videoPath, thumbnailKey, options = {}) {
let metadata = null;
try {
const isValid = await isValidVideo(videoPath);
if (!isValid) {
throw new Error('Invalid video file');
}
const metadata = await extractVideoMetadata(videoPath);
await generateVideoThumbnail(videoPath, thumbnailKey, options);
const storage = getStorage();
const exists = await storage.exists(thumbnailKey);
if (!exists) {
throw new Error('Thumbnail generation failed (not in storage)');
}
return {
success: true,
metadata,
thumbnailKey
};
metadata = await extractVideoMetadata(videoPath);
} catch (error) {
logger.error('Error processing video', { error: error.message, videoPath });
throw error;
logger.error('Video metadata extraction failed — continuing without duration/codec/dimensions', {
error: error.message,
videoPath
});
}
let generatedThumbnailKey = null;
try {
await generateVideoThumbnail(videoPath, thumbnailKey, options);
const storage = getStorage();
if (await storage.exists(thumbnailKey)) {
generatedThumbnailKey = thumbnailKey;
}
} catch (error) {
logger.error('Video thumbnail generation failed — continuing without a thumbnail', {
error: error.message,
videoPath
});
}
// Never return "success" with no thumbnail at all: the gallery grid
// (GridGalleryLayout/JustifiedGalleryLayout) falls back to
// `photo.thumbnail_url || photo.url` when there's no thumbnail, which
// makes AuthenticatedImage download the full ORIGINAL VIDEO and try to
// render it as an <img> — a broken tile and a multi-GB fetch just from
// opening the gallery (codex review, #1371/#1372). Fall back to the same
// ffmpeg-free SVG placeholder the callers already generate for a total
// processing failure, so a bare thumbnail-generation failure degrades to
// that placeholder too, not to "no thumbnail". thumbnailKey is always
// `thumbnails/thumb_<name>.jpg` (see callers) — strip the prefix back to
// a filename so generateVideoPlaceholder recomputes this exact same key.
if (!generatedThumbnailKey) {
try {
const {
generateVideoPlaceholder,
DEFAULT_THUMBNAIL_WIDTH,
DEFAULT_THUMBNAIL_HEIGHT
} = require('./imageProcessor');
const placeholderFilename = path.basename(thumbnailKey).replace(/^thumb_/, '');
// Explicit width/height make generateVideoPlaceholder skip its
// configured-thumbnail-size DB lookup (see its own comment) — this
// call can run from inside processUploadedPhotos' open per-file
// SQLite transaction, where that lookup would otherwise deadlock.
const placeholderKey = await generateVideoPlaceholder(placeholderFilename, {
width: DEFAULT_THUMBNAIL_WIDTH,
height: DEFAULT_THUMBNAIL_HEIGHT
});
if (placeholderKey) {
generatedThumbnailKey = placeholderKey;
}
} catch (error) {
logger.error('Video placeholder generation also failed', { error: error.message, videoPath });
}
}
// A real thumbnail AND the ffmpeg-free SVG placeholder both failing points
// at something systemic (storage backend down, disk full) rather than a
// quirk of this one file — that's worth surfacing as a retryable failure
// rather than silently completing with no thumbnail at all, which would
// make the gallery fall back to rendering the raw video as an <img>
// (codex review, #1371/#1372). Metadata (if any was extracted) is lost
// here, same trade-off the callers' own pre-existing total-failure
// handling already makes.
if (!generatedThumbnailKey) {
throw new Error('Unable to generate any thumbnail (real or placeholder) for this video');
}
return {
success: true,
metadata,
thumbnailKey: generatedThumbnailKey
};
}
/**
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "3.131.2-beta.0",
"version": "3.131.3-beta.0",
"type": "module",
"scripts": {
"dev": "vite",