From 84ef295451e5e8333bf158711b7ac7a43fc8a277 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:34:53 +0000 Subject: [PATCH 1/9] Initial plan From 80626461ae146fc3a7c8fbd86c00ed281f933fc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:45:12 +0000 Subject: [PATCH 2/9] initial plan: eliminate redundant project data queries Co-authored-by: thehabes <3287006+thehabes@users.noreply.github.com> --- package-lock.json | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index f369b697..8f34cb47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1483,9 +1483,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1500,9 +1497,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1517,9 +1511,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1534,9 +1525,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1551,9 +1539,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1568,9 +1553,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1585,9 +1567,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1602,9 +1581,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ From 2f6f4dc476a9b616ab93c2d022f481cef4633f4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:50:27 +0000 Subject: [PATCH 3/9] eliminate redundant project data queries across route handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add optional `project` param to `findPageById()` and `findLayerById()` in shared.js so callers with a pre-loaded project can skip the redundant DB fetch - Pass pre-loaded `project` to `findLayerById()` and `findPageById()` in layer/index.js - Pass pre-loaded `project` to `findPageById()` in page/index.js PUT /:pageId - Pass pre-loaded `project` to `findPageById()` in line/index.js (POST, PUT, PATCH×2) - Use `project.data` directly in customMetadataRouter.js GET/POST/PUT /:id/custom instead of calling `database.findOne()` again after `checkUserAccess()` already loaded it - Restructure projectReadRouter.js GET /:id to call `loadAsUser()` once and derive the access check from its result via new `userHasAccess()` helper (saves project+group load) - Remove redundant `loadAsUser()` call in projectReadRouter.js GET /:id/manifest; use `project.data` from `checkUserAccess()` for the existence check instead - Add optional `preloadedProjectData` param to `ProjectFactory.exportManifest()` so callers with already-loaded data skip the internal `loadAsUser()` fetch Co-authored-by: thehabes <3287006+thehabes@users.noreply.github.com> --- classes/Project/ProjectFactory.js | 4 +-- layer/index.js | 4 +-- line/index.js | 8 +++--- page/index.js | 2 +- project/customMetadataRouter.js | 11 ++++---- project/projectReadRouter.js | 43 ++++++++++++++++++++++++------- utilities/shared.js | 8 +++--- 7 files changed, 52 insertions(+), 28 deletions(-) diff --git a/classes/Project/ProjectFactory.js b/classes/Project/ProjectFactory.js index 9e6b9213..d572c6cf 100644 --- a/classes/Project/ProjectFactory.js +++ b/classes/Project/ProjectFactory.js @@ -736,12 +736,12 @@ export default class ProjectFactory { * - A dynamically fetched list of manifest items, including canvases and their annotations. * - All elements are embedded in the manifest object. */ - static async exportManifest(projectId) { + static async exportManifest(projectId, preloadedProjectData = null) { if (!projectId) { throw { status: 400, message: "No project ID provided" } } - const project = await ProjectFactory.loadAsUser(projectId, null) + const project = preloadedProjectData ?? await ProjectFactory.loadAsUser(projectId, null) const manifestJson = await this.fetchJson(project.manifest[0]) const manifest = { diff --git a/layer/index.js b/layer/index.js index f91b666a..fef114f8 100644 --- a/layer/index.js +++ b/layer/index.js @@ -42,7 +42,7 @@ router.route('/:layerId') return respondWithError(res, 403, 'You do not have permission to update this layer') } if (!project?.data) return respondWithError(res, 404, `Project ${projectId} was not found`) - const layer = await findLayerById(layerId, projectId) + const layer = await findLayerById(layerId, projectId, project) // Only update top-level properties that are present in the request Object.keys(update ?? {}).forEach(key => { layer[key] = update[key] @@ -56,7 +56,7 @@ router.route('/:layerId') }) let pages = [] if (providedPages && Array.isArray(providedPages) && providedPages.length > 0) { - pages = await Promise.all(providedPages.map(p => findPageById(p.split("/").pop(), projectId) )) + pages = await Promise.all(providedPages.map(p => findPageById(p.split("/").pop(), projectId, project) )) layer.pages = pages } await updateLayerAndProject(layer, project, user._id) diff --git a/line/index.js b/line/index.js index 43b1ff35..724e54b4 100644 --- a/line/index.js +++ b/line/index.js @@ -56,7 +56,7 @@ router.post('/', auth0Middleware(), async (req, res) => { return respondWithError(res, 403, 'You do not have permission to create lines in this project') } if (!project?.data) return respondWithError(res, 404, `Project ${req.params.projectId} was not found`) - const page = await findPageById(req.params.pageId, req.params.projectId) + const page = await findPageById(req.params.pageId, req.params.projectId, project) if (!req.body || (Array.isArray(req.body) && req.body.length === 0)) { return respondWithError(res, 400, "Request body with line data is required") @@ -120,7 +120,7 @@ router.put('/:lineId', auth0Middleware(), screenContentMiddleware(), async (req, return respondWithError(res, 403, 'You do not have permission to update lines in this project') } if (!project?.data) return respondWithError(res, 404, `Project ${req.params.projectId} was not found`) - const page = await findPageById(req.params.pageId, req.params.projectId) + const page = await findPageById(req.params.pageId, req.params.projectId, project) let oldLine = page.items?.find(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop()) if (!oldLine) { return respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`) @@ -202,7 +202,7 @@ router.patch('/:lineId/text', auth0Middleware(), screenContentMiddleware(), asyn if (typeof req.body !== 'string') { return respondWithError(res, 400, 'Invalid request body. Expected a string.') } - const page = await findPageById(req.params.pageId, req.params.projectId) + const page = await findPageById(req.params.pageId, req.params.projectId, project) const oldLine = page.items?.find(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop()) if (!oldLine) { return respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`) @@ -282,7 +282,7 @@ router.patch('/:lineId/bounds', auth0Middleware(), async (req, res) => { return respondWithError(res, 400, 'Invalid request body. Expected an object with x, y, w, and h as non-negative integers.') } const bounds = { x: parseInt(req.body.x, 10), y: parseInt(req.body.y, 10), w: parseInt(req.body.w, 10), h: parseInt(req.body.h, 10) } - const page = await findPageById(req.params.pageId, req.params.projectId) + const page = await findPageById(req.params.pageId, req.params.projectId, project) const findOldLine = page.items?.find(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop()) if (!findOldLine) { return respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`) diff --git a/page/index.js b/page/index.js index 012e6de1..da2d61a9 100644 --- a/page/index.js +++ b/page/index.js @@ -91,7 +91,7 @@ router.route('/:pageId') try { if (hasSuspiciousPageData(req.body)) return respondWithError(res, 400, "Suspicious page data will not be processed.") // Find the page object - const page = await findPageById(pageId, projectId) + const page = await findPageById(pageId, projectId, project) page.creator = user.agent.split('/').pop() page.partOf = layerId diff --git a/project/customMetadataRouter.js b/project/customMetadataRouter.js index d232d351..3609bd8d 100644 --- a/project/customMetadataRouter.js +++ b/project/customMetadataRouter.js @@ -90,8 +90,7 @@ router.route("/:id/custom").get(auth0Middleware(), async (req, res) => { return respondWithError(res, 403, "You do not have permission to read this project's metadata") } - // Fetch the project from database - const projectData = await database.findOne({ _id: id }, "projects") + const projectData = project.data if (!projectData) { return respondWithError(res, 404, `No TPEN3 project with ID '${id}' found`) @@ -139,8 +138,8 @@ router.route("/:id/custom").post(auth0Middleware(), async (req, res) => { // Get namespace from request origin const namespace = getNamespaceFromOrigin(req) - // Fetch the full project data - const projectData = await database.findOne({ _id: id }, "projects") + // Use the already-loaded project data + const projectData = project.data if (!projectData) { return respondWithError(res, 404, `No TPEN3 project with ID '${id}' found`) @@ -195,8 +194,8 @@ router.route("/:id/custom").put(auth0Middleware(), async (req, res) => { // Get namespace from request origin const namespace = getNamespaceFromOrigin(req) - // Fetch the full project data - const projectData = await database.findOne({ _id: id }, "projects") + // Use the already-loaded project data + const projectData = project.data if (!projectData) { return respondWithError(res, 404, `No TPEN3 project with ID '${id}' found`) diff --git a/project/projectReadRouter.js b/project/projectReadRouter.js index 60cb9bf5..220436d1 100644 --- a/project/projectReadRouter.js +++ b/project/projectReadRouter.js @@ -92,6 +92,32 @@ function filterProjectInterfaces(project, namespaces) { } } +/** + * Check whether a user has the required access on a project loaded via ProjectFactory.loadAsUser(). + * This avoids a second database round-trip when the project data (with collaborators/roles) is + * already available from the loadAsUser aggregation result. + * + * @param {Object} projectData - The project object returned by ProjectFactory.loadAsUser() + * @param {string} userId - The user ID to check + * @param {string} action - Required action (e.g. ACTIONS.READ) + * @param {string} scope - Required scope (e.g. SCOPES.ALL) + * @param {string} entity - Required entity (e.g. ENTITIES.PROJECT) + * @returns {boolean} + */ +function userHasAccess(projectData, userId, action, scope, entity) { + const userRoleNames = projectData?.collaborators?.[userId]?.roles + if (!userRoleNames) return false + const rolePermissions = projectData.roles ?? {} + return userRoleNames.some(role => { + const perm = rolePermissions[role] + if (!perm) return false + const [permAction, permScope, permEntity] = perm.split("_") + return (permAction === action || permAction === "*") && + (permScope === scope || permScope === "*") && + (permEntity === entity || permEntity === "*") + }) +} + router.route("/:id/manifest").get(auth0Middleware(), async (req, res) => { const { id } = req.params @@ -100,14 +126,14 @@ router.route("/:id/manifest").get(auth0Middleware(), async (req, res) => { if (!id) return respondWithError(res, 400, "No TPEN3 ID provided") if (!validateID(id)) return respondWithError(res, 400, "The TPEN3 project ID provided is invalid") try { - if (!await new Project(id).checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.PROJECT)) { + const project = new Project(id) + if (!await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.PROJECT)) { return respondWithError(res, 403, "You do not have permission to export this project") } - const project = await ProjectFactory.loadAsUser(id, null) - if (!project) { + if (!project.data) { return respondWithError(res, 404, `No TPEN3 project with ID '${id}' found`) } - const manifest = await ProjectFactory.exportManifest(id) + const manifest = await ProjectFactory.exportManifest(id, project.data) await ProjectFactory.uploadFileToGitHub(manifest, `${id}`) res.status(200).json(manifest) } catch (error) { @@ -158,14 +184,13 @@ router.route("/:id").get(auth0Middleware(), async (req, res) => { if (!id) return respondWithError(res, 400, "No TPEN3 ID provided") if (!validateID(id)) return respondWithError(res, 400, "The TPEN3 project ID provided is invalid") try { - const project = new Project(id) - if (!(await project.checkUserAccess(user._id, ACTIONS.READ, SCOPES.ALL, ENTITIES.PROJECT))) { - return respondWithError(res, 403, "You do not have permission to view this project") - } const projectData = await ProjectFactory.loadAsUser(id, user._id) - if (!projectData) { + if (!projectData || projectData instanceof Error) { return respondWithError(res, 404, `No TPEN3 project with ID '${id}' found`) } + if (!userHasAccess(projectData, user._id, ACTIONS.READ, SCOPES.ALL, ENTITIES.PROJECT)) { + return respondWithError(res, 403, "You do not have permission to view this project") + } // Filter interfaces based on origin and query parameters const namespacesToInclude = getNamespacesToInclude(req) diff --git a/utilities/shared.js b/utilities/shared.js index e2ff2441..68a8a173 100644 --- a/utilities/shared.js +++ b/utilities/shared.js @@ -252,8 +252,8 @@ export const getLayerContainingPage = (project, pageId) => { } // Find a page by ID (moved from page/index.js) -export async function findPageById(pageId, projectId) { - const projectData = (await Project.getById(projectId))?.data +export async function findPageById(pageId, projectId, project = null) { + const projectData = project?.data ?? (await Project.getById(projectId))?.data if (!projectData) { const error = new Error(`Project ${projectId} was not found`) error.status = 404 @@ -279,8 +279,8 @@ export async function findPageById(pageId, projectId) { return new Page(layerContainingPage.id, page) } -export async function findLayerById(layerId, projectId) { - const p = await Project.getById(projectId) +export async function findLayerById(layerId, projectId, project = null) { + const p = project ?? await Project.getById(projectId) if (!p?.data) { const error = new Error(`Project ${projectId} was not found`) error.status = 404 From d17b50170b9e199bcbd040997fd935021c962447 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:52:38 +0000 Subject: [PATCH 4/9] address code review: add Array.isArray guard in userHasAccess, add comment on loadAsUser error pattern Co-authored-by: thehabes <3287006+thehabes@users.noreply.github.com> --- project/projectReadRouter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project/projectReadRouter.js b/project/projectReadRouter.js index 220436d1..0cfc5db4 100644 --- a/project/projectReadRouter.js +++ b/project/projectReadRouter.js @@ -106,7 +106,7 @@ function filterProjectInterfaces(project, namespaces) { */ function userHasAccess(projectData, userId, action, scope, entity) { const userRoleNames = projectData?.collaborators?.[userId]?.roles - if (!userRoleNames) return false + if (!userRoleNames || !Array.isArray(userRoleNames)) return false const rolePermissions = projectData.roles ?? {} return userRoleNames.some(role => { const perm = rolePermissions[role] @@ -184,6 +184,7 @@ router.route("/:id").get(auth0Middleware(), async (req, res) => { if (!id) return respondWithError(res, 400, "No TPEN3 ID provided") if (!validateID(id)) return respondWithError(res, 400, "The TPEN3 project ID provided is invalid") try { + // loadAsUser() returns an Error object (not throws) on DB failure; check both null and error cases const projectData = await ProjectFactory.loadAsUser(id, user._id) if (!projectData || projectData instanceof Error) { return respondWithError(res, 404, `No TPEN3 project with ID '${id}' found`) From e009dcccaf1a8d228bc7ef6256bf2951ef7d6f6c Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 16 Mar 2026 14:01:10 -0500 Subject: [PATCH 5/9] no package lock changes --- package-lock.json | 588 ---------------------------------------------- 1 file changed, 588 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f34cb47..11c13d2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,15 +13,12 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.6", "debug": "^4.4.3", - "dompurify": "^3.3.3", "dotenv": "^17.3.1", "express": "^5.2.1", "express-list-endpoints": "^7.1.1", "express-oauth2-jwt-bearer": "^1.7.4", "image-size": "^2.0.2", - "jsdom": "27.0.0", "mariadb": "^3.5.2", - "marked": "^17.0.4", "mime-types": "^3.0.2", "mongodb": "^7.1.0", "morgan": "^1.10.1", @@ -40,34 +37,6 @@ "npm": ">=11.0.0" } }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -564,22 +533,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.0.tgz", - "integrity": "sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0" - }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -1331,13 +1284,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1666,15 +1612,6 @@ "node": ">= 0.6" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1902,15 +1839,6 @@ "node": ">= 0.8" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2427,194 +2355,6 @@ "node": ">= 8" } }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/cssstyle": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", - "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^4.1.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.21", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cssstyle/node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/cssstyle/node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/cssstyle/node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/cssstyle/node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/cssstyle/node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/cssstyle/node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/cssstyle/node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/cssstyle/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/data-urls": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", - "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^15.1.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/data-urls/node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2632,12 +2372,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -2722,15 +2456,6 @@ "node": ">=0.3.1" } }, - "node_modules/dompurify": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", - "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -2806,18 +2531,6 @@ "node": ">= 0.8" } }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -3435,18 +3148,6 @@ "node": ">= 0.4" } }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3474,32 +3175,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3675,12 +3350,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "license": "MIT" - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -4429,45 +4098,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdom": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz", - "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/dom-selector": "^6.5.4", - "cssstyle": "^5.3.0", - "data-urls": "^6.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^7.3.0", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.0.0", - "ws": "^8.18.2", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4608,18 +4238,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, - "node_modules/marked": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", - "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4629,12 +4247,6 @@ "node": ">= 0.4" } }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "license": "CC0-1.0" - }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -5232,18 +4844,6 @@ "license": "MIT", "optional": true }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5516,15 +5116,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -5564,12 +5155,6 @@ "node": ">= 18" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "license": "MIT" - }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -5582,18 +5167,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5827,15 +5400,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -6125,12 +5689,6 @@ "license": "ISC", "optional": true }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "license": "MIT" - }, "node_modules/synckit": { "version": "0.11.12", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", @@ -6208,24 +5766,6 @@ "node": "*" } }, - "node_modules/tldts": { - "version": "7.0.25", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", - "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.25" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.25", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", - "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==", - "license": "MIT" - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6265,34 +5805,10 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/tpen3-services": { "resolved": "", "link": true }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6450,18 +5966,6 @@ "node": ">= 0.8" } }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -6472,62 +5976,6 @@ "makeerror": "1.0.12" } }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", - "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", - "license": "MIT", - "dependencies": { - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6659,42 +6107,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "license": "MIT" - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", From c87c9ef23da6a971ee62f1e126900e1f446ad5ef Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 16 Mar 2026 14:07:43 -0500 Subject: [PATCH 6/9] changes during review --- project/projectReadRouter.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/project/projectReadRouter.js b/project/projectReadRouter.js index 0cfc5db4..5000aa9e 100644 --- a/project/projectReadRouter.js +++ b/project/projectReadRouter.js @@ -109,12 +109,14 @@ function userHasAccess(projectData, userId, action, scope, entity) { if (!userRoleNames || !Array.isArray(userRoleNames)) return false const rolePermissions = projectData.roles ?? {} return userRoleNames.some(role => { - const perm = rolePermissions[role] - if (!perm) return false - const [permAction, permScope, permEntity] = perm.split("_") - return (permAction === action || permAction === "*") && - (permScope === scope || permScope === "*") && - (permEntity === entity || permEntity === "*") + const perms = rolePermissions[role] + if (!perms || !Array.isArray(perms)) return false + return perms.some(perm => { + const [permAction, permScope, permEntity] = perm.split("_") + return (permAction === action || permAction === "*") && + (permScope === scope || permScope === "*") && + (permEntity === entity || permEntity === "*") + }) }) } From 70442e7535ee706694f02898ab8c5f5f7ffc8f33 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 16 Mar 2026 14:48:17 -0500 Subject: [PATCH 7/9] changes during review --- classes/Project/ProjectFactory.js | 3 +++ project/projectReadRouter.js | 7 +++++-- utilities/shared.js | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/classes/Project/ProjectFactory.js b/classes/Project/ProjectFactory.js index d572c6cf..4741d820 100644 --- a/classes/Project/ProjectFactory.js +++ b/classes/Project/ProjectFactory.js @@ -742,6 +742,9 @@ export default class ProjectFactory { } const project = preloadedProjectData ?? await ProjectFactory.loadAsUser(projectId, null) + if (!project || project instanceof Error) { + throw { status: project?.status || 404, message: project?.message || `No project found with ID '${projectId}'` } + } const manifestJson = await this.fetchJson(project.manifest[0]) const manifest = { diff --git a/project/projectReadRouter.js b/project/projectReadRouter.js index 5000aa9e..afc2fc53 100644 --- a/project/projectReadRouter.js +++ b/project/projectReadRouter.js @@ -186,9 +186,12 @@ router.route("/:id").get(auth0Middleware(), async (req, res) => { if (!id) return respondWithError(res, 400, "No TPEN3 ID provided") if (!validateID(id)) return respondWithError(res, 400, "The TPEN3 project ID provided is invalid") try { - // loadAsUser() returns an Error object (not throws) on DB failure; check both null and error cases + // loadAsUser() returns an Error object (not throws) on DB failure const projectData = await ProjectFactory.loadAsUser(id, user._id) - if (!projectData || projectData instanceof Error) { + if (projectData instanceof Error) { + return respondWithError(res, projectData.status || 500, projectData.message ?? "An error occurred while fetching the project data.") + } + if (!projectData) { return respondWithError(res, 404, `No TPEN3 project with ID '${id}' found`) } if (!userHasAccess(projectData, user._id, ACTIONS.READ, SCOPES.ALL, ENTITIES.PROJECT)) { diff --git a/utilities/shared.js b/utilities/shared.js index 68a8a173..096182e2 100644 --- a/utilities/shared.js +++ b/utilities/shared.js @@ -280,7 +280,7 @@ export async function findPageById(pageId, projectId, project = null) { } export async function findLayerById(layerId, projectId, project = null) { - const p = project ?? await Project.getById(projectId) + const p = project?.data ? project : await Project.getById(projectId) if (!p?.data) { const error = new Error(`Project ${projectId} was not found`) error.status = 404 From 646dc415673b8d2d79ae24e74e8acca22da991e0 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 16 Mar 2026 16:05:10 -0500 Subject: [PATCH 8/9] Changes during review --- classes/Project/ProjectFactory.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/classes/Project/ProjectFactory.js b/classes/Project/ProjectFactory.js index 4741d820..ad7f2b71 100644 --- a/classes/Project/ProjectFactory.js +++ b/classes/Project/ProjectFactory.js @@ -729,6 +729,7 @@ export default class ProjectFactory { * manifest data is assembled, and the final JSON is saved to the filesystem. * * @param {string} projectId - Project ID for a specific project. + * @param {Object|null} preloadedProjectData - Pre-loaded project data to avoid a redundant DB query. Falls back to loadAsUser() if null. * @returns {Object} - Returns the assembled IIIF manifest object. * * The manifest follows the IIIF Presentation API 3.0 specification and includes: @@ -1006,7 +1007,7 @@ export default class ProjectFactory { }, { $set: { - roles: { $mergeObjects: [{ $ifNull: ['$thisGroup.customRoles', {}] }, Group.defaultRoles] }, + roles: { $mergeObjects: [Group.defaultRoles, { $ifNull: ['$thisGroup.customRoles', {}] }] }, } }, { From 307c1d83fad782395e9802776c06d24adde48d8a Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 16 Mar 2026 17:35:50 -0500 Subject: [PATCH 9/9] Changes during review --- project/projectReadRouter.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/project/projectReadRouter.js b/project/projectReadRouter.js index afc2fc53..e82a7d06 100644 --- a/project/projectReadRouter.js +++ b/project/projectReadRouter.js @@ -109,8 +109,11 @@ function userHasAccess(projectData, userId, action, scope, entity) { if (!userRoleNames || !Array.isArray(userRoleNames)) return false const rolePermissions = projectData.roles ?? {} return userRoleNames.some(role => { - const perms = rolePermissions[role] - if (!perms || !Array.isArray(perms)) return false + let perms = rolePermissions[role] + if (!perms) return false + // Custom roles may store permissions as a space-delimited string + if (typeof perms === 'string') perms = perms.split(' ') + if (!Array.isArray(perms)) return false return perms.some(perm => { const [permAction, permScope, permEntity] = perm.split("_") return (permAction === action || permAction === "*") &&