added direct s3 bucket calls from teh client

This commit is contained in:
Dmitri 2026-03-25 17:32:44 +04:00
parent c25e7cdcc2
commit 96e771bbfb
9 changed files with 978 additions and 281 deletions

View File

@ -17,6 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1011.0", "@aws-sdk/client-s3": "^3.1011.0",
"@aws-sdk/s3-request-presigner": "^3.1016.0",
"@google-cloud/storage": "^7.0.0", "@google-cloud/storage": "^7.0.0",
"axios": "^1.13.0", "axios": "^1.13.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
@ -41,9 +42,9 @@
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-microsoft": "^2.0.0", "passport-microsoft": "^2.0.0",
"pg": "^8.20.0", "pg": "^8.20.0",
"pg-hstore": "2.3.4",
"pino": "^9.0.0", "pino": "^9.0.0",
"pino-pretty": "^11.0.0", "pino-pretty": "^11.0.0",
"pg-hstore": "2.3.4",
"sequelize": "^6.37.0", "sequelize": "^6.37.0",
"sequelize-json-schema": "^2.1.1", "sequelize-json-schema": "^2.1.1",
"sqlite": "4.0.15", "sqlite": "4.0.15",

View File

@ -18,6 +18,33 @@ router.get('/download', (req, res) => {
services.downloadFile(req, res); services.downloadFile(req, res);
}); });
// POST /api/file/presign - Generate presigned URLs for multiple assets
router.post('/presign', jsonParser, async (req, res) => {
const { urls } = req.body || {};
if (!Array.isArray(urls) || urls.length === 0) {
return res.status(400).json({ error: 'urls array required' });
}
if (urls.length > 50) {
return res.status(400).json({ error: 'Maximum 50 URLs per request' });
}
// Validate that all URLs are strings
const invalidUrls = urls.filter((url) => typeof url !== 'string' || !url.trim());
if (invalidUrls.length > 0) {
return res.status(400).json({ error: 'All URLs must be non-empty strings' });
}
try {
const presignedUrls = await services.generatePresignedUrls(urls);
res.json({ presignedUrls });
} catch (error) {
console.error('Failed to generate presigned URLs', error);
res.status(500).json({ error: 'Failed to generate presigned URLs' });
}
});
router.post('/upload/:table/:field', passport.authenticate('jwt', {session: false}), (req, res) => { router.post('/upload/:table/:field', passport.authenticate('jwt', {session: false}), (req, res) => {
const fileName = `${req.params.table}/${req.params.field}`; const fileName = `${req.params.table}/${req.params.field}`;

View File

@ -13,6 +13,7 @@ const {
ListObjectsV2Command, ListObjectsV2Command,
DeleteObjectsCommand, DeleteObjectsCommand,
} = require('@aws-sdk/client-s3'); } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const ensureDirectoryExistence = (filePath) => { const ensureDirectoryExistence = (filePath) => {
const dirname = path.dirname(filePath); const dirname = path.dirname(filePath);
@ -1090,6 +1091,44 @@ const deleteFile = async (privateUrl) => {
return deleteLocal(privateUrl); return deleteLocal(privateUrl);
} }
const PRESIGN_EXPIRY_SECONDS = 3600; // 1 hour
/**
* Generate presigned GET URLs for multiple assets.
* For S3: returns direct S3 signed URLs.
* For other providers: returns backend proxy URLs.
*
* @param {string[]} urls - Array of storage_key paths
* @returns {Promise<Record<string, string>>} Map of original path to presigned/proxy URL
*/
const generatePresignedUrls = async (urls) => {
const provider = getFileStorageProvider();
if (provider !== 's3') {
// For non-S3 providers, return backend proxy URLs
return urls.reduce((acc, url) => {
acc[url] = `/api/file/download?privateUrl=${encodeURIComponent(url)}`;
return acc;
}, {});
}
const { client, bucket, prefix } = initS3();
const presignedUrls = {};
await Promise.all(
urls.map(async (url) => {
const key = buildStoragePath(prefix, url);
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
presignedUrls[url] = await getSignedUrl(client, command, {
expiresIn: PRESIGN_EXPIRY_SECONDS,
});
})
);
return presignedUrls;
};
module.exports = { module.exports = {
initUploadSession, initUploadSession,
getUploadSession, getUploadSession,
@ -1110,4 +1149,5 @@ module.exports = {
downloadGCloud, downloadGCloud,
uploadS3, uploadS3,
downloadS3, downloadS3,
generatePresignedUrls,
} }

View File

@ -77,7 +77,7 @@
"@smithy/util-utf8" "^2.0.0" "@smithy/util-utf8" "^2.0.0"
tslib "^2.6.2" tslib "^2.6.2"
"@aws-crypto/sha256-js@^5.2.0", "@aws-crypto/sha256-js@5.2.0": "@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0":
version "5.2.0" version "5.2.0"
resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz" resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz"
integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==
@ -93,7 +93,7 @@
dependencies: dependencies:
tslib "^2.6.2" tslib "^2.6.2"
"@aws-crypto/util@^5.2.0", "@aws-crypto/util@5.2.0": "@aws-crypto/util@5.2.0", "@aws-crypto/util@^5.2.0":
version "5.2.0" version "5.2.0"
resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz" resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz"
integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==
@ -182,6 +182,25 @@
"@smithy/util-utf8" "^4.2.2" "@smithy/util-utf8" "^4.2.2"
tslib "^2.6.2" tslib "^2.6.2"
"@aws-sdk/core@^3.973.24":
version "3.973.24"
resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.973.24.tgz#3fe12eb94fb8733a7b07f7a00fda9b20b42179fd"
integrity sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw==
dependencies:
"@aws-sdk/types" "^3.973.6"
"@aws-sdk/xml-builder" "^3.972.15"
"@smithy/core" "^3.23.12"
"@smithy/node-config-provider" "^4.3.12"
"@smithy/property-provider" "^4.2.12"
"@smithy/protocol-http" "^5.3.12"
"@smithy/signature-v4" "^5.3.12"
"@smithy/smithy-client" "^4.12.7"
"@smithy/types" "^4.13.1"
"@smithy/util-base64" "^4.3.2"
"@smithy/util-middleware" "^4.2.12"
"@smithy/util-utf8" "^4.2.2"
tslib "^2.6.2"
"@aws-sdk/crc64-nvme@^3.972.5": "@aws-sdk/crc64-nvme@^3.972.5":
version "3.972.5" version "3.972.5"
resolved "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz" resolved "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz"
@ -410,6 +429,26 @@
"@smithy/util-utf8" "^4.2.2" "@smithy/util-utf8" "^4.2.2"
tslib "^2.6.2" tslib "^2.6.2"
"@aws-sdk/middleware-sdk-s3@^3.972.24":
version "3.972.24"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.24.tgz#1a327a22ef88042b3a5542eb98fd31c31e568af9"
integrity sha512-4sXxVC/enYgMkZefNMOzU6C6KtAXEvwVJLgNcUx1dvROH6GvKB5Sm2RGnGzTp0/PwkibIyMw4kOzF8tbLfaBAQ==
dependencies:
"@aws-sdk/core" "^3.973.24"
"@aws-sdk/types" "^3.973.6"
"@aws-sdk/util-arn-parser" "^3.972.3"
"@smithy/core" "^3.23.12"
"@smithy/node-config-provider" "^4.3.12"
"@smithy/protocol-http" "^5.3.12"
"@smithy/signature-v4" "^5.3.12"
"@smithy/smithy-client" "^4.12.7"
"@smithy/types" "^4.13.1"
"@smithy/util-config-provider" "^4.2.2"
"@smithy/util-middleware" "^4.2.12"
"@smithy/util-stream" "^4.5.20"
"@smithy/util-utf8" "^4.2.2"
tslib "^2.6.2"
"@aws-sdk/middleware-ssec@^3.972.8": "@aws-sdk/middleware-ssec@^3.972.8":
version "3.972.8" version "3.972.8"
resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz" resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz"
@ -488,6 +527,32 @@
"@smithy/types" "^4.13.1" "@smithy/types" "^4.13.1"
tslib "^2.6.2" tslib "^2.6.2"
"@aws-sdk/s3-request-presigner@^3.1016.0":
version "3.1016.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1016.0.tgz#4903e6188a8fdc16cd5f331ae02b9ea9843d881e"
integrity sha512-BROPno9Y8xYltQu5k1AupDPaWdFR9Ig8zfDSZzTE+MTvKpif6wyAHFJRW0C0xIwZckaHya2oFoTZbPHtyIlQkg==
dependencies:
"@aws-sdk/signature-v4-multi-region" "^3.996.12"
"@aws-sdk/types" "^3.973.6"
"@aws-sdk/util-format-url" "^3.972.8"
"@smithy/middleware-endpoint" "^4.4.27"
"@smithy/protocol-http" "^5.3.12"
"@smithy/smithy-client" "^4.12.7"
"@smithy/types" "^4.13.1"
tslib "^2.6.2"
"@aws-sdk/signature-v4-multi-region@^3.996.12":
version "3.996.12"
resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.12.tgz#7228509cba3845e00c089fe41b4636cf81fee798"
integrity sha512-abRObSqjVeKUUHIZfAp78PTYrEsxCgVKDs/YET357pzT5C02eDDEvmWyeEC2wglWcYC4UTbBFk22gd2YJUlCQg==
dependencies:
"@aws-sdk/middleware-sdk-s3" "^3.972.24"
"@aws-sdk/types" "^3.973.6"
"@smithy/protocol-http" "^5.3.12"
"@smithy/signature-v4" "^5.3.12"
"@smithy/types" "^4.13.1"
tslib "^2.6.2"
"@aws-sdk/signature-v4-multi-region@^3.996.9": "@aws-sdk/signature-v4-multi-region@^3.996.9":
version "3.996.9" version "3.996.9"
resolved "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.9.tgz" resolved "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.9.tgz"
@ -539,6 +604,16 @@
"@smithy/util-endpoints" "^3.3.3" "@smithy/util-endpoints" "^3.3.3"
tslib "^2.6.2" tslib "^2.6.2"
"@aws-sdk/util-format-url@^3.972.8":
version "3.972.8"
resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz#803273f72617edb16b4087bcff2e52d740a26250"
integrity sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==
dependencies:
"@aws-sdk/types" "^3.973.6"
"@smithy/querystring-builder" "^4.2.12"
"@smithy/types" "^4.13.1"
tslib "^2.6.2"
"@aws-sdk/util-locate-window@^3.0.0": "@aws-sdk/util-locate-window@^3.0.0":
version "3.965.5" version "3.965.5"
resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz" resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz"
@ -577,6 +652,15 @@
fast-xml-parser "5.5.6" fast-xml-parser "5.5.6"
tslib "^2.6.2" tslib "^2.6.2"
"@aws-sdk/xml-builder@^3.972.15":
version "3.972.15"
resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz#7cbc823f8eb11fa8c02d81a744892e41b1762619"
integrity sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==
dependencies:
"@smithy/types" "^4.13.1"
fast-xml-parser "5.5.8"
tslib "^2.6.2"
"@aws/lambda-invoke-store@^0.2.2": "@aws/lambda-invoke-store@^0.2.2":
version "0.2.4" version "0.2.4"
resolved "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz" resolved "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz"
@ -1121,6 +1205,20 @@
"@smithy/util-middleware" "^4.2.12" "@smithy/util-middleware" "^4.2.12"
tslib "^2.6.2" tslib "^2.6.2"
"@smithy/middleware-endpoint@^4.4.27":
version "4.4.27"
resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz#cf2b334f7fc302e7ebf3fe00c1a1279ee9214afd"
integrity sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==
dependencies:
"@smithy/core" "^3.23.12"
"@smithy/middleware-serde" "^4.2.15"
"@smithy/node-config-provider" "^4.3.12"
"@smithy/shared-ini-file-loader" "^4.4.7"
"@smithy/types" "^4.13.1"
"@smithy/url-parser" "^4.2.12"
"@smithy/util-middleware" "^4.2.12"
tslib "^2.6.2"
"@smithy/middleware-retry@^4.4.43": "@smithy/middleware-retry@^4.4.43":
version "4.4.43" version "4.4.43"
resolved "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz" resolved "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz"
@ -1250,6 +1348,19 @@
"@smithy/util-stream" "^4.5.20" "@smithy/util-stream" "^4.5.20"
tslib "^2.6.2" tslib "^2.6.2"
"@smithy/smithy-client@^4.12.7":
version "4.12.7"
resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.12.7.tgz#3867272c062e39d3d4b719bf83ba491c76e1ee93"
integrity sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==
dependencies:
"@smithy/core" "^3.23.12"
"@smithy/middleware-endpoint" "^4.4.27"
"@smithy/middleware-stack" "^4.2.12"
"@smithy/protocol-http" "^5.3.12"
"@smithy/types" "^4.13.1"
"@smithy/util-stream" "^4.5.20"
tslib "^2.6.2"
"@smithy/types@^4.13.1": "@smithy/types@^4.13.1":
version "4.13.1" version "4.13.1"
resolved "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz" resolved "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz"
@ -1518,13 +1629,10 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: acorn@^8.9.0:
version "8.15.0" version "8.16.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
agent-base@^7.1.0, agent-base@^7.1.2: integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
version "7.1.4"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
agent-base@6: agent-base@6:
version "6.0.2" version "6.0.2"
@ -1533,8 +1641,15 @@ agent-base@6:
dependencies: dependencies:
debug "4" debug "4"
agent-base@^7.1.0, agent-base@^7.1.2:
version "7.1.4"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
ajv@^6.12.4: ajv@^6.12.4:
version "6.12.6" version "6.14.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a"
integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==
dependencies: dependencies:
fast-deep-equal "^3.1.1" fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0" fast-json-stable-stringify "^2.0.0"
@ -1813,14 +1928,7 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0" balanced-match "^1.0.0"
concat-map "0.0.1" concat-map "0.0.1"
brace-expansion@^2.0.1: brace-expansion@^2.0.1, brace-expansion@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz"
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
dependencies:
balanced-match "^1.0.0"
brace-expansion@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz"
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
@ -1936,22 +2044,7 @@ chalk@^4.0.0, chalk@^4.1.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chokidar@^3.5.2: chokidar@^3.5.2, chokidar@^3.5.3:
version "3.6.0"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
chokidar@^3.5.3:
version "3.6.0" version "3.6.0"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
@ -2006,6 +2099,11 @@ combined-stream@^1.0.8:
dependencies: dependencies:
delayed-stream "~1.0.0" delayed-stream "~1.0.0"
commander@6.2.0:
version "6.2.0"
resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz"
integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==
commander@^10.0.0: commander@^10.0.0:
version "10.0.1" version "10.0.1"
resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz"
@ -2016,11 +2114,6 @@ commander@^6.1.0:
resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz" resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
commander@6.2.0:
version "6.2.0"
resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz"
integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
@ -2044,7 +2137,7 @@ config-chain@^1.1.13:
ini "^1.3.4" ini "^1.3.4"
proto-list "~1.2.1" proto-list "~1.2.1"
content-disposition@^0.5.3, content-disposition@0.5.4: content-disposition@0.5.4, content-disposition@^0.5.3:
version "0.5.4" version "0.5.4"
resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
@ -2154,28 +2247,6 @@ dateformat@^4.6.3:
resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz" resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz"
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
debug@^3.2.7:
version "3.2.7"
dependencies:
ms "^2.1.1"
debug@^4, debug@^4.3.4, debug@^4.3.5, debug@4:
version "4.3.5"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz"
integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==
dependencies:
ms "2.1.2"
debug@^4.3.1:
version "4.4.3"
dependencies:
ms "^2.1.3"
debug@^4.3.2:
version "4.4.3"
dependencies:
ms "^2.1.3"
debug@2.6.9: debug@2.6.9:
version "2.6.9" version "2.6.9"
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
@ -2183,6 +2254,27 @@ debug@2.6.9:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@4, debug@^4, debug@^4.3.4, debug@^4.3.5:
version "4.3.5"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz"
integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==
dependencies:
ms "2.1.2"
debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
dependencies:
ms "^2.1.1"
debug@^4.3.1, debug@^4.3.2:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
ms "^2.1.3"
decamelize@^4.0.0: decamelize@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz"
@ -2226,16 +2318,16 @@ denque@^1.4.1:
resolved "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz" resolved "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz"
integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==
depd@^1.1.0:
version "1.1.2"
resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz"
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
depd@2.0.0: depd@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
depd@^1.1.0:
version "1.1.2"
resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz"
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
destroy@1.2.0: destroy@1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz"
@ -2246,18 +2338,20 @@ diff@^5.2.0:
resolved "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz" resolved "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz"
integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A== integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==
doctrine@^2.1.0: doctrine@3.0.0, doctrine@^3.0.0:
version "2.1.0"
dependencies:
esutils "^2.0.2"
doctrine@^3.0.0, doctrine@3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz"
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
dependencies:
esutils "^2.0.2"
dotenv@^16.4.0: dotenv@^16.4.0:
version "16.6.1" version "16.6.1"
resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz" resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz"
@ -2292,7 +2386,7 @@ eastasianwidth@^0.2.0:
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
ecdsa-sig-formatter@^1.0.11, ecdsa-sig-formatter@1.0.11: ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11:
version "1.0.11" version "1.0.11"
resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
@ -2583,7 +2677,7 @@ eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
"eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^8.57.0: eslint@^8.57.0:
version "8.57.1" version "8.57.1"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz"
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
@ -2683,7 +2777,7 @@ express-validator@^7.0.0:
lodash "^4.17.21" lodash "^4.17.21"
validator "~13.15.23" validator "~13.15.23"
"express@>=4.0.0 || >=5.0.0-beta", express@4.18.2: express@4.18.2:
version "4.18.2" version "4.18.2"
resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz"
integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
@ -2757,7 +2851,7 @@ fast-xml-builder@^1.1.4:
dependencies: dependencies:
path-expression-matcher "^1.1.3" path-expression-matcher "^1.1.3"
fast-xml-parser@^5.3.4, fast-xml-parser@5.5.6: fast-xml-parser@5.5.6, fast-xml-parser@^5.3.4:
version "5.5.6" version "5.5.6"
resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz" resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz"
integrity sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw== integrity sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==
@ -2766,6 +2860,15 @@ fast-xml-parser@^5.3.4, fast-xml-parser@5.5.6:
path-expression-matcher "^1.1.3" path-expression-matcher "^1.1.3"
strnum "^2.1.2" strnum "^2.1.2"
fast-xml-parser@5.5.8:
version "5.5.8"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz#929571ed8c5eb96e6d9bd572ba14fc4b84875716"
integrity sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==
dependencies:
fast-xml-builder "^1.1.4"
path-expression-matcher "^1.2.0"
strnum "^2.2.0"
fastq@^1.6.0: fastq@^1.6.0:
version "1.20.1" version "1.20.1"
resolved "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz" resolved "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz"
@ -2823,7 +2926,9 @@ flat@^5.0.2:
integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
flatted@^3.2.9: flatted@^3.2.9:
version "3.3.3" version "3.4.2"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726"
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
follow-redirects@^1.15.11: follow-redirects@^1.15.11:
version "1.15.11" version "1.15.11"
@ -2839,6 +2944,8 @@ for-each@^0.3.3:
for-each@^0.3.5: for-each@^0.3.5:
version "0.3.5" version "0.3.5"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==
dependencies: dependencies:
is-callable "^1.2.7" is-callable "^1.2.7"
@ -2883,7 +2990,7 @@ forwarded@0.2.0:
resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fresh@^0.5.2, fresh@0.5.2: fresh@0.5.2, fresh@^0.5.2:
version "0.5.2" version "0.5.2"
resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
@ -2903,6 +3010,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2: function-bind@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@ -2972,29 +3084,7 @@ get-caller-file@^2.0.5:
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4:
version "1.2.4"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz"
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
hasown "^2.0.0"
get-intrinsic@^1.2.1, get-intrinsic@^1.2.4:
version "1.2.4"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz"
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
hasown "^2.0.0"
get-intrinsic@^1.2.3:
version "1.2.4" version "1.2.4"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz"
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
@ -3049,6 +3139,8 @@ get-symbol-description@^1.1.0:
glob-parent@^6.0.2: glob-parent@^6.0.2:
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
dependencies: dependencies:
is-glob "^4.0.3" is-glob "^4.0.3"
@ -3059,6 +3151,18 @@ glob-parent@~5.1.2:
dependencies: dependencies:
is-glob "^4.0.1" is-glob "^4.0.1"
glob@7.1.6:
version "7.1.6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^10.4.2: glob@^10.4.2:
version "10.5.0" version "10.5.0"
resolved "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz" resolved "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz"
@ -3094,18 +3198,6 @@ glob@^8.1.0:
minimatch "^5.0.1" minimatch "^5.0.1"
once "^1.3.0" once "^1.3.0"
glob@7.1.6:
version "7.1.6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^13.19.0: globals@^13.19.0:
version "13.24.0" version "13.24.0"
resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz"
@ -3290,20 +3382,6 @@ https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1:
agent-base "^7.1.2" agent-base "^7.1.2"
debug "4" debug "4"
iconv-lite@^0.6.2:
version "0.6.3"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
iconv-lite@^0.6.3:
version "0.6.3"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
iconv-lite@0.4.24: iconv-lite@0.4.24:
version "0.4.24" version "0.4.24"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz"
@ -3311,6 +3389,13 @@ iconv-lite@0.4.24:
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3" safer-buffer ">= 2.1.2 < 3"
iconv-lite@^0.6.2, iconv-lite@^0.6.3:
version "0.6.3"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
ieee754@^1.2.1: ieee754@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
@ -3352,7 +3437,7 @@ inflight@^1.0.4:
once "^1.3.0" once "^1.3.0"
wrappy "1" wrappy "1"
inherits@^2.0.3, inherits@^2.0.4, inherits@2, inherits@2.0.4: inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -3464,6 +3549,8 @@ is-core-module@^2.13.0:
is-core-module@^2.16.1: is-core-module@^2.16.1:
version "2.16.1" version "2.16.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
dependencies: dependencies:
hasown "^2.0.2" hasown "^2.0.2"
@ -3647,7 +3734,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3:
dependencies: dependencies:
has-symbols "^1.0.2" has-symbols "^1.0.2"
is-symbol@^1.0.4: is-symbol@^1.0.4, is-symbol@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz" resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz"
integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==
@ -3656,13 +3743,6 @@ is-symbol@^1.0.4:
has-symbols "^1.1.0" has-symbols "^1.1.0"
safe-regex-test "^1.1.0" safe-regex-test "^1.1.0"
is-symbol@^1.1.1:
version "1.1.1"
dependencies:
call-bound "^1.0.2"
has-symbols "^1.1.0"
safe-regex-test "^1.1.0"
is-typed-array@^1.1.13: is-typed-array@^1.1.13:
version "1.1.13" version "1.1.13"
resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz" resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz"
@ -3986,16 +4066,16 @@ media-typer@0.3.0:
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
merge-descriptors@^1.0.1:
version "1.0.3"
resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz"
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
merge-descriptors@1.0.1: merge-descriptors@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
merge-descriptors@^1.0.1:
version "1.0.3"
resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz"
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
methods@^1.1.2, methods@~1.1.2: methods@^1.1.2, methods@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
@ -4013,7 +4093,7 @@ mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34:
dependencies: dependencies:
mime-db "1.52.0" mime-db "1.52.0"
mime@^1.3.4, mime@1.6.0: mime@1.6.0, mime@^1.3.4:
version "1.6.0" version "1.6.0"
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
@ -4044,14 +4124,7 @@ minimatch@^5.0.1, minimatch@^5.1.6:
dependencies: dependencies:
brace-expansion "^2.0.1" brace-expansion "^2.0.1"
minimatch@^9.0.1: minimatch@^9.0.1, minimatch@^9.0.4:
version "9.0.9"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz"
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
dependencies:
brace-expansion "^2.0.2"
minimatch@^9.0.4:
version "9.0.9" version "9.0.9"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz"
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
@ -4101,16 +4174,11 @@ moment-timezone@^0.5.43:
dependencies: dependencies:
moment "^2.29.4" moment "^2.29.4"
moment@^2.29.4, moment@2.30.1: moment@2.30.1, moment@^2.29.4:
version "2.30.1" version "2.30.1"
resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz"
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
ms@^2.1.1, ms@^2.1.3, ms@2.1.3:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
ms@2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
@ -4121,6 +4189,11 @@ ms@2.1.2:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@2.1.3, ms@^2.1.1, ms@^2.1.3:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
multer@^2.0.0: multer@^2.0.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz" resolved "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz"
@ -4337,11 +4410,6 @@ open@^8.0.0:
is-docker "^2.1.1" is-docker "^2.1.1"
is-wsl "^2.2.0" is-wsl "^2.2.0"
openapi-types@>=7:
version "12.1.3"
resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz"
integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==
optionator@^0.9.3: optionator@^0.9.3:
version "0.9.4" version "0.9.4"
resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz"
@ -4416,7 +4484,7 @@ passport-microsoft@^2.0.0:
dependencies: dependencies:
passport-oauth2 "1.8.0" passport-oauth2 "1.8.0"
passport-oauth2@^1.1.2, passport-oauth2@1.8.0: passport-oauth2@1.8.0, passport-oauth2@^1.1.2:
version "1.8.0" version "1.8.0"
resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz"
integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==
@ -4427,7 +4495,7 @@ passport-oauth2@^1.1.2, passport-oauth2@1.8.0:
uid2 "0.0.x" uid2 "0.0.x"
utils-merge "1.x.x" utils-merge "1.x.x"
passport-strategy@^1.0.0, passport-strategy@1.x.x: passport-strategy@1.x.x, passport-strategy@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz" resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz"
integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==
@ -4451,6 +4519,11 @@ path-expression-matcher@^1.1.3:
resolved "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz" resolved "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz"
integrity sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ== integrity sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==
path-expression-matcher@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz#9bdae3787f43b0857b0269e9caaa586c12c8abee"
integrity sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==
path-is-absolute@^1.0.0: path-is-absolute@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
@ -4527,7 +4600,7 @@ pg-types@2.2.0:
postgres-date "~1.0.4" postgres-date "~1.0.4"
postgres-interval "^1.1.0" postgres-interval "^1.1.0"
pg@^8.20.0, pg@>=8.0: pg@^8.20.0:
version "8.20.0" version "8.20.0"
resolved "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz" resolved "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz"
integrity sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA== integrity sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==
@ -4681,6 +4754,8 @@ pump@^3.0.0:
punycode@^2.1.0: punycode@^2.1.0:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
qs@6.11.0: qs@6.11.0:
version "6.11.0" version "6.11.0"
@ -4826,6 +4901,8 @@ resolve@^1.22.1:
resolve@^1.22.4: resolve@^1.22.4:
version "1.22.11" version "1.22.11"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262"
integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==
dependencies: dependencies:
is-core-module "^2.16.1" is-core-module "^2.16.1"
path-parse "^1.0.7" path-parse "^1.0.7"
@ -4890,7 +4967,7 @@ safe-array-concat@^1.1.3:
has-symbols "^1.1.0" has-symbols "^1.1.0"
isarray "^2.0.5" isarray "^2.0.5"
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
version "5.2.1" version "5.2.1"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -4946,12 +5023,7 @@ semver@^6.3.1:
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.5.3: semver@^7.5.3, semver@^7.5.4:
version "7.7.4"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz"
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
semver@^7.5.4:
version "7.7.4" version "7.7.4"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz" resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz"
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
@ -5003,7 +5075,7 @@ sequelize-pool@^7.1.0:
resolved "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz" resolved "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz"
integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg== integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==
sequelize@^6.37.0, "sequelize@>= 4": sequelize@^6.37.0:
version "6.37.8" version "6.37.8"
resolved "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz" resolved "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz"
integrity sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw== integrity sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==
@ -5214,13 +5286,6 @@ streamsearch@^1.1.0:
resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
string_decoder@^1.1.1, string_decoder@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3" version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
@ -5299,6 +5364,13 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1" define-properties "^1.2.1"
es-object-atoms "^1.0.0" es-object-atoms "^1.0.0"
string_decoder@^1.1.1, string_decoder@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1" version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
@ -5335,6 +5407,11 @@ strnum@^2.1.2:
resolved "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz" resolved "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz"
integrity sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg== integrity sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==
strnum@^2.2.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.2.2.tgz#f11fd94ab62b536ba2ecc615858f3747c2881b3f"
integrity sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==
stubs@^3.0.0: stubs@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz" resolved "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz"
@ -5644,7 +5721,7 @@ universalify@^2.0.0:
resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unpipe@~1.0.0, unpipe@1.0.0: unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
@ -5661,7 +5738,7 @@ util-deprecate@^1.0.1:
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
utils-merge@^1.0.1, utils-merge@1.0.1, utils-merge@1.x.x: utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
@ -5671,12 +5748,7 @@ uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2:
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0: uuid@^9.0.0, uuid@^9.0.1:
version "9.0.1"
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
uuid@^9.0.1:
version "9.0.1" version "9.0.1"
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
@ -5755,18 +5827,7 @@ which-collection@^1.0.2:
is-weakmap "^2.0.2" is-weakmap "^2.0.2"
is-weakset "^2.0.3" is-weakset "^2.0.3"
which-typed-array@^1.1.14: which-typed-array@^1.1.14, which-typed-array@^1.1.15:
version "1.1.15"
resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz"
integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==
dependencies:
available-typed-arrays "^1.0.7"
call-bind "^1.0.7"
for-each "^0.3.3"
gopd "^1.0.1"
has-tostringtag "^1.0.2"
which-typed-array@^1.1.15:
version "1.1.15" version "1.1.15"
resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz" resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz"
integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==

View File

@ -26,7 +26,12 @@ import { PRELOAD_CONFIG } from '../config/preload.config';
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator'; import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
import { useTransitionPlayback } from '../hooks/useTransitionPlayback'; import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
import { logger } from '../lib/logger'; import { logger } from '../lib/logger';
import { resolveAssetPlaybackUrl } from '../lib/assetUrl'; import {
resolveAssetPlaybackUrl,
markPresignedUrlFailed,
isRelativeStoragePath,
} from '../lib/assetUrl';
import { baseURLApi } from '../config';
import { buildElementStyle } from '../lib/elementStyles'; import { buildElementStyle } from '../lib/elementStyles';
import type { import type {
RuntimeProject, RuntimeProject,
@ -42,20 +47,90 @@ interface RuntimePresentationProps {
const getRows = (response: any) => const getRows = (response: any) =>
Array.isArray(response?.data?.rows) ? response.data.rows : []; Array.isArray(response?.data?.rows) ? response.data.rows : [];
/**
* Check if URL is a presigned S3 URL
*/
const isPresignedUrl = (url: string): boolean => {
return url.includes('X-Amz-Signature=') || url.includes('x-amz-signature=');
};
/**
* Build proxy URL from storage key
*/
const buildProxyUrl = (storageKey: string): string => {
const normalizedPath = storageKey.replace(/^\/+/, '');
return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPath)}`;
};
/**
* Load and decode a single image with presigned URL fallback
*/
const loadImageWithFallback = (
url: string,
storageKey?: string,
): Promise<void> => {
return new Promise((resolve) => {
const img = new window.Image();
const tryLoad = (srcUrl: string, isRetry = false) => {
img.src = srcUrl;
const handleSuccess = () => {
if (typeof img.decode === 'function') {
img
.decode()
.then(() => resolve())
.catch(() => resolve());
} else {
resolve();
}
};
const handleError = () => {
// If this was a presigned URL and we have a storage key, retry with proxy
if (!isRetry && isPresignedUrl(srcUrl) && storageKey) {
logger.info('Image presigned URL failed, retrying with proxy', {
storageKey: storageKey.slice(-50),
});
markPresignedUrlFailed(storageKey);
const proxyUrl = buildProxyUrl(storageKey);
tryLoad(proxyUrl, true);
} else {
// Give up and resolve anyway to not block navigation
resolve();
}
};
img.onload = handleSuccess;
img.onerror = handleError;
};
tryLoad(url);
});
};
/** /**
* Wait for all images on a page to be decoded before switching. * Wait for all images on a page to be decoded before switching.
* Handles presigned URL failures by retrying with proxy URLs.
*/ */
const waitForPageImages = async ( const waitForPageImages = async (
page: RuntimePage | null, page: RuntimePage | null,
timeoutMs = 2000, timeoutMs = 3000,
): Promise<void> => { ): Promise<void> => {
if (!page) return; if (!page) return;
const imageUrls: string[] = []; // Collect image URLs with their original storage keys for fallback
const imageEntries: Array<{ url: string; storageKey?: string }> = [];
if (page.background_image_url) { if (page.background_image_url) {
const url = resolveAssetPlaybackUrl(page.background_image_url); const storageKey = page.background_image_url;
if (url) imageUrls.push(url); const url = resolveAssetPlaybackUrl(storageKey);
if (url) {
imageEntries.push({
url,
storageKey: isRelativeStoragePath(storageKey) ? storageKey : undefined,
});
}
} }
try { try {
@ -77,7 +152,12 @@ const waitForPageImages = async (
const value = el[field]; const value = el[field];
if (typeof value === 'string' && value) { if (typeof value === 'string' && value) {
const url = resolveAssetPlaybackUrl(value); const url = resolveAssetPlaybackUrl(value);
if (url && !imageUrls.includes(url)) imageUrls.push(url); if (url && !imageEntries.some((e) => e.url === url)) {
imageEntries.push({
url,
storageKey: isRelativeStoragePath(value) ? value : undefined,
});
}
} }
}); });
@ -88,7 +168,14 @@ const waitForPageImages = async (
items.forEach((item: Record<string, unknown>) => { items.forEach((item: Record<string, unknown>) => {
if (typeof item.imageUrl === 'string' && item.imageUrl) { if (typeof item.imageUrl === 'string' && item.imageUrl) {
const url = resolveAssetPlaybackUrl(item.imageUrl); const url = resolveAssetPlaybackUrl(item.imageUrl);
if (url && !imageUrls.includes(url)) imageUrls.push(url); if (url && !imageEntries.some((e) => e.url === url)) {
imageEntries.push({
url,
storageKey: isRelativeStoragePath(item.imageUrl)
? item.imageUrl
: undefined,
});
}
} }
}); });
} }
@ -98,23 +185,10 @@ const waitForPageImages = async (
// Ignore parse errors // Ignore parse errors
} }
if (imageUrls.length === 0) return; if (imageEntries.length === 0) return;
const decodePromises = imageUrls.map( const decodePromises = imageEntries.map((entry) =>
(url) => loadImageWithFallback(entry.url, entry.storageKey),
new Promise<void>((resolve) => {
const img = new window.Image();
img.src = url;
if (typeof img.decode === 'function') {
img
.decode()
.then(() => resolve())
.catch(() => resolve());
} else {
img.onload = () => resolve();
img.onerror = () => resolve();
}
}),
); );
await Promise.race([ await Promise.race([

View File

@ -12,8 +12,31 @@ import { downloadEventBus } from '../lib/offline/DownloadEventBus';
import { StorageManager } from '../lib/offline/StorageManager'; import { StorageManager } from '../lib/offline/StorageManager';
import { PRELOAD_CONFIG } from '../config/preload.config'; import { PRELOAD_CONFIG } from '../config/preload.config';
import { OFFLINE_CONFIG } from '../config/offline.config'; import { OFFLINE_CONFIG } from '../config/offline.config';
import { resolveAssetPlaybackUrl } from '../lib/assetUrl'; import {
resolveAssetPlaybackUrl,
queuePresignedUrls,
isRelativeStoragePath,
markPresignedUrlFailed,
markPresignedUrlsVerified,
getPresignedUrl,
} from '../lib/assetUrl';
import { baseURLApi } from '../config';
import { logger } from '../lib/logger'; import { logger } from '../lib/logger';
/**
* Check if URL is a presigned S3 URL
*/
const isPresignedUrl = (url: string): boolean => {
return url.includes('X-Amz-Signature=') || url.includes('x-amz-signature=');
};
/**
* Build proxy URL from storage key
*/
const buildProxyUrl = (storageKey: string): string => {
const normalizedPath = storageKey.replace(/^\/+/, '');
return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPath)}`;
};
import type { import type {
PreloadPage, PreloadPage,
PreloadPageLink, PreloadPageLink,
@ -33,6 +56,7 @@ interface UsePreloadOrchestratorOptions {
interface PreloadQueueItem { interface PreloadQueueItem {
id: string; id: string;
url: string; url: string;
storageKey?: string; // Original storage key for presigned URL cache invalidation
priority: number; priority: number;
assetType: 'image' | 'video' | 'audio' | 'transition' | 'other'; assetType: 'image' | 'video' | 'audio' | 'transition' | 'other';
pageId: string; pageId: string;
@ -305,12 +329,46 @@ export function usePreloadOrchestrator(
url: item.url.slice(-50), url: item.url.slice(-50),
}); });
preloadedUrls.add(item.url); preloadedUrls.add(item.url);
// If this was a presigned URL, mark presigned URLs as verified
if (isPresignedUrl(item.url)) {
markPresignedUrlsVerified();
}
// Also mark proxy URL as preloaded if we have a storage key
if (item.storageKey) {
const proxyUrl = buildProxyUrl(item.storageKey);
preloadedUrls.add(proxyUrl);
}
}) })
.catch((err) => { .catch(async (err) => {
logger.error('[PRELOAD] Download failed', { logger.error('[PRELOAD] Download failed', {
url: item.url.slice(-50), url: item.url.slice(-50),
error: err?.message, error: err?.message,
}); });
// If presigned URL failed (e.g., CORS), retry with proxy URL
if (item.storageKey && isPresignedUrl(item.url)) {
markPresignedUrlFailed(item.storageKey);
const proxyUrl = buildProxyUrl(item.storageKey);
logger.info('[PRELOAD] Retrying with proxy URL', {
storageKey: item.storageKey.slice(-50),
proxyUrl: proxyUrl.slice(-60),
});
try {
await preloadWithProgress(proxyUrl, generateJobId(), item.id);
logger.info('[PRELOAD] Proxy download complete', {
url: proxyUrl.slice(-60),
});
preloadedUrls.add(proxyUrl);
// Also mark original presigned URL as preloaded (for cache lookup)
preloadedUrls.add(item.url);
} catch (retryErr) {
logger.error('[PRELOAD] Proxy download also failed', {
url: proxyUrl.slice(-60),
error: retryErr instanceof Error ? retryErr.message : 'unknown',
});
}
}
}) })
.finally(() => { .finally(() => {
activeDownloadsRef.current--; activeDownloadsRef.current--;
@ -453,72 +511,125 @@ export function usePreloadOrchestrator(
assets: assets.map((a) => ({ type: a.assetType, url: a.url.slice(-50) })), assets: assets.map((a) => ({ type: a.assetType, url: a.url.slice(-50) })),
}); });
// Add background assets from pages // Collect all raw storage paths that need presigning
const storagePaths: string[] = [];
const currentPage = pages.find((p) => p.id === currentPageId); const currentPage = pages.find((p) => p.id === currentPageId);
if (currentPage?.background_image_url) {
const resolvedUrl = resolveAssetPlaybackUrl( if (currentPage?.background_image_url && isRelativeStoragePath(currentPage.background_image_url)) {
currentPage.background_image_url, storagePaths.push(currentPage.background_image_url);
);
if (resolvedUrl) {
addToQueue({
id: `bg-img-${currentPageId}`,
url: resolvedUrl,
priority: PRELOAD_CONFIG.priority.currentPage + 200,
assetType: 'image',
pageId: currentPageId,
});
}
} }
if (currentPage?.background_video_url) { if (currentPage?.background_video_url && isRelativeStoragePath(currentPage.background_video_url)) {
const resolvedUrl = resolveAssetPlaybackUrl( storagePaths.push(currentPage.background_video_url);
currentPage.background_video_url,
);
if (resolvedUrl) {
addToQueue({
id: `bg-vid-${currentPageId}`,
url: resolvedUrl,
priority: PRELOAD_CONFIG.priority.currentPage + 150,
assetType: 'video',
pageId: currentPageId,
});
}
} }
// Add element assets
assets.forEach((asset) => { assets.forEach((asset) => {
const resolvedUrl = resolveAssetPlaybackUrl(asset.url); if (isRelativeStoragePath(asset.url)) {
if (resolvedUrl) { storagePaths.push(asset.url);
addToQueue({
id: generateJobId(),
url: resolvedUrl,
priority: asset.priority,
assetType: asset.assetType,
pageId: asset.pageId,
});
} }
}); });
// If aggressive preloading, also preload neighbor backgrounds
if (shouldPreloadAggressively) { if (shouldPreloadAggressively) {
const neighbors = neighborGraph.getNeighbors(currentPageId, 1); const neighbors = neighborGraph.getNeighbors(currentPageId, 1);
neighbors.forEach(({ pageId }) => { neighbors.forEach(({ pageId }) => {
const page = pages.find((p) => p.id === pageId); const page = pages.find((p) => p.id === pageId);
if (page?.background_image_url) { if (page?.background_image_url && isRelativeStoragePath(page.background_image_url)) {
const resolvedUrl = resolveAssetPlaybackUrl( storagePaths.push(page.background_image_url);
page.background_image_url,
);
if (resolvedUrl) {
addToQueue({
id: `bg-img-${pageId}`,
url: resolvedUrl,
priority: PRELOAD_CONFIG.priority.neighborBase,
assetType: 'image',
pageId,
});
}
} }
}); });
} }
// Batch fetch presigned URLs, then add to queue
const addAssetsToQueue = () => {
// Add background assets from current page
if (currentPage?.background_image_url) {
const storageKey = currentPage.background_image_url;
const resolvedUrl = resolveAssetPlaybackUrl(storageKey);
if (resolvedUrl) {
addToQueue({
id: `bg-img-${currentPageId}`,
url: resolvedUrl,
storageKey: isRelativeStoragePath(storageKey) ? storageKey : undefined,
priority: PRELOAD_CONFIG.priority.currentPage + 200,
assetType: 'image',
pageId: currentPageId,
});
}
}
if (currentPage?.background_video_url) {
const storageKey = currentPage.background_video_url;
const resolvedUrl = resolveAssetPlaybackUrl(storageKey);
if (resolvedUrl) {
addToQueue({
id: `bg-vid-${currentPageId}`,
url: resolvedUrl,
storageKey: isRelativeStoragePath(storageKey) ? storageKey : undefined,
priority: PRELOAD_CONFIG.priority.currentPage + 150,
assetType: 'video',
pageId: currentPageId,
});
}
}
// Add element assets
assets.forEach((asset) => {
const storageKey = asset.url;
const resolvedUrl = resolveAssetPlaybackUrl(storageKey);
if (resolvedUrl) {
addToQueue({
id: generateJobId(),
url: resolvedUrl,
storageKey: isRelativeStoragePath(storageKey) ? storageKey : undefined,
priority: asset.priority,
assetType: asset.assetType,
pageId: asset.pageId,
});
}
});
// If aggressive preloading, also preload neighbor backgrounds
if (shouldPreloadAggressively) {
const neighbors = neighborGraph.getNeighbors(currentPageId, 1);
neighbors.forEach(({ pageId }) => {
const page = pages.find((p) => p.id === pageId);
if (page?.background_image_url) {
const storageKey = page.background_image_url;
const resolvedUrl = resolveAssetPlaybackUrl(storageKey);
if (resolvedUrl) {
addToQueue({
id: `bg-img-${pageId}`,
url: resolvedUrl,
storageKey: isRelativeStoragePath(storageKey) ? storageKey : undefined,
priority: PRELOAD_CONFIG.priority.neighborBase,
assetType: 'image',
pageId,
});
}
}
});
}
};
// If there are storage paths to presign, fetch them first
if (storagePaths.length > 0) {
logger.info('[PRELOAD] Fetching presigned URLs', {
count: storagePaths.length,
});
queuePresignedUrls(storagePaths)
.then(() => {
logger.info('[PRELOAD] Presigned URLs fetched, adding to queue');
addAssetsToQueue();
})
.catch((error) => {
logger.error('[PRELOAD] Failed to fetch presigned URLs, falling back to proxy', {
error: error?.message,
});
// Fallback: add to queue without presigned URLs (will use backend proxy)
addAssetsToQueue();
});
} else {
// No storage paths to presign, add directly to queue
addAssetsToQueue();
}
}, [ }, [
enabled, enabled,
currentPageId, currentPageId,

View File

@ -8,6 +8,8 @@ import {
} from 'react'; } from 'react';
import axios from 'axios'; import axios from 'axios';
import { logger } from '../lib/logger'; import { logger } from '../lib/logger';
import { markPresignedUrlFailed, isRelativeStoragePath } from '../lib/assetUrl';
import { baseURLApi } from '../config';
import { useReversePlayback } from './useReversePlayback'; import { useReversePlayback } from './useReversePlayback';
export type ReverseMode = 'none' | 'reverse' | 'separate'; export type ReverseMode = 'none' | 'reverse' | 'separate';
@ -100,6 +102,40 @@ function buildBlobRequestUrl(url: string): string {
return url; return url;
} }
/**
* Check if a URL is a presigned S3 URL (contains X-Amz-Signature)
*/
function isPresignedUrl(url: string): boolean {
return url.includes('X-Amz-Signature=') || url.includes('x-amz-signature=');
}
/**
* Convert a presigned URL back to proxy URL
* Extracts the storage key from the S3 path and builds a proxy URL
*/
function getProxyUrlFallback(presignedUrl: string, originalStorageKey?: string): string | null {
// If we have the original storage key, use it directly
if (originalStorageKey && isRelativeStoragePath(originalStorageKey)) {
const normalizedPath = originalStorageKey.replace(/^\/+/, '');
return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPath)}`;
}
// Try to extract path from presigned URL
try {
const url = new URL(presignedUrl);
// S3 path format: /bucket-prefix/assets/project-id/filename.ext
const pathParts = url.pathname.split('/').filter(Boolean);
// Skip the bucket prefix, take the rest as the storage path
if (pathParts.length >= 2) {
const storagePath = pathParts.slice(1).join('/');
return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(storagePath)}`;
}
} catch {
// URL parsing failed
}
return null;
}
async function waitForImages(urls: string[], timeoutMs = 2000): Promise<void> { async function waitForImages(urls: string[], timeoutMs = 2000): Promise<void> {
if (urls.length === 0) return; if (urls.length === 0) return;
@ -154,6 +190,8 @@ export function useTransitionPlayback(
const activeSourceUrlRef = useRef<string | null>(null); const activeSourceUrlRef = useRef<string | null>(null);
const lastLoadedBlobUrlRef = useRef<string | null>(null); const lastLoadedBlobUrlRef = useRef<string | null>(null);
const lastLoadedSourceUrlRef = useRef<string | null>(null); const lastLoadedSourceUrlRef = useRef<string | null>(null);
const didTryFallbackRef = useRef(false);
const currentPlayableUrlRef = useRef<string | null>(null);
const startWatchdogTimerRef = useRef<ReturnType<typeof setTimeout> | null>( const startWatchdogTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
null, null,
); );
@ -315,6 +353,8 @@ export function useTransitionPlayback(
activeSourceUrlRef.current = sourceUrl; activeSourceUrlRef.current = sourceUrl;
didFinishRef.current = false; didFinishRef.current = false;
didStartPlaybackRef.current = false; didStartPlaybackRef.current = false;
didTryFallbackRef.current = false;
currentPlayableUrlRef.current = null;
setPhase('preparing'); setPhase('preparing');
const isReverseMode = currentTransition.reverseMode === 'reverse'; const isReverseMode = currentTransition.reverseMode === 'reverse';
@ -469,6 +509,7 @@ export function useTransitionPlayback(
video.currentTime = 0; video.currentTime = 0;
video.load(); video.load();
lastLoadedSourceUrlRef.current = playableSourceUrl; lastLoadedSourceUrlRef.current = playableSourceUrl;
currentPlayableUrlRef.current = playableSourceUrl;
attemptPlay(); attemptPlay();
@ -568,9 +609,38 @@ export function useTransitionPlayback(
const onEnded = () => finishPlayback('ended'); const onEnded = () => finishPlayback('ended');
const onVideoError = () => { const onVideoError = async () => {
if (didFinishRef.current) return; if (didFinishRef.current) return;
logIssue('video-error'); logIssue('video-error');
// Check if this is a presigned URL failure (likely CORS)
const currentUrl = currentPlayableUrlRef.current;
if (currentUrl && isPresignedUrl(currentUrl) && !didTryFallbackRef.current) {
logger.info('Presigned URL failed, trying proxy fallback', {
url: currentUrl.slice(0, 80),
});
// Mark presigned URL as failed so future resolves use proxy
// Extract storage key from the original transition videoUrl
const originalVideoUrl = currentTransition.videoUrl;
if (originalVideoUrl && isRelativeStoragePath(originalVideoUrl)) {
markPresignedUrlFailed(originalVideoUrl);
}
// Get proxy fallback URL
const fallbackUrl = getProxyUrlFallback(currentUrl, currentTransition.videoUrl);
if (fallbackUrl) {
didTryFallbackRef.current = true;
video.pause();
video.src = fallbackUrl;
currentPlayableUrlRef.current = fallbackUrl;
video.currentTime = 0;
video.load();
attemptPlay();
return;
}
}
handleError('video-error'); handleError('video-error');
}; };

View File

@ -2,10 +2,292 @@
* Asset URL Resolution Utility * Asset URL Resolution Utility
* *
* Resolves relative asset paths to absolute backend URLs for playback and preloading. * Resolves relative asset paths to absolute backend URLs for playback and preloading.
* Supports presigned S3 URLs for direct, fast asset downloads.
*/ */
import axios, { AxiosError } from 'axios';
import { baseURLApi } from '../config'; import { baseURLApi } from '../config';
/**
* Check if a URL is a presigned S3 URL
*/
const isPresignedS3Url = (url: string): boolean => {
return url.includes('X-Amz-Signature=') || url.includes('x-amz-signature=');
};
/**
* Setup Axios interceptor to detect presigned URL failures.
* Called once during app initialization.
*/
export const setupPresignedUrlInterceptor = (): void => {
axios.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
const url = error.config?.url || '';
// Check if this is a presigned S3 URL failure (likely CORS)
if (isPresignedS3Url(url) && !presignedUrlsDisabled) {
console.info('[assetUrl] Presigned URL request failed, disabling presigned URLs');
disablePresignedUrls();
}
return Promise.reject(error);
}
);
};
/**
* Disable presigned URLs globally.
* Called when we detect S3 CORS is not configured.
*/
export const disablePresignedUrls = (): void => {
if (!presignedUrlsDisabled) {
presignedUrlsDisabled = true;
presignedUrlCache.clear();
console.info('[assetUrl] Presigned URLs disabled - all requests will use proxy');
}
};
interface PresignedUrlCache {
url: string;
expiresAt: number;
}
// In-memory cache for presigned URLs
const presignedUrlCache = new Map<string, PresignedUrlCache>();
const CACHE_BUFFER_MS = 5 * 60 * 1000; // Refresh 5 min before expiry
const PRESIGN_TTL_MS = 60 * 60 * 1000; // 1 hour TTL
// Global flag to disable presigned URLs when CORS is not configured
// Once set to true, all URL resolutions will use proxy URLs
let presignedUrlsDisabled = false;
// Flag to track if presigned URLs have been verified to work
// Until verified, resolveAssetPlaybackUrl will use proxy URLs
let presignedUrlsVerified = false;
// Batch queue for presigned URL requests
let pendingBatch: string[] = [];
let batchPromiseResolvers: Array<{
resolve: (value: Record<string, string>) => void;
reject: (reason?: unknown) => void;
}> = [];
let batchTimeout: ReturnType<typeof setTimeout> | null = null;
const BATCH_DELAY_MS = 10; // Small delay to batch concurrent requests
/**
* Fetch presigned URLs for a batch of storage keys
*/
const fetchPresignedUrlsBatch = async (
urls: string[],
): Promise<Record<string, string>> => {
const response = await axios.post<{ presignedUrls: Record<string, string> }>(
`${baseURLApi}/file/presign`,
{ urls },
);
// Cache the results
const now = Date.now();
Object.entries(response.data.presignedUrls).forEach(([key, url]) => {
presignedUrlCache.set(key, {
url,
expiresAt: now + PRESIGN_TTL_MS,
});
});
return response.data.presignedUrls;
};
/**
* Process the pending batch of URLs
*/
const processBatch = async (): Promise<void> => {
const urls = Array.from(new Set(pendingBatch)); // Deduplicate
const resolvers = [...batchPromiseResolvers];
pendingBatch = [];
batchPromiseResolvers = [];
batchTimeout = null;
if (urls.length === 0) {
resolvers.forEach(({ resolve }) => resolve({}));
return;
}
try {
const result = await fetchPresignedUrlsBatch(urls);
resolvers.forEach(({ resolve }) => resolve(result));
} catch (error) {
resolvers.forEach(({ reject }) => reject(error));
}
};
/**
* Queue a URL for batch presigning and return a promise that resolves when the batch completes.
* Used by the preloader to efficiently fetch presigned URLs for multiple assets.
*/
export const queuePresignedUrl = (storageKey: string): Promise<string | null> => {
// Check cache first
const cached = presignedUrlCache.get(storageKey);
if (cached && cached.expiresAt > Date.now() + CACHE_BUFFER_MS) {
return Promise.resolve(cached.url);
}
// Add to pending batch
if (!pendingBatch.includes(storageKey)) {
pendingBatch.push(storageKey);
}
// Return a promise that will be resolved when the batch is processed
return new Promise((resolve, reject) => {
batchPromiseResolvers.push({
resolve: (result) => resolve(result[storageKey] || null),
reject,
});
// Schedule batch processing if not already scheduled
if (!batchTimeout) {
batchTimeout = setTimeout(processBatch, BATCH_DELAY_MS);
}
});
};
/**
* Queue multiple URLs for batch presigning.
* More efficient than calling queuePresignedUrl multiple times.
* Returns empty object if presigned URLs are disabled.
*/
export const queuePresignedUrls = (storageKeys: string[]): Promise<Record<string, string>> => {
// Skip if presigned URLs are disabled
if (presignedUrlsDisabled) {
return Promise.resolve({});
}
// Filter out already cached URLs
const uncachedKeys = storageKeys.filter((key) => {
const cached = presignedUrlCache.get(key);
return !cached || cached.expiresAt <= Date.now() + CACHE_BUFFER_MS;
});
if (uncachedKeys.length === 0) {
// All URLs are cached, return from cache
const result: Record<string, string> = {};
storageKeys.forEach((key) => {
const cached = presignedUrlCache.get(key);
if (cached) {
result[key] = cached.url;
}
});
return Promise.resolve(result);
}
// Add uncached keys to pending batch
uncachedKeys.forEach((key) => {
if (!pendingBatch.includes(key)) {
pendingBatch.push(key);
}
});
// Return a promise that will be resolved when the batch is processed
return new Promise((resolve, reject) => {
batchPromiseResolvers.push({
resolve: (result) => {
// Include cached URLs in the result
const finalResult: Record<string, string> = {};
storageKeys.forEach((key) => {
if (result[key]) {
finalResult[key] = result[key];
} else {
const cached = presignedUrlCache.get(key);
if (cached) {
finalResult[key] = cached.url;
}
}
});
resolve(finalResult);
},
reject,
});
// Schedule batch processing if not already scheduled
if (!batchTimeout) {
batchTimeout = setTimeout(processBatch, BATCH_DELAY_MS);
}
});
};
/**
* Flush the pending batch immediately and fetch presigned URLs.
* Call this before starting asset preloading to ensure all URLs are ready.
*/
export const flushPresignedUrlQueue = async (): Promise<void> => {
if (batchTimeout) {
clearTimeout(batchTimeout);
batchTimeout = null;
}
await processBatch();
};
/**
* Get a presigned URL from cache (synchronous).
* Returns null if not cached, expired, disabled, or not yet verified.
*/
export const getPresignedUrl = (storageKey: string): string | null => {
// Return null if presigned URLs are disabled or not verified yet
if (presignedUrlsDisabled || !presignedUrlsVerified) {
return null;
}
const cached = presignedUrlCache.get(storageKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.url;
}
return null;
};
/**
* Check if presigned URLs are currently disabled.
*/
export const arePresignedUrlsDisabled = (): boolean => {
return presignedUrlsDisabled;
};
/**
* Mark presigned URLs as verified (they work).
* Called by preloader after a successful presigned URL fetch.
*/
export const markPresignedUrlsVerified = (): void => {
if (!presignedUrlsDisabled && !presignedUrlsVerified) {
presignedUrlsVerified = true;
console.info('[assetUrl] Presigned URLs verified - enabling direct S3 access');
}
};
/**
* Mark a presigned URL as failed (removes from cache).
* Call this when a fetch using the presigned URL fails (e.g., CORS error).
* This allows resolveAssetPlaybackUrl to fall back to the proxy URL.
* Also disables presigned URLs globally for this session.
*/
export const markPresignedUrlFailed = (storageKey: string): void => {
presignedUrlCache.delete(storageKey);
disablePresignedUrls();
};
/**
* Check if a path is a relative storage path (not a full URL or special protocol)
*/
export const isRelativeStoragePath = (url: string): boolean => {
const normalized = url.trim();
return (
normalized.length > 0 &&
!normalized.startsWith('http://') &&
!normalized.startsWith('https://') &&
!normalized.startsWith('data:') &&
!normalized.startsWith('blob:') &&
!normalized.startsWith('/api/file/download') &&
!normalized.startsWith('/file/download')
);
};
/** /**
* Resolves an asset path to its full playback URL. * Resolves an asset path to its full playback URL.
* *
@ -14,7 +296,7 @@ import { baseURLApi } from '../config';
* - /api/file/download URLs (passthrough) * - /api/file/download URLs (passthrough)
* - /file/download URLs (prepend baseURLApi) * - /file/download URLs (prepend baseURLApi)
* - Full http/https URLs (passthrough) * - Full http/https URLs (passthrough)
* - Relative paths (convert to /api/file/download?privateUrl=...) * - Relative paths (check presigned cache, fallback to /api/file/download?privateUrl=...)
* *
* @param value - The asset URL or path to resolve * @param value - The asset URL or path to resolve
* @returns The resolved full URL, or empty string if no value * @returns The resolved full URL, or empty string if no value
@ -38,7 +320,21 @@ export const resolveAssetPlaybackUrl = (value?: string): string => {
if (normalized.startsWith('http://') || normalized.startsWith('https://')) if (normalized.startsWith('http://') || normalized.startsWith('https://'))
return normalized; return normalized;
// Relative path - convert to API download URL // Relative path - check presigned URL cache first
const presigned = getPresignedUrl(normalized);
if (presigned) {
return presigned;
}
// Fallback to backend proxy
const normalizedPrivateUrl = normalized.replace(/^\/+/, ''); const normalizedPrivateUrl = normalized.replace(/^\/+/, '');
return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPrivateUrl)}`; return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPrivateUrl)}`;
}; };
/**
* Clear the presigned URL cache.
* Useful when user logs out or storage provider changes.
*/
export const clearPresignedUrlCache = (): void => {
presignedUrlCache.clear();
};

View File

@ -23,6 +23,14 @@ import {
} from '../stores/introSteps'; } from '../stores/introSteps';
import { DownloadProvider } from '../context/DownloadContext'; import { DownloadProvider } from '../context/DownloadContext';
import { logger } from '../lib/logger'; import { logger } from '../lib/logger';
import { disablePresignedUrls } from '../lib/assetUrl';
/**
* Check if a URL is a presigned S3 URL
*/
const isPresignedS3Url = (url: string): boolean => {
return url.includes('X-Amz-Signature=') || url.includes('x-amz-signature=');
};
// Initialize axios // Initialize axios
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
@ -48,7 +56,7 @@ axios.interceptors.request.use(
(error) => Promise.reject(error), (error) => Promise.reject(error),
); );
// Set up axios response interceptor to handle 401 errors and redirect to login // Set up axios response interceptor to handle 401 errors and presigned URL failures
axios.interceptors.response.use( axios.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error) => {
@ -59,6 +67,15 @@ axios.interceptors.response.use(
requestUrl.includes('/auth/signin/local') || requestUrl.includes('/auth/signin/local') ||
requestUrl.includes('auth/signin/local'); requestUrl.includes('auth/signin/local');
// Detect presigned S3 URL failures (CORS not configured)
// Network errors (status 0) or CORS errors typically indicate S3 CORS issues
if (isPresignedS3Url(requestUrl) && (!status || status === 0 || error.message?.includes('Network Error'))) {
logger.info('[axios] Presigned URL failed, disabling presigned URLs', {
url: requestUrl.slice(0, 80),
});
disablePresignedUrls();
}
if (status === 401 && !isLoginRequest) { if (status === 401 && !isLoginRequest) {
// Clear stored tokens // Clear stored tokens
sessionStorage.removeItem('token'); sessionStorage.removeItem('token');