diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index 8d2b8fa1..ba498dce 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -6,202 +6,35 @@ This file provides guidance to AI Assistants when working with code in this repo
TPEN Services is a Node.js Express API service for TPEN3 (Transcription for Paleographical and Editorial Notation). This provides RESTful APIs for digital humanities, cultural heritage, annotation services, and IIIF manifest handling. The service supports multiple database backends (MongoDB, MariaDB) and uses Auth0 for authentication.
-Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
-
## Working Effectively
### Bootstrap, Build, and Test the Repository
-- Copy environment configuration: `cp .env.development .env`
+- Environment configuration is in .env. Do not overwrite the existing .env file. If an .env file does not exist then copy environment configuration: `cp .env.development .env`
- Install dependencies: `npm install` -- takes up to 20 seconds. NEVER CANCEL. Set timeout to 60+ seconds.
-- Run unit tests: `npm run unitTests` -- takes 12 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
-- Run existence tests: `npm run existsTests` -- takes 7 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
-- Run all tests: `npm run allTests` -- takes 12 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
### Run the Application
- ALWAYS run the bootstrapping steps first.
- Production server: `npm start` -- starts on port 3011
- Development server: `npm run dev` -- starts with nodemon auto-reload on port 3011
-- Test basic functionality: `curl http://localhost:3011/` should return "TPEN3 SERVICES BABY!!!"
-
-### Environment Requirements
-- Node.js >= 22.20.0
-- MongoDB (for database tests and full functionality)
-- MariaDB (for database tests and full functionality)
-- Copy `.env.development` to `.env` for basic functionality
-- For full functionality, configure database connection strings in `.env`
+- Test basic functionality: `curl http://localhost:3011/API.html` should return the API page.
## Validation
### Always Validate Core Functionality After Changes
-- Start the application: `npm start` or `npm run dev`
-- Test the root endpoint: `curl http://localhost:3011/` -- should return HTML with "TPEN3 SERVICES BABY!!!"
-- Run unit tests that don't require databases: `npm run unitTests` -- many tests pass without database connections
-- Run existence tests: `npm run existsTests` -- validates route registration and class imports
-- ALWAYS wait for full test completion. Tests may appear to hang but will complete within 12 seconds.
-- NOTE: Application may crash after serving initial requests due to database connection attempts - this is expected behavior without running MongoDB/MariaDB.
-
-### Test Categories Available
-- `npm run unitTests` -- Core unit tests (some require databases)
-- `npm run existsTests` -- Route and class existence validation (database-independent)
-- `npm run functionsTests` -- Function-level tests
-- `npm run E2Etests` -- End-to-end API tests
-- `npm run dbTests` -- Database-specific tests (require running databases)
-- `npm run authTest` -- Authentication tests (require Auth0 configuration)
+- Run `npm run allTests` once you have completed your task and need to verify correctness before continuing.
+- ALWAYS wait for full test completion. Tests can take minutes. NEVER CANCEL.
+- NOTE: Application may crash after serving initial requests due to database connection attempts - this is expected behavior without a connection to MongoDB.
### Expected Test Behavior
- Tests requiring databases will timeout/fail without MongoDB/MariaDB running
- Auth tests fail without proper AUDIENCE and DOMAIN environment variables
- Core functionality tests (exists, basic units) should pass with minimal `.env` setup
-- Database-independent tests complete in 6-15 seconds
-
-## Common Tasks
-
-### Repository Structure
-```
-/home/runner/work/TPEN-services/TPEN-services/
-├── app.js # Express application setup
-├── bin/tpen3_services.js # Server entry point
-├── package.json # Dependencies and scripts
-├── jest.config.js # Test configuration
-├── config.env # Safe defaults (committed)
-├── .env.development # Development template
-├── .env.production # Production template
-├── API.md # API documentation
-├── classes/ # Domain model classes
-│ ├── Project/ # Project management
-│ ├── User/ # User management
-│ ├── Group/ # Group management
-│ ├── Layer/ # Annotation layers
-│ ├── Line/ # Text line handling
-│ ├── Page/ # Page management
-│ └── Manifest/ # IIIF manifest handling
-├── database/ # Database drivers
-│ ├── mongo/ # MongoDB controller
-│ ├── maria/ # MariaDB controller
-│ └── tiny/ # TinyPEN API controller
-├── auth/ # Auth0 authentication
-├── project/ # Project API routes
-├── userProfile/ # User API routes
-├── line/ # Line API routes
-├── page/ # Page API routes
-└── utilities/ # Helper functions
-```
-
-### Key API Endpoints
-- `GET /` -- Service status (returns "TPEN3 SERVICES BABY!!!")
-- `GET /project/:id` -- Get project by ID (requires authentication)
-- `POST /project/create` -- Create new project (requires authentication)
-- `POST /project/import?createFrom=URL` -- Import project from IIIF manifest
-- `GET /user/:id` -- Get user profile (public)
-- `GET /my/profile` -- Get authenticated user profile
-- `GET /line/:id` -- Get text line annotation
-- `GET /page/:id` -- Get annotation page
-
-### Authentication
-- Uses Auth0 JWT bearer tokens
-- Protected endpoints require `Authorization: Bearer ` header
-- Environment variables AUDIENCE and DOMAIN must be configured for auth tests
-- Public endpoints: `/`, `/user/:id`
-- Protected endpoints: `/project/*`, `/my/*`, most POST/PUT/DELETE operations
-
-### Database Configuration
-- MongoDB: Configure MONGODB and MONGODBNAME in `.env`
-- MariaDB: Configure MARIADB, MARIADBNAME, MARIADBUSER, MARIADBPASSWORD in `.env`
-- TinyPEN API: Configure TINYPEN in `.env`
-- Default configurations in `config.env` point to localhost development services
### Development Workflow
-1. Always start with: `cp .env.development .env && npm install`
+1. Ensure .env exists (if not: `cp .env.development .env`) and run `npm install`
2. Make code changes
-3. Test with: `npm run existsTests` (fast, database-independent)
-4. For database changes: ensure MongoDB/MariaDB running, then `npm run dbTests`
-5. For API changes: `npm run E2Etests`
-6. Start dev server: `npm run dev`
-7. Test manually: `curl http://localhost:3011/` and relevant endpoints
-
-### Debugging and Troubleshooting
-- Application logs appear in console when running `npm start` or `npm run dev`
-- Database connection errors indicate missing database services
-- Auth errors indicate missing AUDIENCE/DOMAIN environment variables
-- 404 errors on routes indicate route registration issues
-- Check `app.js` for middleware and route registration
-- Jest warnings about experimental VM modules are expected (ES module usage)
-
-### CI/CD Integration
-- GitHub Actions workflows in `.github/workflows/`
-- `test_pushes.yaml` runs unit tests on pushes
-- `ci_dev.yaml` runs E2E tests on PRs to development
-- Tests require environment secrets configured in GitHub repository settings
-
-### Performance Notes
-- Application startup: 2-3 seconds
-- npm install: ~1-20 seconds depending on cache (timeout: 60+ seconds)
-- Unit tests: ~12 seconds (timeout: 30+ seconds)
-- Existence tests: ~7 seconds (timeout: 30+ seconds)
-- Database tests: variable depending on database response times
-
-### Critical Environment Variables
-Required for basic functionality:
-- `PORT` (default: 3011)
-- `SERVERURL` (default: http://localhost:3011)
-
-Required for database functionality:
-- `MONGODB` (MongoDB connection string)
-- `MONGODBNAME` (MongoDB database name)
-- `MARIADB` (MariaDB host)
-- `MARIADBNAME`, `MARIADBUSER`, `MARIADBPASSWORD` (MariaDB credentials)
-
-Required for authentication:
-- `AUDIENCE` (Auth0 audience)
-- `DOMAIN` (Auth0 domain)
-
-Required for external services:
-- `TINYPEN` (TinyPEN API base URL)
-- `RERUMURL` (RERUM repository URL)
-
-### Manual Testing Scenarios
-After making changes, always validate:
-1. **Basic Service**: Start server with `npm start`, test with `curl http://localhost:3011/` - should return HTML containing "TPEN3 SERVICES BABY!!!" in the response body
-2. **Route Registration**: `npm run existsTests` passes without errors
-3. **Core Logic**: `npm run unitTests` passes tests that don't require databases (some MongoDB tests will timeout - this is expected)
-4. **API Authentication**: Protected endpoints like `/my/profile` return 401 status code without valid tokens
-5. **Application Behavior**: Server may crash after serving requests when MongoDB is not available - this is expected and indicates database connection attempts are working correctly
-
-### Complete Validation Workflow Example
-```bash
-# Basic setup
-cp .env.development .env
-npm install
-
-# Test core functionality without databases
-npm run existsTests # Should pass completely
-npm run unitTests # Should pass most tests, MongoDB tests will timeout
-
-# Test application serving
-npm start &
-sleep 3
-curl http://localhost:3011/ # Should return HTML with service name
-curl -w "Status: %{http_code}\n" http://localhost:3011/my/profile # Should return 401
-kill %1 # Stop the background server
-```
-
-### Common File Locations
-- Main application entry: `bin/tpen3_services.js`
-- Express app setup: `app.js`
-- Route definitions: `project/index.js`, `userProfile/index.js`, etc.
-- Database controllers: `database/mongo/controller.js`, `database/maria/controller.js`
-- Authentication middleware: `auth/index.js`
-- Domain models: `classes/[Entity]/[Entity].js`
-- Configuration: `config.env` (safe defaults), `.env.development` and `.env.production` (templates), `.env` (local config, gitignored)
-
-### Dependencies and Versions
-- Express.js for REST API framework
-- MongoDB driver for document storage
-- MariaDB driver for relational storage
-- Auth0 libraries for JWT authentication
-- Jest for testing framework
-- Nodemon for development auto-reload
-- IIIF libraries for manifest handling
+3. Once the task is complete and verification is needed: `npm run allTests`
+4. Start app if manual testing is needed.
### External Resources
- [IIIF Presentation API](https://iiif.io/api/presentation/)
@@ -231,4 +64,4 @@ kill %1 # Stop the background server
7. Use JDoc style for code documentation. Cleanup, fix, or generate documentation for the code you work on as you encounter it.
8. We use `npm start` often to run the app locally. However, do not make code edits based on this assumption. Production and development load balance in the app with pm2, not by using `npm start`
9. NEVER CANCEL long-running commands. Application builds and tests are designed to complete within documented timeouts. Always wait for completion to ensure accurate validation of changes.
-10. All work on issues for bugs, features, and enhancements will target the `development` branch. The `main` branch will only be targetted with hotfixes by admins or by PRs from the `development` branch. New work should branch from `development`.
\ No newline at end of file
+10. All work on issues for bugs, features, and enhancements will target the `development` branch. The `main` branch will only be targetted with hotfixes by admins or by PRs from the `development` branch. New work should branch from `development`.
diff --git a/.claude/settings.json b/.claude/settings.json
deleted file mode 100644
index 9f1ee439..00000000
--- a/.claude/settings.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "statusLine": {
- "type": "command",
- "command": "bash /mnt/e/tpen3-services/.claude/statusline-command.sh"
- },
- "env": {
- "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "500000",
- "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1",
- "MAX_MCP_OUTPUT_TOKENS": "500000",
- "DISABLE_ERROR_REPORTING": "0",
- "DISABLE_NON_ESSENTIAL_MODEL_CALLS": "0",
- "DISABLE_PROMPT_CACHING": "0",
- "MAX_THINKING_TOKENS": "500000",
- "BASH_MAX_TIMEOUT_MS": "3000000",
- "OPENCODE_DISABLE_PRUNE": "true",
- "OPENCODE_DISABLE_AUTOCOMPACT": "true"
- }
-}
diff --git a/.claude/statusline-command.sh b/.claude/statusline-command.sh
deleted file mode 100644
index ed13514a..00000000
--- a/.claude/statusline-command.sh
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/bin/bash
-
-# Read JSON input
-input=$(cat)
-
-# Extract data from JSON
-cwd=$(echo "$input" | jq -r '.workspace.current_dir')
-cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
-api_duration=$(echo "$input" | jq -r '.cost.total_api_duration_ms // 0')
-total_duration=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
-lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
-lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
-model_display=$(echo "$input" | jq -r '.model.display_name // "unknown"')
-
-# Calculate API duration in seconds
-api_duration_sec=$(echo "scale=1; $api_duration / 1000" | bc -l 2>/dev/null || echo "0")
-
-# Get git branch if in a git repository
-git_branch=""
-if git -C "$cwd" rev-parse --git-dir > /dev/null 2>&1; then
- branch=$(git -C "$cwd" -c core.fileMode=false branch --show-current 2>/dev/null)
- if [ -n "$branch" ]; then
- git_branch="($branch)"
- fi
-fi
-
-# Build the enhanced status line
-# Format: (branch) model $cost | API: Xs | +L/-L
-
-# Cyan for git branch
-if [ -n "$git_branch" ]; then
- printf '\033[36m%s\033[0m ' "$git_branch"
-fi
-
-# Magenta for model name
-printf '\033[35m%s\033[0m ' "$model_display"
-
-# Bold yellow for cost (live updating token usage proxy)
-printf '\033[1;33m$%.4f\033[0m' "$cost"
-
-# Green for API time (shows compute usage)
-if [ "$api_duration" != "0" ]; then
- printf ' \033[32m| API: %ss\033[0m' "$api_duration_sec"
-fi
-
-# White for code changes (productivity)
-if [ "$lines_added" != "0" ] || [ "$lines_removed" != "0" ]; then
- printf ' \033[37m| +%s/-%s\033[0m' "$lines_added" "$lines_removed"
-fi
-
-printf '\n'
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index dadeca69..f9baba29 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,32 +1,18 @@
-# TPEN Services
+# copilot-instructions.md
-TPEN Services is a Node.js Express API service for TPEN3 (Transcription for Paleographical and Editorial Notation). This provides RESTful APIs for digital humanities, cultural heritage, annotation services, and IIIF manifest handling. The service supports multiple database backends (MongoDB, MariaDB) and uses Auth0 for authentication.
+This file provides guidance to GitHub Copilot and other AI Assistants when working with code in this repository.
+
+## TPEN Services
-Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
+## Project Overview
+
+TPEN Services is a Node.js Express API service for TPEN3 (Transcription for Paleographical and Editorial Notation). This provides RESTful APIs for digital humanities, cultural heritage, annotation services, and IIIF manifest handling. The service supports multiple database backends (MongoDB, MariaDB) and uses Auth0 for authentication.
## Working Effectively
### Bootstrap, Build, and Test the Repository
-
-- Copy environment configuration: `cp .env.development .env`
+- Environment configuration is in .env. Copy environment configuration: `cp .env.development .env`
- Install dependencies: `npm install` -- takes up to 20 seconds. NEVER CANCEL. Set timeout to 60+ seconds.
-- Run unit tests: `npm run unitTests` -- takes 12 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
-- Run existence tests: `npm run existsTests` -- takes 7 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
-- Run all tests: `npm run allTests` -- takes 12 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
-
-### Run the Application
-
-- ALWAYS run the bootstrapping steps first.
-- Production server: `npm start` -- starts on port 3011
-- Development server: `npm run dev` -- starts with nodemon auto-reload on port 3011
-- Test basic functionality: `curl http://localhost:3011/` should return the TPEN3 Services index HTML (e.g. contains `TPEN3 Services `)
-
-### Environment Requirements
-- Node.js >= 22.20.0
-- MongoDB (for database tests and full functionality)
-- MariaDB (for database tests and full functionality)
-- Copy `.env.development` to `.env` for basic functionality
-- For full functionality, configure database connection strings, GITHUB_TOKEN, and Auth0 credentials in `.env`
### Environment Configuration
@@ -63,10 +49,12 @@ This allows developers to work immediately with sensible defaults while keeping
- Test the root endpoint: `curl http://localhost:3011/` -- should return HTML containing the TPEN3 Services index (heading + welcome text)
- Run unit tests that don't require databases: `npm run unitTests` -- many tests pass without database connections
- Run existence tests: `npm run existsTests` -- validates route registration and class imports
-- ALWAYS wait for full test completion. Tests may appear to hang but will complete within 12 seconds.
+- Run all tests: `npm run allTests` -- Full test suite confirming full app functionality
+- ALWAYS wait for full test completion. Tests may appear to hang but should complete within 2 minutes.
- NOTE: Application may crash after serving initial requests due to database connection attempts - this is expected behavior without running MongoDB/MariaDB.
### Test Categories Available
+- `npm run allTests` -- Full test suite which requires .env settings
- `npm run unitTests` -- Core unit tests (some require databases)
- `npm run existsTests` -- Route and class existence validation (database-independent)
- `npm run functionsTests` -- Function-level tests
@@ -82,93 +70,10 @@ This allows developers to work immediately with sensible defaults while keeping
## Common Tasks
-### Repository Structure
-```
-/home/runner/work/TPEN-services/TPEN-services/
-├── app.js # Express application setup
-├── bin/tpen3_services.js # Server entry point
-├── package.json # Dependencies and scripts
-├── jest.config.js # Test configuration
-├── config.env # Safe defaults (committed)
-├── .env.development # Development template
-├── .env.production # Production template
-├── API.md # API documentation
-├── classes/ # Domain model classes
-│ ├── Project/ # Project management
-│ ├── User/ # User management
-│ ├── Group/ # Group management
-│ ├── Layer/ # Annotation layers
-│ ├── Line/ # Text line handling
-│ ├── Page/ # Page management
-│ └── Manifest/ # IIIF manifest handling
-├── database/ # Database drivers
-│ ├── mongo/ # MongoDB controller
-│ ├── maria/ # MariaDB controller
-│ └── tiny/ # TinyPEN API controller
-├── auth/ # Auth0 authentication
-├── project/ # Project API routes
-├── userProfile/ # User API routes
-├── line/ # Line API routes
-├── page/ # Page API routes
-└── utilities/ # Helper functions
-```
-
-### Key API Endpoints
-- `GET /` -- Service status (returns the TPEN3 Services index HTML)
-- `GET /project/:id` -- Get project by ID (requires authentication)
-- `POST /project/create` -- Create new project (requires authentication)
-- `POST /project/import?createFrom=URL` -- Import project from IIIF manifest
-- `GET /user/:id` -- Get user profile (public)
-- `GET /my/profile` -- Get authenticated user profile
-- `GET /line/:id` -- Get text line annotation
-- `GET /page/:id` -- Get annotation page
-
-### Authentication
-- Uses Auth0 JWT bearer tokens
-- Protected endpoints require `Authorization: Bearer ` header
-- Environment variables AUDIENCE and DOMAIN must be configured for auth tests
-- Public endpoints: `/`, `/user/:id`
-- Protected endpoints: `/project/*`, `/my/*`, most POST/PUT/DELETE operations
-
### Database Configuration
-- MongoDB: Configure MONGODB and MONGODBNAME in `.env`
-- MariaDB: Configure MARIADB, MARIADBNAME, MARIADBUSER, MARIADBPASSWORD in `.env`
-- TinyPEN API: Configure TINYPEN in `.env`
-- Default configurations in `config.env` point to localhost development services
-
-### Development Workflow
+> See config.env
-1. Always start with: `cp .env.development .env && npm install`
-2. Make code changes
-3. Test with: `npm run existsTests` (fast, database-independent)
-4. For database changes: ensure MongoDB/MariaDB running, then `npm run dbTests`
-5. For API changes: `npm run E2Etests`
-6. Start dev server: `npm run dev`
-7. Test manually: `curl http://localhost:3011/` and relevant endpoints
-
-### Debugging and Troubleshooting
-- Application logs appear in console when running `npm start` or `npm run dev`
-- Database connection errors indicate missing database services
-- Auth errors indicate missing AUDIENCE/DOMAIN environment variables
-- 404 errors on routes indicate route registration issues
-- Check `app.js` for middleware and route registration
-- Jest warnings about experimental VM modules are expected (ES module usage)
-
-### CI/CD Integration
-- GitHub Actions workflows in `.github/workflows/`
-- `test_pushes.yaml` runs unit tests on pushes
-- `ci_dev.yaml` runs E2E tests on PRs to development
-- Tests require environment secrets configured in GitHub repository settings
-
-### Performance Notes
-- Application startup: 2-3 seconds
-- npm install: ~1-20 seconds depending on cache (timeout: 60+ seconds)
-- Unit tests: ~12 seconds (timeout: 30+ seconds)
-- Existence tests: ~7 seconds (timeout: 30+ seconds)
-- Database tests: variable depending on database response times
-
-### Critical Environment Variables
Required for basic functionality:
- `PORT` (default: 3011)
@@ -181,6 +86,13 @@ Required for database functionality:
- `MARIADB` (MariaDB host)
- `MARIADBNAME`, `MARIADBUSER`, `MARIADBPASSWORD` (MariaDB credentials)
+MongoDB collection names (configured in `config.env`):
+
+- `TPENPROJECTS` (default: projects) - Project documents collection
+- `TPENGROUPS` (default: groups) - User groups collection
+- `TPENUSERS` (default: users) - User profiles collection
+- `TPENCOLUMNS` (default: columns) - Column annotations collection
+
Required for authentication:
- `AUDIENCE` (Auth0 audience)
@@ -191,53 +103,14 @@ Required for external services:
- `TINYPEN` (TinyPEN API base URL)
- `RERUMIDPREFIX` (RERUM ID prefix URL)
-### Manual Testing Scenarios
-
-After making changes, always validate:
-
-1. **Basic Service**: Start server with `npm start`, test with `curl http://localhost:3011/` - should return HTML containing the TPEN3 Services index (e.g. the `` heading)
-2. **Route Registration**: `npm run existsTests` passes without errors
-3. **Core Logic**: `npm run unitTests` passes tests that don't require databases (some MongoDB tests will timeout - this is expected)
-4. **API Authentication**: Protected endpoints like `/my/profile` return 401 status code without valid tokens
-5. **Application Behavior**: Server may crash after serving requests when MongoDB is not available - this is expected and indicates database connection attempts are working correctly
-
-### Complete Validation Workflow Example
-
-```bash
-# Basic setup
-cp .env.development .env
-npm install
-
-# Test core functionality without databases
-npm run existsTests # Should pass completely
-npm run unitTests # Should pass most tests, MongoDB tests will timeout
-
-# Test application serving
-npm start &
-sleep 3
-curl http://localhost:3011/ # Should return HTML with service name
-curl -w "Status: %{http_code}\n" http://localhost:3011/my/profile # Should return 401
-kill %1 # Stop the background server
-```
-
-### Common File Locations
-
-- Main application entry: `bin/tpen3_services.js`
-- Express app setup: `app.js`
-- Route definitions: `project/index.js`, `userProfile/index.js`, etc.
-- Database controllers: `database/mongo/controller.js`, `database/maria/controller.js`
-- Authentication middleware: `auth/index.js`
-- Domain models: `classes/[Entity]/[Entity].js`
-- Configuration: `config.env` (safe defaults), `.env` (local config, gitignored), `.env.development` and `.env.production` (templates)
-
-### Dependencies and Versions
-- Express.js for REST API framework
-- MongoDB driver for document storage
-- MariaDB driver for relational storage
-- Auth0 libraries for JWT authentication
-- Jest for testing framework
-- Nodemon for development auto-reload
-- IIIF libraries for manifest handling
+### Development Workflow
+
+1. Do not overwrite the existing .env file. If an .env file does not exist or is not populated then copy environment configuration: `cp .env.development .env`
+2. Install dependencies with `npm install`
+3. Make code changes
+4. Test with: `npm run existsTests` (fast, database-independent)
+5. For all other tests use `npm run allTests`
+6. Test manually: `curl http://localhost:3011/` and relevant endpoints
NEVER CANCEL long-running commands. Application builds and tests are designed to complete within documented timeouts. Always wait for completion to ensure accurate validation of changes.
diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml
index 4b8477ea..c8b42fa9 100644
--- a/.github/workflows/cd_dev.yaml
+++ b/.github/workflows/cd_dev.yaml
@@ -40,7 +40,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@master
with:
- node-version: "^22.20.0"
+ node-version: "24"
- name: Cache node modules
uses: actions/cache@master
env:
diff --git a/.github/workflows/cd_prod.yaml b/.github/workflows/cd_prod.yaml
index d4dabf62..7650f506 100644
--- a/.github/workflows/cd_prod.yaml
+++ b/.github/workflows/cd_prod.yaml
@@ -40,7 +40,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@master
with:
- node-version: "^22.20.0"
+ node-version: "24"
- name: Cache node modules
uses: actions/cache@master
env:
diff --git a/.github/workflows/ci_dev.yaml b/.github/workflows/ci_dev.yaml
index a3a8e358..0ce4cf78 100644
--- a/.github/workflows/ci_dev.yaml
+++ b/.github/workflows/ci_dev.yaml
@@ -34,7 +34,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@master
with:
- node-version: "^22.20.0"
+ node-version: "24"
- name: Cache node modules
uses: actions/cache@master
env:
diff --git a/.github/workflows/ci_prod.yaml b/.github/workflows/ci_prod.yaml
index aa86c6da..e1fe40d3 100644
--- a/.github/workflows/ci_prod.yaml
+++ b/.github/workflows/ci_prod.yaml
@@ -34,7 +34,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@master
with:
- node-version: "^22.20.0"
+ node-version: "24"
- name: Cache node modules
uses: actions/cache@master
env:
diff --git a/.github/workflows/test_pushes.yaml b/.github/workflows/test_pushes.yaml
index 858a7bde..7e291fdf 100644
--- a/.github/workflows/test_pushes.yaml
+++ b/.github/workflows/test_pushes.yaml
@@ -52,7 +52,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@master
with:
- node-version: "^22.20.0"
+ node-version: "24"
- name: Cache node modules
uses: actions/cache@master
env:
diff --git a/API.md b/API.md
deleted file mode 100644
index d3d90823..00000000
--- a/API.md
+++ /dev/null
@@ -1,408 +0,0 @@
-# TPEN Services API Documentation
-
-This document provides an overview of the available routes in the TPEN Services API. These routes allow interaction with the application for various functionalities.
-
-## Base URL
-
-- **Production**: [https://api.t-pen.org](https://api.t-pen.org)
-- **Development**: [https://dev.api.t-pen.org](https://dev.api.t-pen.org)
-
-## Endpoints
-
-### 1. **Authentication**
-
-Endpoints marked with a 🔐 requiring authentication expect a valid JWT token in the `Authorization` header. Use the [public TPEN3 login](https://three.t-pen.org/login/) to get one of these JWT tokens.
-
----
-
-### 2. **Project**
-
-#### `POST /project/create` 🔐
-
-- **Description**: Create a new project. This is a high-skill maneuver requiring a complete project object.
-- **Request Body**:
-
- ```json
- {
- "label": "string",
- "metadata": [ { Metadata } ],
- "layers": [ { Layer } ],
- "manifests": [ "URIs" ],
- "creator": "URI",
- "group": "hexstring"
- }
- ```
-
-- **Responses**:
-
- - **201**: Project created successfully
- - **400**: Project creation failed, validation errors
- - **401**: Unauthorized
- - **500**: Server error
-
- The Location header of a successful response is the Project id.
-
-#### `POST /import?createFrom="URL"` 🔐
-
-- **Description**: Create a new project by importing a web resource.
-- **Request Body**:
-
- ```json
- {
- "url": "URL"
- }
- ```
-
-- **Responses**:
-
- - **201**: Project created successfully
- - **400**: Project creation failed, validation errors
- - **401**: Unauthorized
- - **500**: Server error
-
- The Location header of a successful response is the Project id. The project will be created with the label and metadata of the imported resource. A complete project object will be created with the imported resource as the first manifest and returned.
-
-#### `GET /project/:id` 🔐
-
-- **Description**: Retrieve a project by ID.
-- **Parameters**:
- - `id`: ID of the project.
-- **Responses**:
-
- - **200**: Project found
- ```json
- {
- "id": "string",
- "label": "string",
- "metadata": [ { Metadata } ],
- "creator": "URI",
- "layers": [ { Layer } ],
- "manifests": [ "URIs" ],
- "group": "hexstring",
- "tools": [ "string" ],
- "options": { OptionsMap }
- }
- ```
- - **404**: Project not found
- - **401**: Unauthorized
- - **403**: Forbidden
- - **500**: Server error
-
-The response is not a complete Project data object, but a projection designed for use in the client interface.
-
----
-
-### 3. **Collaborators**
-
-Manage the users and roles in a Project.
-
-#### `POST /project/:projectId/invite-member` 🔐
-
-- **Description**: Invite a user to a project. If the user does not have a TPEN account, they will be sent an email invitation.
-- **Parameters**:
- - `projectId`: ID of the project as a hexstring or the Project slug.
-- **Request Body**:
-
- ```json
- {
- "email": "string",
- "roles": ["string"] | "string"
- }
- ```
-
- - `email`: The email address of the user to invite.
- - `roles`: The roles of the user in the project as an array or space-delimited string.
-- **Responses**:
-
- - **200**: User invited successfully
- - **400**: User invitation failed, validation errors
- - **401**: Unauthorized
- - **403**: Forbidden
- - **500**: Server error
-
-This API is not able to track the status of the invitation. Email addresses that fail to be delivered or are rejected by the recipient will not be reported.
-
-#### `POST /project/:projectId/remove-member` 🔐
-
-- **Description**: Remove a user from a project group.
-- **Parameters**:
- - `projectId`: ID of the project.
-- **Request Body**:
-
- ```json
- {
- "userID": "string"
- }
- ```
-- **Responses**:
-
- - **204**: User removed successfully
- - **401**: Unauthorized
- - **403**: Forbidden
- - **500**: Server error
-
-This removes a user and their roles from the project. The user will no longer have access to the project, but their annotations will remain with full attribution.
-
-#### `PUT /project/:projectId/collaborator/:collaboratorId/setRoles` 🔐
-
-- **Description**: Set the roles of a User in a project, replacing all currently defined roles.
-- **Parameters**:
- - `projectId`: ID of the project.
- - `collaboratorId`: ID of the collaborator.
-- **Request Body**:
-
- ```json
- {
- "roles": ["string"] | "string"
- }
- ```
-- **Responses**:
-
- - **200**: Roles set successfully
- - **400**: Roles setting failed, validation errors
- - **401**: Unauthorized
- - **403**: Forbidden
- - **500**: Server error
-
-The User requesting this action must have permissions to set roles for the collaborator. The OWNER role cannot be set using this endpoint.
-
-#### `POST /project/:projectId/switch/owner` 🔐
-
-- **Description**: Transfer ownership of a project to another user.
-- **Parameters**:
- - `projectId`: ID of the project.
-- **Request Body**:
-
- ```json
- {
- "newOwnerId": "hexstring"
- }
- ```
-- **Responses**:
-
- - **200**: Ownership transferred successfully
- - **400**: Ownership transfer failed, validation errors
- - **401**: Unauthorized
- - **403**: Forbidden
- - **500**: Server error
-
-The User requesting this action must be the current owner of the project. The OWNER role will be removed from the current owner and assigned to the new owner. The new owner must be a member of the project. If the current owner has no other roles, the CONTRIBUTOR role will be assigned to them.
-
-#### `POST /project/:projectId/setCustomRoles` 🔐
-
-- **Description**: Set custom roles for a Project group. Custom roles must be provided as a JSON object with keys as roles and values as arrays of permissions or space-delimited strings.
-- **Parameters**:
- - `projectId`: ID of the project.
-- **Request Body**:
-
- ```json
- {
- "roles": {
- "roleName": ["permission1", "permission2"] | "space-delimited permissions"
- }
- }
- ```
-
-- **Responses**:
-
- - **200**: Custom roles set successfully
- - **400**: Invalid request, validation errors
- - **401**: Unauthenticated request
- - **403**: Permission denied
- - **500**: Server error
-
-The User requesting this action must have permissions to update roles. Default roles cannot be modified using this endpoint. Custom roles can be complicated and there is more detailed description of their use in the [Cookbook](#).
-
----
-
-### 4. **Users**
-
-Private account modification, personal details, and public profile retrieval.
-
-#### `GET /my/profile` 🔐
-
-- **Description**: Retrieve the profile of the authenticated user.
-- **Responses**:
-
- - **200**: Profile found
- ```json
- {
- "_id": "hexstring",
- "_sub": "string",
- "agent": "URI",
- "email": "string",
- "profile": {
- "displayName": "string",
- "custom": Any
- }
- "name": "string"
- }
- ```
- - **401**: Unauthorized
- - **500**: Server error
-
-Users can always see and manipulate their own profile. The profile object is a projection of the full user object.
-
-#### `GET /my/projects` 🔐
-
-- **Description**: Retrieve a list of projects the authenticated user is a member of.
-- **Responses**:
-
- - **200**: Projects found
- ```json
- [
- {
- "_id": "hexstring",
- "label": "string",
- "roles": ["string"]
- }, ...
- ]
- ```
- - **401**: Unauthorized
- - **500**: Server error
-
-The response is a list of projects the user is a member of regardless of the permissions afforded to them in each project.
-
-#### `GET /user/:id`
-
-- **Description**: Retrieve the public profile of a user.
-- **Parameters**:
- - `id`: ID of the user.
-- **Responses**:
-
- - **200**: Profile found
- ```json
- {
- "_id": "hexstring",
- "displayName": "string",
- "custom": Any
- }
- ```
- - **404**: Profile not found
- - **500**: Server error
-
-The response is a projection of the full user object. The public profile is available to all users and serialized into this response with whatever the user has chosen to share. The `custom` field is not an actual property - it is a placeholder for any additional fields the user has added to their profile. The `displayName` and `_id` fields are always present.
-
----
-
-### 5. **Layers**
-
-#### `POST /project/:projectId/layer` 🔐
-
-- **Description**: Create a new layer within a project.
-- **Parameters**:
- - `projectId`: ID of the project.
-- **Request Body**:
-
- ```json
- {
- "label": "string",
- "canvases": ["string"]
- }
- ```
-
-- **Responses**:
-
- - **201**: Layer created successfully
- ```json
- {
- "id": "string",
- "label": "string",
- "pages": ["string"]
- }
- ```
- - **400**: Invalid input
- - **404**: Project not found
- - **401**: Unauthorized
- - **500**: Server error
-
-Note that requests without at least one canvas are considered invalid.
-
-#### `PUT /project/:projectId/layer/:layerId` 🔐
-
-- **Description**: Update an existing layer within a project.
-- **Parameters**:
- - `projectId`: ID of the project.
- - `layerId`: ID of the layer.
-- **Request Body**:
-
- ```json
- {
- "label": "string",
- "canvases": ["string"]
- }
- ```
-
-- **Responses**:
-
- - **200**: Layer updated successfully
- ```json
- {
- "id": "string",
- "label": "string",
- "pages": ["string"]
- }
- ```
- - **400**: Invalid input
- - **404**: Layer or project not found
- - **401**: Unauthorized
- - **500**: Server error
-
-Note that you may not empty the canvases of an existing layer. If the `canvases` property is an empty array the request will be treated as if it did not contain the `canvases` property at all.
-
-#### `GET /project/:projectId/layer/:layerId`
-
-- **Description**: Get an existing layer within a project.
-- **Parameters**:
- - `projectId`: ID of the project.
- - `layerId`: ID of the layer.
-
-- **Responses**:
-
- - **200**: Layer found
- ```json
- {
- "@context": "string",
- "id": "string",
- "type": "AnnotationCollection",
- "label": {
- "none":[
- "TPEN3 Layer"
- ]
- },
- "total": "integer",
- "first": "string",
- "last": "string"
- }
- ```
- - **500**: Server error
-
-#### `GET /project/:projectId/layer/:layerId/page/:pageid`
-#### `GET /project/:projectId/page/:pageid`
-
-- **Description**: Get an existing page within a project.
-- **Parameters**:
- - `projectId`: ID of the project.
- - `layerId`: Optional. ID of the layer.
- - `pageId`: The ID of the page.
-
-- **Responses**:
-
- - **200**: Page found
- ```json
- {
- "@context": "string",
- "id": "string",
- "type": "AnnotationPage",
- "label": {
- "none":[
- "TPEN3 Page"
- ]
- },
- "target": "string",
- "items":[ {"type": "Canvas"} ],
- "prev": "string",
- "next": "string"
- }
- ```
- - **500**: Server error
diff --git a/README.md b/README.md
index 48ef6d3d..e1b834df 100644
--- a/README.md
+++ b/README.md
@@ -99,4 +99,3 @@ For production deployments:
4. Never commit production secrets to the repository
See [CONFIG.md](./CONFIG.md) for detailed deployment instructions.
-
\ No newline at end of file
diff --git a/__tests__/mount.test.js b/__tests__/mount.test.js
index 2a60668d..38dde54d 100644
--- a/__tests__/mount.test.js
+++ b/__tests__/mount.test.js
@@ -87,14 +87,10 @@ function routeExists(path, method = null) {
describe('Check to see that all expected route patterns exist. #exists_unit', () => {
- describe('Root and API routes', () => {
+ describe('Root routes', () => {
it('GET / -- root index route', () => {
expect(routeExists('/', 'get')).toBe(true)
})
-
- it('GET /api -- API documentation route', () => {
- expect(routeExists('/api', 'get')).toBe(true)
- })
})
describe('Private user routes (/my)', () => {
@@ -191,6 +187,10 @@ describe('Check to see that all expected route patterns exist. #exists_unit', ()
it('GET /project/:projectId/collaborator/:collaboratorId/decline -- decline project invitation', () => {
expect(routeExists('/:projectId/collaborator/:collaboratorId/decline', 'get')).toBe(true)
})
+
+ it('POST /project/:projectId/leave -- member leave project', () => {
+ expect(routeExists('/:id/leave', 'post')).toBe(true)
+ })
})
describe('Project custom roles routes', () => {
@@ -339,7 +339,6 @@ describe('Check to see that critical repo files are present #exists_unit', () =>
expect(fs.existsSync(filePath+"CODEOWNERS")).toBeTruthy()
expect(fs.existsSync(filePath+"CONTRIBUTING.md")).toBeTruthy()
expect(fs.existsSync(filePath+"README.md")).toBeTruthy()
- expect(fs.existsSync(filePath+"API.md")).toBeTruthy()
expect(fs.existsSync(filePath+"LICENSE.md")).toBeTruthy()
expect(fs.existsSync(filePath+".gitignore")).toBeTruthy()
expect(fs.existsSync(filePath+"package.json")).toBeTruthy()
diff --git a/__tests__/smoke.test.js b/__tests__/smoke.test.js
index 4f63d730..a3cf220c 100644
--- a/__tests__/smoke.test.js
+++ b/__tests__/smoke.test.js
@@ -57,7 +57,7 @@ async function runSmokeChecks() {
if (!(hasHeading || hasWelcome)) throw new Error('Expected TPEN3 Services index HTML not found in response')
}))
- // Protected endpoint requires authentication
+ // Protected endpoint requires authentication (401 when Authorization header is missing)
checks.push(await runCheck('Protected endpoint requires authentication', async () => {
const res = await request('/my/profile')
if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`)
diff --git a/app.js b/app.js
index 36e127f7..b8d3be08 100644
--- a/app.js
+++ b/app.js
@@ -21,6 +21,8 @@ import userProfileRouter from './userProfile/index.js'
import privateProfileRouter from './userProfile/privateProfile.js'
import proxyRouter from './utilities/proxy.js'
import feedbackRouter from './feedback/feedbackRoutes.js'
+import routeErrorHandler from './utilities/routeErrorHandler.js'
+import { respondWithError } from './utilities/shared.js'
let app = express()
@@ -65,10 +67,11 @@ app.use(express.static(path.join(__dirname, 'public')))
*/
app.all('*_', (req, res, next) => {
if (process.env.DOWN === 'true') {
- return res.status(503).json({
- message:
- 'TPEN3 services are down for updates or maintenance at this time. We apologize for the inconvenience. Try again later.'
- })
+ return respondWithError(
+ res,
+ 503,
+ 'TPEN3 services are down for updates or maintenance at this time. We apologize for the inconvenience. Try again later.'
+ )
}
next()
})
@@ -82,9 +85,12 @@ app.use('/my', privateProfileRouter)
app.use('/proxy', proxyRouter)
app.use('/beta', feedbackRouter)
+// Centralized error handling middleware
+app.use(routeErrorHandler)
+
//catch 404 because of an invalid site path
app.use('*_', (req, res) => {
- res.status(404).json({ message: res.statusMessage ?? 'This page does not exist' })
+ return respondWithError(res, 404, res.statusMessage ?? 'This page does not exist')
})
export { app as default }
diff --git a/auth/index.js b/auth/index.js
index 702684f4..06b16ccf 100644
--- a/auth/index.js
+++ b/auth/index.js
@@ -1,7 +1,5 @@
import { auth } from "express-oauth2-jwt-bearer"
-import { extractToken, extractUser, isTokenExpired } from "../utilities/token.js"
import User from "../classes/User/User.js"
-
/**
* This function verifies authorization tokens using Auth0 library. to protect a route using this function in a different component:
1. import the function in that component
@@ -17,7 +15,7 @@ function auth0Middleware() {
issuerBaseURL: `https://${process.env.DOMAIN}/`,
})
- // Extract user from the token and set req.user. req.user can be set to specific info from the payload, like sib, roles, etc.
+ // Extract user from the token and set req.user. req.user can be set to specific info from the payload, like sub, roles, etc.
async function setUser(req, res, next) {
const { payload } = req.auth
@@ -32,24 +30,67 @@ function auth0Middleware() {
try {
const uid = agent.split("id/")[1]
const user = new User(uid)
- user.getSelf().then(async (u) => {
- if(!u || !u?.profile) {
+ const u = await user.getSelf()
+ if(!u || !u?.profile) {
+ const email = payload.name
+
+ // Check if a temporary user exists with this email
+ let existingUser = null
+ try {
+ existingUser = await user.getByEmail(email)
+ } catch (err) {
+ // No user found - that's fine, continue
+ }
+
+ if (existingUser && existingUser.inviteCode) {
+ // Found a temporary user - merge their memberships into this new user
user.data = {
_id: uid,
agent,
_sub: payload.sub,
- email: payload.name,
+ email: email,
profile: { displayName: payload.nickname },
}
- user.save()
+ await user.mergeFromTemporaryUser(existingUser)
+ await user.save()
+ req.user = user
+ next()
+ return
+ } else if (existingUser) {
+ // Non-temporary user with same email - this is a conflict
+ const err = new Error(`User with email ${email} already exists. Please contact TPEN3 administrators for assistance.`)
+ err.status = 409
+ next(err)
+ return
+ } else {
+ // No existing user - create new
+ user.data = {
+ _id: uid,
+ agent,
+ _sub: payload.sub,
+ email: email,
+ profile: { displayName: payload.nickname },
+ }
+ await user.save()
req.user = user
next()
return
}
- req.user = u
- next()
- return
- })
+ }
+ // Ensure no inviteCode on authenticated user
+ delete u.inviteCode
+
+ // If user exists but has wrong _sub (e.g., from temp user), update it
+ if (u._sub !== payload.sub) {
+ u._sub = payload.sub
+ const userObj = new User(uid)
+ userObj.data = u
+ await userObj.update()
+ }
+
+ req.user = u
+ next()
+ return
} catch (error) {
next(error)
}
diff --git a/classes/Column/Column.js b/classes/Column/Column.js
new file mode 100644
index 00000000..addc85d0
--- /dev/null
+++ b/classes/Column/Column.js
@@ -0,0 +1,146 @@
+import dbDriver from "../../database/driver.js"
+const database = new dbDriver("mongo")
+
+/**
+ * Represents a Column entity that organizes line annotations within a page.
+ * Columns provide a way to structure transcription content into logical groupings.
+ *
+ * @class Column
+ */
+export default class Column {
+ /**
+ * Creates a new Column instance.
+ *
+ * @param {string} [_id=database.reserveId()] - The unique identifier for this column
+ */
+ constructor(_id = database.reserveId()) {
+ this._id = _id
+ this.data = null
+ }
+
+ /**
+ * Gets the column data from the database if not already loaded.
+ *
+ * @returns {Promise} The column data object
+ * @throws {Error} If the column cannot be loaded from the database
+ */
+ async getColumnData() {
+ if(!this.data) {
+ await this.#loadFromDB()
+ }
+ return this.data
+ }
+
+ /**
+ * Loads the column data from the database.
+ *
+ * @private
+ * @returns {Promise} This Column instance with loaded data
+ * @throws {Error} If the database operation fails
+ */
+ async #loadFromDB() {
+ try {
+ this.data = await database.getById(this._id, process.env.TPENCOLUMNS)
+ if (!this.data) {
+ throw new Error(`Column with ID '${this._id}' not found`)
+ }
+ return this
+ } catch (error) {
+ throw new Error(`Failed to load column from database: ${error.message}`)
+ }
+ }
+
+ /**
+ * Saves the column data to the database.
+ *
+ * @returns {Promise} The saved column record
+ * @throws {Error} If the save operation fails or data is missing
+ */
+ async save() {
+ try {
+ if (!this.data) {
+ throw new Error('Cannot save column: data is null or undefined')
+ }
+ return await database.save(this.data, process.env.TPENCOLUMNS)
+ } catch (error) {
+ throw new Error(`Failed to save column: ${error.message}`)
+ }
+ }
+
+ /**
+ * Updates the column data in the database.
+ *
+ * @returns {Promise} The updated column record
+ * @throws {Error} If the update operation fails or data is missing
+ */
+ async update() {
+ try {
+ if (!this.data) {
+ throw new Error('Cannot update column: data is null or undefined')
+ }
+ return await database.update(this.data, process.env.TPENCOLUMNS)
+ } catch (error) {
+ throw new Error(`Failed to update column: ${error.message}`)
+ }
+ }
+
+ /**
+ * Deletes the column from the database.
+ *
+ * @returns {Promise} The result of the delete operation
+ * @throws {Error} If the delete operation fails
+ */
+ async delete() {
+ try {
+ return await database.delete(this._id, process.env.TPENCOLUMNS)
+ } catch (error) {
+ throw new Error(`Failed to delete column: ${error.message}`)
+ }
+ }
+
+ /**
+ * Creates a new column with the specified properties and saves it to the database.
+ *
+ * @static
+ * @param {string} pageId - The ID of the page this column belongs to
+ * @param {string} projectId - The ID of the project this column belongs to
+ * @param {string} label - The label/name for this column
+ * @param {string[]} [annotations=[]] - Array of annotation IDs in this column
+ * @returns {Promise} The saved column record
+ * @throws {Error} If validation fails or the save operation fails
+ */
+ static async createNewColumn(pageId, projectId, label, annotations=[]) {
+ try {
+ // Input validation
+ let newColumn = new Column()
+ if (label === null || label === undefined) {
+ label = `Column ${newColumn._id}`
+ }
+ if (!pageId || typeof pageId !== 'string') {
+ throw new Error('pageId must be a non-empty string')
+ }
+ if (!projectId || typeof projectId !== 'string') {
+ throw new Error('projectId must be a non-empty string')
+ }
+ if (!label || typeof label !== 'string' || label.trim().length === 0) {
+ throw new Error('label must be a non-empty string')
+ }
+ if (!Array.isArray(annotations)) {
+ throw new Error('annotations must be an array')
+ }
+
+ newColumn.data = {
+ _id: newColumn._id,
+ label: label,
+ onPage: pageId,
+ inProject: projectId,
+ next: null,
+ prev: null,
+ lines: annotations
+ }
+ return await newColumn.save()
+ } catch (error) {
+ throw new Error(`Failed to create new column: ${error.message}`)
+ }
+ }
+}
diff --git a/classes/Group/Group.js b/classes/Group/Group.js
index 5098bfcc..e32a18c6 100644
--- a/classes/Group/Group.js
+++ b/classes/Group/Group.js
@@ -36,7 +36,7 @@ export default class Group {
throw err
}
const roles = this.data.members[memberId]?.roles
- const allRoles = Object.assign(Group.defaultRoles, this.data.customRoles)
+ const allRoles = { ...Group.defaultRoles, ...this.data.customRoles }
return Object.fromEntries(roles.map(role => [role, allRoles[role]]))
}
@@ -61,15 +61,16 @@ export default class Group {
throw err
}
this.data.members[memberId] = { roles: [] }
- this.setMemberRoles(memberId, roles)
+ await this.setMemberRoles(memberId, roles)
}
/**
* Replace all roles for a member with the provided roles.
* @param {String} memberId _id of the member
* @param {Array | String} roles [ROLE, ROLE, ...] or "ROLE ROLE ..."
+ * @param {boolean} shouldUpdate Persist changes when true
*/
- async setMemberRoles(memberId, roles) {
+ async setMemberRoles(memberId, roles, shouldUpdate = true) {
if (!Object.keys(this.data.members).length) {
await this.#loadFromDB()
}
@@ -92,15 +93,19 @@ export default class Group {
roles = washRoles(roles)
this.data.members[memberId].roles = roles
- await this.update()
+ if (shouldUpdate) {
+ await this.update()
+ }
}
/**
* Add if not in roles for a member with the provided roles.
* @param {String} memberId _id of the member
* @param {Array | String} roles [ROLE, ROLE, ...] or "ROLE ROLE ..."
+ * @param {boolean} allowOwner Allow assignment of OWNER role
+ * @param {boolean} shouldUpdate Persist changes when true
*/
- async addMemberRoles(memberId, roles, allowOwner = false) {
+ async addMemberRoles(memberId, roles, allowOwner = false, shouldUpdate = true) {
if (!Object.keys(this.data.members).length) {
await this.#loadFromDB()
@@ -122,14 +127,19 @@ export default class Group {
}
roles = washRoles(roles, allowOwner)
this.data.members[memberId].roles = [...new Set([...this.data.members[memberId].roles, ...roles])]
+ if (shouldUpdate) {
+ await this.update()
+ }
}
/**
* Remove roles if found for a member.
* @param {String} memberId _id of the member
* @param {Array | String} roles [ROLE, ROLE, ...] or "ROLE ROLE ..."
+ * @param {boolean} allowOwner Allow removal of OWNER role
+ * @param {boolean} shouldUpdate Persist changes when true
*/
- async removeMemberRoles(memberId, roles, allowOwner = false) {
+ async removeMemberRoles(memberId, roles, allowOwner = false, shouldUpdate = true) {
if (!Object.keys(this.data.members).length) {
await this.#loadFromDB()
}
@@ -158,14 +168,77 @@ export default class Group {
}
this.data.members[memberId].roles = this.data.members[memberId].roles.filter(role => !roles.includes(role))
- await this.update()
+ if (shouldUpdate) {
+ await this.update()
+ }
}
- async removeMember(memberId) {
+ /**
+ * Remove a member from a Group.
+ * A member can be removed by an admin or by themselves.
+ * Validations:
+ * - User must be a member of the project
+ * - Cannot remove the only OWNER (must transfer ownership first)
+ *
+ * @param {string} memberId The User/member _id to remove from the Group and perhaps delete from the db.
+ * @param {boolean} voluntary Whether the user is leaving voluntarily (true) or being removed by admin (false).
+ * @param {boolean} shouldUpdate Persist changes when true
+ */
+ async removeMember(memberId, voluntary = false, shouldUpdate = true) {
if (!Object.keys(this.data.members).length) {
await this.#loadFromDB()
}
+ const member = this.data.members[memberId]
+ if (!member) {
+ throw {
+ status: 400,
+ message: "User is not a member of this group"
+ }
+ }
+ const userRoles = member.roles
+ // Prevent removing the only OWNER
+ if (userRoles.includes("OWNER")) {
+ const owners = this.getByRole("OWNER")
+ if (owners.length === 1) {
+ throw {
+ status: 403,
+ message: "Cannot remove: This user is the only owner. Transfer ownership first."
+ }
+ }
+ }
delete this.data.members[memberId]
+ if (shouldUpdate) {
+ await this.update()
+ }
+ }
+
+ /**
+ * Transfer membership from one user to another.
+ * Copies all roles from sourceMemberId to targetMemberId and removes sourceMemberId.
+ * If targetMemberId already exists, roles are merged (union).
+ * @param {String} sourceMemberId - The member being replaced (e.g., temp user)
+ * @param {String} targetMemberId - The member receiving the membership (e.g., real user)
+ */
+ async transferMembership(sourceMemberId, targetMemberId) {
+ if (!Object.keys(this.data.members).length) {
+ await this.#loadFromDB()
+ }
+
+ const sourceRoles = this.data.members[sourceMemberId]?.roles || []
+ if (!sourceRoles.length) return
+
+ if (this.data.members[targetMemberId]) {
+ // Merge roles if target already exists
+ this.data.members[targetMemberId].roles = [
+ ...new Set([...this.data.members[targetMemberId].roles, ...sourceRoles])
+ ]
+ } else {
+ // Add target with source's roles
+ this.data.members[targetMemberId] = { roles: [...sourceRoles] }
+ }
+
+ delete this.data.members[sourceMemberId]
+ await this.update()
}
getByRole(role) {
@@ -248,10 +321,10 @@ export default class Group {
}
if (!this.getByRole("OWNER")?.length) {
- await this.addMemberRoles(this.data.creator, "OWNER", true)
+ await this.addMemberRoles(this.data.creator, "OWNER", true, false)
}
if (!this.getByRole("LEADER")?.length) {
- await this.addMemberRoles(this.data.creator, "LEADER")
+ await this.addMemberRoles(this.data.creator, "LEADER", false, false)
}
}
@@ -265,6 +338,18 @@ export default class Group {
return await newGroup.save()
}
+ /**
+ * Find all groups containing a specific member.
+ * @param {String} memberId - The _id of the member to search for
+ * @returns {Promise} - Array of group documents containing this member
+ */
+ static async getGroupsByMember(memberId) {
+ return database.find(
+ { [`members.${memberId}`]: { $exists: true } },
+ process.env.TPENGROUPS
+ )
+ }
+
static defaultRoles = {
OWNER: ["*_*_*"],
LEADER: ["UPDATE_*_PROJECT", "READ_*_PROJECT", "*_*_MEMBER", "*_*_ROLE", "*_*_PERMISSION", "*_*_LAYER", "*_*_PAGE"],
diff --git a/classes/Layer/Layer.js b/classes/Layer/Layer.js
index 13e869bb..67ce979c 100644
--- a/classes/Layer/Layer.js
+++ b/classes/Layer/Layer.js
@@ -1,5 +1,4 @@
import dbDriver from "../../database/driver.js"
-import { handleVersionConflict } from "../../utilities/shared.js"
import Page from "../Page/Page.js"
import { fetchUserAgent } from "../../utilities/shared.js"
import ProjectFactory from "../Project/ProjectFactory.js"
@@ -8,6 +7,7 @@ const databaseTiny = new dbDriver("tiny")
export default class Layer {
#tinyAction = 'create'
+ #hydrated = false
/**
* Constructs a Layer from the JSON Object in the Project `layers` Array.
@@ -17,6 +17,7 @@ export default class Layer {
* @param {string} id The ID of the layer. This is the Layer stored in the Project.
* @param {string} label The label of the layer. This is the Layer stored in the Project.
* @param {Array} pages The pages in the layer by reference.
+ * @param {string|null} [creator=null] The creator/agent URI for this layer.
* @seeAlso {@link Layer.build}
*/
constructor(projectId, { id, label, pages, creator = null }) {
@@ -31,6 +32,9 @@ export default class Layer {
this.label = label
this.creator = creator
this.pages = pages
+ this.total = pages.length
+ this.first = pages.at(0)?.id
+ this.last = pages.at(-1)?.id
if (this.id.startsWith(process.env.RERUMIDPREFIX)) {
this.#tinyAction = 'update'
}
@@ -79,6 +83,10 @@ export default class Layer {
this.#setRerumId()
await this.#saveCollectionToRerum()
}
+ this.total = this.pages.length
+ this.first = this.pages.at(0)?.id
+ this.last = this.pages.at(-1)?.id
+ this.#hydrated = true
return this.#formatCollectionForProject()
}
@@ -86,6 +94,38 @@ export default class Layer {
return this.#formatCollectionForProject()
}
+ /**
+ * Returns a JSON representation of the Layer as a W3C AnnotationCollection.
+ * @param {boolean} isLD - If true, returns JSON-LD format with @context and type. If false, returns a simple object.
+ * @returns {Promise} The Layer as JSON.
+ */
+ async asJSON(isLD) {
+ if (!this.#hydrated && this.id?.startsWith?.(process.env.RERUMIDPREFIX)) {
+ await this.#loadAnnotationCollectionDataFromRerum()
+ }
+ let result
+ if (isLD) {
+ result = {
+ '@context': 'http://iiif.io/api/presentation/3/context.json',
+ id: this.id,
+ type: 'AnnotationCollection',
+ label: { "none": [this.label] },
+ total: this.pages.length,
+ first: this.pages.at(0)?.id,
+ last: this.pages.at(-1)?.id
+ }
+ if (this.creator) result.creator = this.creator
+ }
+ else {
+ result = {
+ id: this.id,
+ label: this.label,
+ pages: this.pages
+ }
+ }
+ return result
+ }
+
// Private Methods
#setRerumId() {
if (!this.id.startsWith(process.env.RERUMIDPREFIX)) {
@@ -94,50 +134,106 @@ export default class Layer {
return this
}
+ /**
+ * Resolve the RERUM URI of the Layer and sync Layer properties with the AnnotationCollection properties.
+ * The RERUM data will take preference and overwrite any properties that are already set.
+ * Only RERUM URIs are supported.
+ */
+ async #loadAnnotationCollectionDataFromRerum() {
+ if (this.id.startsWith?.(process.env.RERUMIDPREFIX)) {
+ const rawLayerData = await fetch(this.id).then(async (resp) => {
+ if (resp.ok) return resp.json()
+ let rerumErrorMessage
+ try {
+ rerumErrorMessage = `${resp.status ?? 500}: ${this.id} - ${await resp.text()}`
+ } catch (e) {
+ rerumErrorMessage = `500: ${this.id} - A RERUM error occurred`
+ }
+ const err = new Error(rerumErrorMessage)
+ err.status = 502
+ throw err
+ })
+ .catch(err => {
+ if (err.status === 502) throw err
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
+ })
+ if (!(rawLayerData.id || rawLayerData["@id"])) {
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
+ }
+ this.#tinyAction = 'update'
+ this.id = rawLayerData.id ?? rawLayerData["@id"] ?? this.id
+ if (rawLayerData.label) this.label = ProjectFactory.getLabelAsString(rawLayerData.label)
+ if (rawLayerData.creator) this.creator = rawLayerData.creator
+ this.#hydrated = true
+ }
+ return this
+ }
+
#formatCollectionForProject() {
return {
id: this.id,
label: this.label,
- pages: this.pages.map(p => new Page(this.id, p).asProjectPage())
+ pages: this.pages.map(p => {
+ const page = new Page(this.id, p).asProjectPage()
+ if (p.columns) page.columns = p.columns
+ return page
+ })
}
}
async #saveCollectionToRerum() {
const layerAsCollection = {
- "@context": "http://www.w3.org/ns/anno.jsonld",
+ "@context": "http://iiif.io/api/presentation/3/context.json",
id: this.id,
type: "AnnotationCollection",
label: { "none": [this.label] },
creator: await fetchUserAgent(this.creator),
total: this.pages.length,
- first: this.pages.at(0).id,
- last: this.pages.at(-1).id
+ first: this.pages.at(0)?.id,
+ last: this.pages.at(-1)?.id
}
if (this.#tinyAction === 'create') {
- await databaseTiny.save(layerAsCollection).catch(err => {
+ await databaseTiny.save(layerAsCollection)
+ .catch(err => {
console.error(err, layerAsCollection)
throw new Error(`Failed to save Layer to RERUM: ${err.message}`)
})
this.#tinyAction = 'update'
+ this.#hydrated = true
return this
}
- const existingLayer = await fetch(this.id).then(res => res.json())
- if (!existingLayer) {
- throw new Error(`Layer not found in RERUM: ${this.id}`)
- }
- const updatedLayer = { ...existingLayer, ...layerAsCollection }
-
- // Handle optimistic locking version if available
- try {
- await databaseTiny.overwrite(updatedLayer)
- return this
- } catch (err) {
- if (err.status === 409) {
- throw handleVersionConflict(null, err)
+ const existingLayer = await fetch(this.id).then(async (resp) => {
+ if (resp.ok) return resp.json()
+ let rerumErrorMessage
+ try {
+ rerumErrorMessage = `${resp.status ?? 500}: ${this.id} - ${await resp.text()}`
+ } catch (e) {
+ rerumErrorMessage = `500: ${this.id} - A RERUM error occurred`
}
+ const err = new Error(rerumErrorMessage)
+ err.status = 502
throw err
+ })
+ .catch(err => {
+ if (err.status === 502) throw err
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
+ })
+ if (!(existingLayer?.id || existingLayer?.["@id"])) {
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
}
+ const updatedLayer = { ...existingLayer, ...layerAsCollection }
+ await databaseTiny.overwrite(updatedLayer)
+ this.#hydrated = true
+ return this
}
}
diff --git a/classes/Line/Line.js b/classes/Line/Line.js
index 8bdb5dfd..d0f853a5 100644
--- a/classes/Line/Line.js
+++ b/classes/Line/Line.js
@@ -1,10 +1,11 @@
import dbDriver from "../../database/driver.js"
-import { fetchUserAgent } from "../../utilities/shared.js"
-
+import { fetchUserAgent, hasAnnotationChanges } from "../../utilities/shared.js"
const databaseTiny = new dbDriver("tiny")
+
export default class Line {
#tinyAction = 'create'
+ #hydrated = false
#setRerumId() {
if (!this.id.startsWith(process.env.RERUMIDPREFIX)) {
this.id = `${process.env.RERUMIDPREFIX}${this.id.split("/").pop()}`
@@ -42,7 +43,7 @@ export default class Line {
motivation: this.motivation ?? "transcribing",
target: this.target,
creator: await fetchUserAgent(this.creator),
- body: this.body
+ body: this.body ?? []
}
if (this.label) lineAsAnnotation.label = { "none": [this.label] }
if (this.#tinyAction === 'create') {
@@ -51,50 +52,116 @@ export default class Line {
throw new Error(`Failed to save Line to RERUM: ${err.message}`)
})
this.#tinyAction = 'update'
+ this.#hydrated = true
return this
}
- // ...else Update the existing page in RERUM
- const existingLine = await fetch(this.id).then(res => res.json())
- .catch(err => {
- if (err.status === 404) {
- // If the line doesn't exist, we can create it
- return null
+ // ...else Update the existing line in RERUM
+ const existingLine = await fetch(this.id).then(async (resp) => {
+ if (resp.ok) return resp.json()
+ if (resp.status === 404) return null
+ let rerumErrorMessage
+ try {
+ rerumErrorMessage = `${resp.status ?? 500}: ${this.id} - ${await resp.text()}`
+ } catch (e) {
+ rerumErrorMessage = `500: ${this.id} - A RERUM error occurred`
}
- throw new Error(`Failed to fetch existing Line from RERUM: ${err.message}`)
+ const err = new Error(rerumErrorMessage)
+ err.status = 502
+ throw err
})
-
- if (!existingLine) {
+ .catch(err => {
+ if (err.status === 502) throw err
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
+ })
+ if (!(existingLine?.id || existingLine?.["@id"])) {
// This id doesn't exist in RERUM, so we need to create it
this.#tinyAction = 'create'
}
+ // Skip RERUM update if no content changes detected
+ // Uses hasAnnotationChanges from shared.js instead of a private Class method for testability.
+ if (existingLine && !hasAnnotationChanges(existingLine, lineAsAnnotation)) {
+ this.#hydrated = true
+ return this // Return without versioning
+ }
+ const action = this.#tinyAction === 'create' ? 'save' : this.#tinyAction
const updatedLine = existingLine ? { ...existingLine, ...lineAsAnnotation } : lineAsAnnotation
- const newURI = await databaseTiny[this.#tinyAction](updatedLine).then(res => res.id)
+ const newURI = await databaseTiny[action](updatedLine).then(res => res.id)
.catch(err => {
throw new Error(`Failed to update Line in RERUM: ${err.message}`)
})
this.id = newURI
this.#tinyAction = 'update'
+ this.#hydrated = true
+ return this
+ }
+
+ #updateLineForPage() {
+ return {
+ id: this.id,
+ type: this.type ?? "Annotation",
+ target: this.target
+ }
+ }
+
+ /**
+ * Resolve the RERUM URI of the Line and sync Line properties with the Annotation properties.
+ * The RERUM data will take preferences and overwrite any properties that are already set.
+ * Only RERUM URIs are supported.
+ */
+ async #loadAnnotationDataFromRerum() {
+ if (this.id.startsWith?.(process.env.RERUMIDPREFIX)) {
+ const rawLineData = await fetch(this.id).then(async (resp) => {
+ if (resp.ok) return resp.json()
+ // The response from RERUM indicates a failure, likely with a specific code and textual body
+ let rerumErrorMessage
+ try {
+ rerumErrorMessage = `${resp.status ?? 500}: ${this.id} - ${await resp.text()}`
+ } catch (e) {
+ rerumErrorMessage = `500: ${this.id} - A RERUM error occurred`
+ }
+ const err = new Error(rerumErrorMessage)
+ err.status = 502
+ throw err
+ })
+ .catch(err => {
+ if (err.status === 502) throw err
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
+ })
+ if (!(rawLineData.id || rawLineData["@id"])) {
+ // A 200 with garbled data, call it a fail
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
+ }
+ // We don't have Class getters and setters for these properties...
+ if ('body' in rawLineData) this.body = rawLineData.body
+ if ('target' in rawLineData) this.target = rawLineData.target
+ if (rawLineData.creator) this.creator = rawLineData.creator
+ if (rawLineData.motivation) this.motivation = rawLineData.motivation
+ if (rawLineData.label) this.label = rawLineData.label
+ if (rawLineData.type) this.type = rawLineData.type
+ this.#tinyAction = 'update'
+ this.#hydrated = true
+ }
return this
}
- /**
+
+ /**
* Check the Project for any RERUM documents and either upgrade a local version or overwrite the RERUM version.
* @returns {Promise} Resolves to the updated Layer object as stored in Project.
*/
- async update() {
- if (this.#tinyAction === 'update' || this.body) {
- this.#setRerumId()
- await this.#saveLineToRerum()
- }
- return this.#updateLineForPage()
-}
-
-#updateLineForPage() {
- return {
- id: this.id,
- type: this.type ?? "Annotation",
- target: this.target
+ async update() {
+ if (this.#tinyAction === 'update' || this.body) {
+ this.#setRerumId()
+ await this.#saveLineToRerum()
+ }
+ return this.#updateLineForPage()
}
-}
+
/**
* Updates the textual content of the annotation body.
*
@@ -115,14 +182,13 @@ export default class Line {
if (typeof text !== 'string') throw new Error('Text content must be a string')
if (!this.body) this.body = "" // simple variant for no body
this.creator = options.creator
- const isVariantTextualBody = body => typeof (body?.chars ?? body?.['cnt:asChars'] ?? body?.value ?? body) === 'string'
if (Array.isArray(this.body)) {
const textualBodies = this.body.filter(body => isVariantTextualBody(body))
if (textualBodies.length !== 1) throw new Error(textualBodies.length > 1 ? 'Multiple textual bodies found. Cannot determine which one to update.' : 'No textual body found in the array to update.')
const textualBody = textualBodies[0]
- const currentValue = textualBody.value ?? textualBody.chars ?? textualBody['cnt:asChars'] ?? textualBody
+ const currentValue = extractTextValue(textualBody)
if (currentValue === text) return this
Object.assign(textualBody, { type: 'TextualBody', value: text, format: options.format ?? "text/plain", language: options.language })
// discard Annotation-level options if only one body entry is modified.
@@ -130,7 +196,7 @@ export default class Line {
}
if (isVariantTextualBody(this.body)) {
- const currentValue = this.body.chars ?? this.body['cnt:asChars'] ?? this.body.value ?? this.body
+ const currentValue = extractTextValue(this.body)
if (currentValue === text) return this
this.body = { type: 'TextualBody', value: text, format: options.format ?? "text/plain", language: options.language }
return this.update()
@@ -139,12 +205,48 @@ export default class Line {
throw new Error('Unexpected body format. Cannot update text.')
}
- async updateBounds({x, y, w, h}) {
- if (!x || !y || !w || !h) {
- throw new Error('Bounds ({x,y,w,h}) must be provided')
+ updateTargetXYWH(target, x, y, w, h) {
+ if (typeof target === "object" && target.selector?.value) {
+ const hasPixel = target.selector.value.includes("pixel:")
+ const prefix = hasPixel ? "xywh=pixel:" : "xywh="
+ return {
+ ...target,
+ selector: {
+ ...target.selector,
+ value: `${prefix}${x},${y},${w},${h}`
+ }
+ }
+ }
+
+ if (typeof target === "object" && target.id) {
+ const hasPixel = /xywh=pixel/.test(target.id)
+ const prefix = hasPixel ? "#xywh=pixel:" : "#xywh="
+ return {
+ ...target,
+ id: target.id.replace(/#xywh(=pixel)?:?.*/, `${prefix}${x},${y},${w},${h}`)
+ }
+ }
+
+ if (typeof target === "string") {
+ const hasPixel = /xywh=pixel/.test(target)
+ const prefix = hasPixel ? "#xywh=pixel:" : "#xywh="
+ if (target.includes("#xywh")) {
+ return target.replace(/#xywh(=pixel)?:?.*/, `${prefix}${x},${y},${w},${h}`)
+ }
+ return `${target}#xywh=pixel:${x},${y},${w},${h}`
+ }
+ throw new Error("Unsupported target format")
+ }
+
+ async updateBounds({x, y, w, h}, options = {}) {
+ const isValidBound = v => (Number.isInteger(v) && v >= 0) || (typeof v === 'string' && /^\d+$/.test(v))
+ if (!isValidBound(x) || !isValidBound(y) || !isValidBound(w) || !isValidBound(h)) {
+ throw new Error('Bounds ({x,y,w,h}) must be non-negative integers')
}
+ x = parseInt(x, 10); y = parseInt(y, 10); w = parseInt(w, 10); h = parseInt(h, 10)
+ if (options.creator) this.creator = options.creator
this.target ??= ''
- const newTarget = `${this.target.split('=')[0]}=${x},${y},${w},${h}`
+ const newTarget = this.updateTargetXYWH(this.target, x, y, w, h)
if (this.target === newTarget) {
return this
}
@@ -152,25 +254,44 @@ export default class Line {
return this.update()
}
- asJSON(isLD) {
- return isLD ? {
- '@context': 'http://iiif.io/api/presentation/3/context.json',
- id: this.id,
- type: 'Annotation',
- motivation: this.motivation ?? 'transcribing',
- target: this.target,
- body: this.body,
- } : {
- id: this.id,
- body: this.body ?? '',
- target: this.target ?? '',
+ async asJSON(isLD) {
+ if (!this.#hydrated) await this.#loadAnnotationDataFromRerum()
+ let result
+ if (isLD) {
+ result = {
+ '@context': 'http://iiif.io/api/presentation/3/context.json',
+ id: this.id,
+ type: 'Annotation',
+ motivation: this.motivation ?? 'transcribing',
+ target: this.target,
+ body: this.body,
+ }
+ if (this.creator) result.creator = this.creator
+ }
+ else {
+ result = {
+ id: this.id,
+ body: this.body ?? '',
+ target: this.target ?? '',
+ }
}
+ return result
}
asHTML() {
return Promise.resolve('This is the HTML document content.')
}
+ /**
+ * Extract the plain text content from the Line body.
+ *
+ * @returns {string} The text content of the Line, or empty string if no textual body exists.
+ */
+ async asTextBlob() {
+ if (!this.#hydrated) await this.#loadAnnotationDataFromRerum()
+ return extractTextFromAnnotationBody(this.body)
+ }
+
async delete() {
if (this.#tinyAction === 'update') {
await databaseTiny.remove(this.id)
@@ -179,3 +300,49 @@ export default class Line {
return true
}
}
+
+/**
+ * Extract the text value from a textual body entry.
+ * Priority: value → cnt:asChars → chars → raw body.
+ *
+ * @param {string|Object} body - A textual body entry.
+ * @returns {string|*} The text string if a known text property exists, otherwise the raw body value.
+ */
+function extractTextValue(body) {
+ return body?.value ?? body?.['cnt:asChars'] ?? body?.chars ?? body
+}
+
+/**
+ * Determine whether the given body entry is a textual body variant.
+ * Recognizes: plain string, object with `value`, `chars`, or `cnt:asChars` string property.
+ *
+ * @param {*} body - A single body entry (string, object, or other).
+ * @returns {boolean} True if the body is a textual body variant.
+ */
+function isVariantTextualBody(body) {
+ return typeof extractTextValue(body) === 'string'
+}
+
+/**
+ * Extract the plain text content from raw Annotation body data
+ * Handles all W3C Web Annotation body format variants:
+ * - Plain string body
+ * - Object body with `value`, `chars`, or `cnt:asChars` property
+ * - Array of bodies (returns text from the first textual body found)
+ * - null/undefined/empty array bodies return empty string
+ *
+ * @param {*} body - A single body entry (string, object, array, or other).
+ * @returns {string} The text content of the annotation, or empty string if no textual body exists.
+ */
+function extractTextFromAnnotationBody(body) {
+ if (body === null || body === undefined) return ''
+ if (Array.isArray(body)) {
+ const textualBody = body.find(b => isVariantTextualBody(b))
+ if (!textualBody) return ''
+ return extractTextValue(textualBody)
+ }
+ if (isVariantTextualBody(body)) {
+ return extractTextValue(body)
+ }
+ return ''
+}
diff --git a/classes/Page/Page.js b/classes/Page/Page.js
index 85bd1912..11a7f35e 100644
--- a/classes/Page/Page.js
+++ b/classes/Page/Page.js
@@ -1,11 +1,14 @@
import dbDriver from "../../database/driver.js"
-import { handleVersionConflict, fetchUserAgent } from "../../utilities/shared.js"
+import { fetchUserAgent } from "../../utilities/shared.js"
import ProjectFactory from "../Project/ProjectFactory.js"
+import Line from "../Line/Line.js"
const databaseTiny = new dbDriver("tiny")
export default class Page {
#tinyAction = 'create'
+ #hydrated = false
+ #itemsResolved = false
#setRerumId() {
if (!this.id.startsWith(process.env.RERUMIDPREFIX)) {
this.id = `${process.env.RERUMIDPREFIX}${this.id.split("/").pop()}`
@@ -54,7 +57,7 @@ export default class Page {
let canvasLabel = canvas.label ?? `Page ${canvas.id.split('/').pop()}`
const page = {
data: {
- "@context": "http://www.w3.org/ns/anno.jsonld",
+ "@context": "http://iiif.io/api/presentation/3/context.json",
id,
type: "AnnotationPage",
label: ProjectFactory.getLabelAsString(canvasLabel),
@@ -69,15 +72,97 @@ export default class Page {
return new Page(layerId, page.data)
}
+ /**
+ * Resolve the RERUM URI of the Page and sync Page properties with the AnnotationPage properties.
+ * The RERUM data will take preferences and overwrite any properties that are already set.
+ * Only RERUM URIs are supported.
+ */
+ async #loadAnnotationPageDataFromRerum() {
+ if (this.id.startsWith?.(process.env.RERUMIDPREFIX)) {
+ const rawPageData = await fetch(this.id).then(async (resp) => {
+ if (resp.ok) return resp.json()
+ // The response from RERUM indicates a failure, likely with a specific code and textual body
+ let rerumErrorMessage
+ try {
+ rerumErrorMessage = `${resp.status ?? 500}: ${this.id} - ${await resp.text()}`
+ } catch (e) {
+ rerumErrorMessage = `500: ${this.id} - A RERUM error occurred`
+ }
+ const err = new Error(rerumErrorMessage)
+ err.status = 502
+ throw err
+ })
+ .catch(err => {
+ if (err.status === 502) throw err
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
+ })
+ if (!(rawPageData.id || rawPageData["@id"])) {
+ // A 200 with garbled data, call it a fail
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
+ }
+ this.#tinyAction = 'update'
+ this.id = rawPageData.id ?? rawPageData["@id"] ?? this.id
+ if ('target' in rawPageData) this.target = rawPageData.target
+ if ('items' in rawPageData && !this.#itemsResolved) this.items = rawPageData.items
+ if (rawPageData.creator) this.creator = rawPageData.creator
+ if (rawPageData.label) this.label = ProjectFactory.getLabelAsString(rawPageData.label)
+ if ('partOf' in rawPageData) this.partOf = Array.isArray(rawPageData.partOf) ? rawPageData.partOf[0]?.id ?? rawPageData.partOf[0] : rawPageData.partOf
+ if ('prev' in rawPageData) this.prev = rawPageData.prev
+ if ('next' in rawPageData) this.next = rawPageData.next
+ this.#hydrated = true
+ }
+ return this
+ }
+
+ /**
+ * Resolve all annotations in this Page's items array by fetching their full data from RERUM.
+ * Once resolved, replaces this.items with the resolved data.
+ *
+ * @returns {Promise} Array of fully resolved annotation objects
+ */
+ async #loadAnnotationPageItemsFromRerum() {
+ if (!Array.isArray(this.items)) return []
+ // Process all items in parallel for better performance
+ const resolvedItems = await Promise.all(
+ this.items.map(async (item) => {
+ // If item is a string, it's an annotation ID - fetch from RERUM
+ let lineRef
+ // target is required by Line constructor but will be overwritten by RERUM data
+ // since #hydrated is false, Line.asJSON() always fetches from RERUM.
+ if (item?.id) {
+ lineRef = { ...item, target: item.target ?? "pending-resolution" }
+ }
+ else if (typeof item === "string") lineRef = { "id": item, "target":"pending-resolution" }
+ else return { id: item?.id ?? item, error: "Unrecognized Page item format" }
+ let line = await new Line(lineRef).asJSON(true).catch(err => {
+ return { id: lineRef.id, error: err.message }
+ })
+ delete line["@context"]
+ return line
+ })
+ )
+ this.items = resolvedItems
+ this.#itemsResolved = true
+ return resolvedItems
+ }
+
async #savePageToRerum() {
const prev = this.prev ?? null
const next = this.next ?? null
const pageAsAnnotationPage = {
- "@context": "http://www.w3.org/ns/anno.jsonld",
+ "@context": "http://iiif.io/api/presentation/3/context.json",
id: this.id,
type: "AnnotationPage",
label: { "none": [this.label] },
- items: this.items ?? [],
+ items: (this.items ?? []).map(item =>
+ typeof item === 'object' && item.id
+ ? { id: item.id, type: item.type ?? 'Annotation' }
+ : item
+ ),
prev,
next,
creator: await fetchUserAgent(this.creator),
@@ -85,37 +170,47 @@ export default class Page {
partOf: [{ id: this.partOf, type: "AnnotationCollection" }]
}
if (this.#tinyAction === 'create') {
- const saved = await databaseTiny.save(pageAsAnnotationPage)
+ await databaseTiny.save(pageAsAnnotationPage)
.catch(err => {
console.error(err, pageAsAnnotationPage)
throw new Error(`Failed to save Page to RERUM: ${err.message}`)
})
this.#tinyAction = 'update'
+ this.#hydrated = true
return this
}
// ...else Update the existing page in RERUM
- const existingPage = await fetch(this.id).then(res => res.json())
- if (!existingPage) {
- throw new Error(`Failed to find Page in RERUM: ${this.id}`)
- }
- const updatedPage = { ...existingPage, ...pageAsAnnotationPage }
-
- // Handle optimistic locking version if available
- try {
- await databaseTiny.overwrite(updatedPage)
- return this
- } catch (err) {
- if (err.status === 409) {
- throw handleVersionConflict(null, err)
+ const existingPage = await fetch(this.id).then(async (resp) => {
+ if (resp.ok) return resp.json()
+ let rerumErrorMessage
+ try {
+ rerumErrorMessage = `${resp.status ?? 500}: ${this.id} - ${await resp.text()}`
+ } catch (e) {
+ rerumErrorMessage = `500: ${this.id} - A RERUM error occurred`
}
+ const err = new Error(rerumErrorMessage)
+ err.status = 502
throw err
+ })
+ .catch(err => {
+ if (err.status === 502) throw err
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
+ })
+ if (!(existingPage?.id || existingPage?.["@id"])) {
+ const genericRerumNetworkError = new Error(`500: ${this.id} - A RERUM error occurred`)
+ genericRerumNetworkError.status = 502
+ throw genericRerumNetworkError
}
+ const updatedPage = { ...existingPage, ...pageAsAnnotationPage }
+ await databaseTiny.overwrite(updatedPage)
+ this.#hydrated = true
+ return this
}
/**
* Check the Project for any RERUM documents and either upgrade a local version or overwrite the RERUM version.
- * FIXME: This will save to RERUM even if there has been no content change
- * The rerum variable below is true if the content has changed.
*
* @returns {Promise} Resolves to the updated Layer object as stored in Project.
*/
@@ -128,6 +223,14 @@ export default class Page {
return this.#formatPageForProject()
}
+ /**
+ * Resolve all item references in this Page by fetching full annotation data from RERUM.
+ * @returns {Promise} Array of fully resolved annotation objects.
+ */
+ async resolvePageItems() {
+ return this.#loadAnnotationPageItemsFromRerum()
+ }
+
asProjectPage() {
return this.#formatPageForProject()
}
@@ -141,6 +244,46 @@ export default class Page {
}
}
+ /**
+ * Returns a JSON representation of the Page as a W3C AnnotationPage.
+ * @param {boolean} isLD - If true, returns JSON-LD format with @context and type. If false, returns a simple object.
+ * @returns {Object} The Page as JSON.
+ */
+ async asJSON(isLD) {
+ if (!this.#hydrated && this.id?.startsWith?.(process.env.RERUMIDPREFIX)) {
+ await this.#loadAnnotationPageDataFromRerum()
+ }
+ let result
+ if (isLD) {
+ result = {
+ '@context': 'http://iiif.io/api/presentation/3/context.json',
+ id: this.id,
+ type: 'AnnotationPage',
+ label: { "none": [this.label] },
+ target: this.target,
+ partOf: Array.isArray(this.partOf)
+ ? this.partOf
+ : [{ id: this.partOf, type: "AnnotationCollection" }],
+ items: this.items ?? [],
+ prev: this.prev ?? null,
+ next: this.next ?? null
+ }
+ if (this.creator) result.creator = this.creator
+ }
+ else {
+ result = {
+ id: this.id,
+ label: this.label,
+ target: this.target,
+ items: this.items ?? [],
+ partOf: this.partOf,
+ prev: this.prev ?? null,
+ next: this.next ?? null
+ }
+ }
+ return result
+ }
+
async delete() {
if (this.#tinyAction === 'update') {
// associated Annotations in RERUM will be left intact
diff --git a/classes/Project/Project.js b/classes/Project/Project.js
index 43f9ce22..6e6f6ed3 100644
--- a/classes/Project/Project.js
+++ b/classes/Project/Project.js
@@ -62,13 +62,17 @@ export default class Project {
let user = await userObj.getByEmail(email)
const roles = this.parseRoles(rolesString)
const projectTitle = this.data?.label ?? this.data?.title ?? 'TPEN Project'
- let message = `You have been invited to the TPEN project ${projectTitle}.
+ let message = `You have been invited to the TPEN project ${projectTitle}.
View project here .`
- if (user) {
+
+ if (user && !user.inviteCode) {
+ // Existing registered TPEN3 user (not a temp user)
// FIXME this does not have the functionality of an 'invite'.
await this.inviteExistingTPENUser(user._id, roles)
- }
+ }
else {
+ // Either no user exists, or user is a temp user (has inviteCode)
+ // In both cases, we need to send invite email with accept/reject links
const inviteData = await this.inviteNewTPENUser(email, roles)
const returnTo = encodeURIComponent(`${process.env.TPENINTERFACES}project?projectID=${this.data._id}&inviteCode=${inviteData.tpenUserID}`)
// Signup starting at the TPEN3 public site
@@ -77,9 +81,9 @@ export default class Project {
const decline = `${process.env.TPENINTERFACES}project/decline?email=${encodeURIComponent(email)}&user=${inviteData.tpenUserID}&project=${this.data._id}&projectTitle=${encodeURIComponent(projectTitle)}`
message += `
- Click the button below to get started with your project
+ Click the button below to get started with your project
Get Started
- or copy the following link into your web browser ${signup}
+ or copy the following link into your web browser ${signup}
This E-mail address may be visible to members of the project so that they know
@@ -89,7 +93,8 @@ export default class Project {
`
}
- await sendMail(email, `Invitation to ${projectTitle}`, message)
+ const recipientName = user?.profile?.displayName || email.split("@")[0]
+ await sendMail(email, `Invitation to ${projectTitle}`, message, recipientName)
return this
} catch (error) {
throw error
@@ -161,9 +166,27 @@ export default class Project {
}
/**
- * Add a new temporary user to the users collection and send the invite E-mail.
- */
+ * Add a new temporary user to the users collection and send the invite E-mail.
+ * If a temp user with this email already exists (invited to another project), reuse it.
+ * This allows users invited to multiple projects to use any invite link to complete signup.
+ */
async inviteNewTPENUser(email, roles) {
+ // Check if a temp user already exists with this email
+ const existingUserLookup = new User()
+ let tempUser = null
+ try {
+ tempUser = await existingUserLookup.getByEmail(email)
+ } catch (err) {
+ // No user found - that's fine, we'll create one
+ }
+
+ if (tempUser && tempUser.inviteCode) {
+ // Reuse existing temp user - just add to this project's group
+ await this.inviteExistingTPENUser(tempUser._id, roles)
+ return { "tpenUserID": tempUser._id, "tpenGroupID": this.data.group }
+ }
+
+ // Create new temp user
const user = new User()
const inviteCode = user._id
const agent = `https://store.rerum.io/v1/id/${user._id}`
@@ -173,7 +196,7 @@ export default class Project {
await user.save()
// FIXME this does not have the functionality of an 'invite'.
await this.inviteExistingTPENUser(user._id, roles)
- return { "tpenUserID":user._id, "tpenGroupID":this.data.group }
+ return { "tpenUserID": user._id, "tpenGroupID": this.data.group }
}
/**
@@ -195,20 +218,57 @@ export default class Project {
}
/**
- * Remove a member from the Project Group.
- * If the member is an invitee (temporary) User, delete that User from the db.
+ * Remove a member from the Project Group
+ * Sends a confirmation email to the removed member (non-blocking).
+ * The member may be leaving voluntarily themselves or is being removed by a project admin.
+ * If the user is an orphaned temp user after they are removed from the project, delete the user.
*
- * @param userId The User/member _id to remove from the Group and perhaps delete from the db.
+ * @param {string} userId The User/member _id to remove from the Group and perhaps delete from the db.
+ * @param {boolean} voluntary Whether the user is leaving voluntarily (true) or being removed by admin (false).
*/
- async removeMember(userId) {
+ async removeMember(userId, voluntary = false) {
try {
+ if (!this.data?.group) {
+ await this.loadProject()
+ }
const group = new Group(this.data.group)
- await group.removeMember(userId)
- await group.update()
- // Don't leave orphaned invitees in the db.
- const member = new User(userId)
- const memberData = await member.getSelf()
- if (memberData?.inviteCode) member.delete()
+ const user = new User(userId)
+ const userData = await user.getSelf()
+ await group.removeMember(userId, voluntary)
+ const projectTitle = this.data?.label ?? this.data?.title ?? 'TPEN Project'
+ try {
+ // Send confirmation email (non-blocking)
+
+ if (userData?.email) {
+ const subject = voluntary
+ ? `You left ${projectTitle}`
+ : `Removed from ${projectTitle}`
+
+ const message = voluntary
+ ? `You have successfully left the project ${projectTitle} .
+Your contributions to the project remain attributed to you, but you may no longer have access to some TPEN3 data. *Access to RERUM data is not affected.
+If you wish to rejoin, please contact a project administrator.
`
+ : `You have been removed from the project ${projectTitle} by a project administrator.
+Your contributions to the project remain attributed to you, but you no longer have access to some TPEN3 data. *Access to RERUM data is not affected.
+If you believe this was done in error, please contact a project administrator.
`
+
+ const recipientName = userData?.profile?.displayName || userData?.email?.split("@")[0]
+ await sendMail(userData.email, subject, message, recipientName)
+ }
+ } catch (emailError) {
+ console.error("Failed to send removal confirmation email:", emailError)
+ }
+ try {
+ // Don't leave orphaned invitees in the db, delete if they have no remaining memberships (non-blocking)
+ if (userData.inviteCode) {
+ const remainingGroups = await Group.getGroupsByMember(userId)
+ if (remainingGroups.length === 0) {
+ await user.delete()
+ }
+ }
+ } catch (cleanupError) {
+ console.error("Failed to remove orphaned temp user:", cleanupError)
+ }
return this
} catch (error) {
throw {
diff --git a/classes/Project/ProjectFactory.js b/classes/Project/ProjectFactory.js
index 9e6b9213..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:
@@ -736,12 +737,15 @@ 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)
+ 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 = {
@@ -1003,7 +1007,7 @@ export default class ProjectFactory {
},
{
$set: {
- roles: { $mergeObjects: [{ $ifNull: ['$thisGroup.customRoles', {}] }, Group.defaultRoles] },
+ roles: { $mergeObjects: [Group.defaultRoles, { $ifNull: ['$thisGroup.customRoles', {}] }] },
}
},
{
diff --git a/classes/User/User.js b/classes/User/User.js
index 5c076624..a1311956 100644
--- a/classes/User/User.js
+++ b/classes/User/User.js
@@ -1,4 +1,5 @@
import dbDriver from "../../database/driver.js"
+import Group from "../Group/Group.js"
const database = new dbDriver("mongo")
export default class User {
@@ -25,7 +26,10 @@ export default class User {
}
async getSelf() {
- return await (this.data ?? this.#loadFromDB().then(u => u.data))
+ if (!this.data) {
+ await this.#loadFromDB()
+ }
+ return this.data
}
async getPublicInfo() {
@@ -58,6 +62,32 @@ export default class User {
})
}
+ /**
+ * Merge a temporary user's group memberships into this user and delete the temp user.
+ * Used when a user signs up independently after being invited to projects.
+ * @param {Object} tempUserData - The temporary user's data (must have inviteCode field)
+ * @throws {Error} If tempUserData does not have an inviteCode (not a temporary user)
+ */
+ async mergeFromTemporaryUser(tempUserData) {
+ if (!tempUserData?.inviteCode) {
+ throw new Error("Cannot merge: provided user is not a temporary user")
+ }
+
+ // Find all groups containing the temp user
+ const groups = await Group.getGroupsByMember(tempUserData._id)
+
+ // Transfer membership in each group
+ for (const groupData of groups) {
+ const group = new Group(groupData._id)
+ group.data = groupData
+ await group.transferMembership(tempUserData._id, this._id)
+ }
+
+ // Delete the temporary user
+ const tempUser = new User(tempUserData._id)
+ await tempUser.delete()
+ }
+
static async create(data) {
// POST requests
if (!data) {
@@ -107,6 +137,21 @@ export default class User {
if (!this.data.profile?.displayName) {
throw new Error("User must have a profile with a displayName")
}
+
+ // Check for duplicate email
+ try {
+ const existingUser = await this.getByEmail(this.data.email)
+ if (existingUser && existingUser._id !== this._id) {
+ throw new Error(`User with email ${this.data.email} already exists`)
+ }
+ } catch (err) {
+ // getByEmail throws if not found - that's ok, continue
+ // But re-throw if it's our duplicate error or other errors
+ if (err.message.includes("already exists") || (!err.message.includes("not found") && err.message !== "No email provided")) {
+ throw err
+ }
+ }
+
// save user to database
return database.save({ _id: this._id, ...this.data }, "users")
}
diff --git a/classes/User/__tests__/unit.test.js b/classes/User/__tests__/unit.test.js
index d9893ffb..9ca2396b 100644
--- a/classes/User/__tests__/unit.test.js
+++ b/classes/User/__tests__/unit.test.js
@@ -154,3 +154,28 @@ describe("GET /my/projects #user_class", () => {
expect(response.status).toBe(401)
})
})
+
+describe("GET /my/projects with no projects #user_class", () => {
+ beforeAll(() => {
+ jest.spyOn(User.prototype, "getProjects").mockResolvedValue([])
+ jest.spyOn(User.prototype, "getSelf").mockResolvedValue({
+ _id: "123456",
+ _lastModified: null
+ })
+ })
+
+ afterAll(() => {
+ jest.restoreAllMocks()
+ })
+
+ it.skip("should return 200 with empty projects array when user has no projects", async () => {
+ const response = await request(app)
+ .get("/my/projects")
+ .set("Authorization", `Bearer ${token}`)
+ expect(response.status).toBe(200)
+ expect(response.body).toHaveProperty("projects")
+ expect(Array.isArray(response.body.projects)).toBe(true)
+ expect(response.body.projects.length).toBe(0)
+ expect(response.body.metrics).toBeNull()
+ })
+})
diff --git a/config.env b/config.env
index 4bb83fc9..1bb3ac5a 100644
--- a/config.env
+++ b/config.env
@@ -20,6 +20,7 @@ DOWN=false
TPENPROJECTS=projects
TPENGROUPS=groups
TPENUSERS=users
+TPENCOLUMNS=columns
# Local Development Database Defaults
# These work out-of-the-box for local Docker/MongoDB/MariaDB
diff --git a/database/maria/controller.js b/database/maria/controller.js
index 977589be..89e2e106 100644
--- a/database/maria/controller.js
+++ b/database/maria/controller.js
@@ -6,7 +6,7 @@
* https://github.com/thehabes
*/
-import mariadb from 'mariadb'
+import * as mariadb from 'mariadb'
class DatabaseController {
constructor(connect=false) {
diff --git a/feedback/feedbackController.js b/feedback/feedbackController.js
index 4403a23a..02a239d1 100644
--- a/feedback/feedbackController.js
+++ b/feedback/feedbackController.js
@@ -10,6 +10,9 @@ import { isSuspiciousValueString } from "../utilities/checkIfSuspicious.js"
* @returns 200 if feedback is submitted successfully, 204 if no feedback is provided, or an error response if the submission fails.
*/
export async function submitFeedback(req, res) {
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
const user = req.user ? `${req.user.profile.displayName} (${req.user._id})` : 'Anonymous'
const { page, feedback } = req.body
if (!feedback) return res.status(204).send()
@@ -18,7 +21,7 @@ export async function submitFeedback(req, res) {
await createGitHubIssue('Feedback', `Feedback from ${user}`, `Page: ${page}\n\nFeedback: ${sanitizeUserInput(feedback)}`)
res.status(200).json({ message: 'Feedback submitted successfully' })
} catch (error) {
- respondWithError(res, error.status ?? 500, error.message ?? 'Failed to submit feedback')
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Failed to submit feedback')
}
}
@@ -30,6 +33,9 @@ export async function submitFeedback(req, res) {
* @returns 200 if the bug report is submitted successfully, 204 if no bug description is provided, or an error response if the submission fails.
*/
export async function submitBug(req, res) {
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
const user = req.user ? `${req.user.profile.displayName} (${req.user._id})` : 'Anonymous'
const { page, bugDescription } = req.body
if (!bugDescription) return res.status(204).send()
@@ -38,7 +44,7 @@ export async function submitBug(req, res) {
await createGitHubIssue('Bug Report', `Bug reported by ${user}`, `Page: ${page}\n\nBug: ${sanitizeUserInput(bugDescription)}`)
res.status(200).json({ message: 'Bug report submitted successfully' })
} catch (error) {
- respondWithError(res, error.status ?? 500, error.message ??'Failed to submit bug report')
+ return respondWithError(res, error.status ?? 500, error.message ??'Failed to submit bug report')
}
}
diff --git a/index.js b/index.js
index efcb78bf..a7246d34 100644
--- a/index.js
+++ b/index.js
@@ -16,64 +16,13 @@ export function respondWithHTML(res) {
res.status(200).sendFile('index.html').end()
}
-import { JSDOM } from 'jsdom'
-import DOMPurify from 'dompurify'
-
-const window = new JSDOM('').window
-const purify = DOMPurify(window)
-
-import fs from 'fs'
-import { marked } from 'marked'
-
-marked.use({
- gfm: true,
-})
-
-router
- .route("/api")
- .get(function (_req,res) {
- fs.readFile('API.md', 'utf8', (err, data) => {
- if (err) {
- respondWithError(res, 500, 'Failed to read API.md')
- return
- }
- res.format({
- html: () => res.send(makeCleanFileFromMarkdown(data))
- })
- })
- })
-
router
.route("/")
.get(function (_req, res, next) {
respondWithHTML(res)
})
.all((_req, res, next) => {
- respondWithError(res, 404, 'There is nothing for you here.')
+ return respondWithError(res, 404, 'There is nothing for you here.')
})
export { router as default }
-
-function makeCleanFileFromMarkdown(file) {
- const sanitizedContent = purify.sanitize(marked.parse(file))
- return `
-
-
-
-
-
-
- API Documentation
-
-
-
-
-
-
-
-
-
- `
-}
diff --git a/layer/index.js b/layer/index.js
index f2ba12c2..fef114f8 100644
--- a/layer/index.js
+++ b/layer/index.js
@@ -7,7 +7,8 @@ import cors from 'cors'
import common_cors from '../utilities/common_cors.json' with {type: 'json'}
import Project from '../classes/Project/Project.js'
import Layer from '../classes/Layer/Layer.js'
-import { findPageById, findLayerById, updateLayerAndProject, respondWithError } from '../utilities/shared.js'
+import { findPageById, findLayerById, updateLayerAndProject, respondWithError, handleVersionConflict } from '../utilities/shared.js'
+import { ACTIONS, ENTITIES, SCOPES } from '../project/groups/permissions_parameters.js'
const router = express.Router({ mergeParams: true })
@@ -18,28 +19,9 @@ router.route('/:layerId')
.get(async (req, res) => {
const { projectId, layerId } = req.params
try {
- const layer = await findLayerById(layerId, projectId, true)
- if (!layer) {
- respondWithError(res, 404, 'No layer found with that ID.')
- return
- }
- if (layer.id?.startsWith(process.env.RERUMIDPREFIX)) {
- // If the page is a RERUM document, we need to fetch it from the server
- res.status(200).json(layer)
- return
- }
- // Make this internal Layer look more like a RERUM AnnotationCollection
- const layerAsCollection = {
- '@context': 'http://www.w3.org/ns/anno.jsonld',
- id: layer.id,
- type: 'AnnotationCollection',
- label: { none: [layer.label] },
- total: layer.pages.length,
- first: layer.pages.at(0).id,
- last: layer.pages.at(-1).id
- }
- if (layer.creator) layerAsCollection.creator = layer.creator
- return res.status(200).json(layerAsCollection)
+ const layer = await findLayerById(layerId, projectId)
+ const layerJson = await layer.asJSON(true)
+ return res.status(200).json(layerJson)
} catch (error) {
console.error(error)
return respondWithError(res, error.status ?? 500, error.message ?? 'Internal Server Error')
@@ -47,49 +29,63 @@ router.route('/:layerId')
})
.put(auth0Middleware(), screenContentMiddleware(), async (req, res) => {
const { projectId, layerId } = req.params
- let label = req.body?.label
const update = req.body
const providedPages = update?.pages
const user = req.user
+ if (!user) return respondWithError(res, 401, 'Not authenticated. Please provide a valid, unexpired Bearer token')
if (!projectId) return respondWithError(res, 400, 'Project ID is required')
if (!layerId) return respondWithError(res, 400, 'Layer ID is required')
try {
if (hasSuspiciousLayerData(req.body)) return respondWithError(res, 400, "Suspicious layer data will not be processed.")
- const project = await Project.getById(projectId)
- if (!project?._id) return respondWithError(res, 404, `Project '${projectId}' does not exist`)
- const layer = await findLayerById(layerId, projectId)
- if (!layer?.id) return respondWithError(res, 404, `Layer '${layerId}' not found in project`)
+ const project = new Project(projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.LAYER))) {
+ 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, project)
// Only update top-level properties that are present in the request
Object.keys(update ?? {}).forEach(key => {
layer[key] = update[key]
})
Object.keys(layer).forEach(key => {
if (layer[key] === undefined || layer[key] === null) {
- // Remove properties that are undefined or null. prev and next can be null
+ // Remove properties that are undefined or null. first and last can be null
if (key !== "first" && key !== "last") delete layer[key]
else layer[key] = null
}
})
- if (providedPages?.length === 0) providedPages = undefined
let pages = []
- if (providedPages && providedPages.length) {
- pages = await Promise.all(providedPages.map(p => findPageById(p.split("/").pop(), projectId) ))
+ if (providedPages && Array.isArray(providedPages) && providedPages.length > 0) {
+ pages = await Promise.all(providedPages.map(p => findPageById(p.split("/").pop(), projectId, project) ))
layer.pages = pages
}
await updateLayerAndProject(layer, project, user._id)
- res.status(200).json(layer)
+ const layerJson = await layer.asJSON(true)
+ res.status(200).json(layerJson)
} catch (error) {
console.error(error)
- return respondWithError(res, error.status ?? 500, error.message ?? 'Error updating layer')
+ // Handle version conflicts with optimistic locking
+ if (error.status === 409) {
+ if (res.headersSent) return
+ return handleVersionConflict(res, error)
+ } else {
+ if (res.headersSent) return
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Error updating layer')
+ }
}
})
.all((req, res) => {
- respondWithError(res, 405, 'Improper request method. Use GET instead.')
+ return respondWithError(res, 405, 'Improper request method. Use GET or PUT.')
})
// Route to create a new layer within a project
router.route('/').post(auth0Middleware(), screenContentMiddleware(), async (req, res) => {
const { projectId } = req.params
+ const user = req.user
+ if (!user) return respondWithError(res, 401, 'Not authenticated. Please provide a valid, unexpired Bearer token')
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, 'Request body is required')
+ }
const { label, canvases } = req.body
if (!projectId) return respondWithError(res, 400, 'Project ID is required')
if (!label || !Array.isArray(canvases) || canvases.length === 0) {
@@ -97,8 +93,11 @@ router.route('/').post(auth0Middleware(), screenContentMiddleware(), async (req,
}
try {
if (hasSuspiciousLayerData(req.body)) return respondWithError(res, 400, "Suspicious layer data will not be processed.")
- const project = await Project.getById(projectId)
- if (!project) return respondWithError(res, 404, 'Project does not exist')
+ const project = new Project(projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.CREATE, SCOPES.ALL, ENTITIES.LAYER))) {
+ return respondWithError(res, 403, 'You do not have permission to create layers in this project')
+ }
+ if (!project?.data) return respondWithError(res, 404, `Project ${projectId} was not found`)
const newLayer = Layer.build(projectId, label, canvases)
project.addLayer(newLayer.asProjectLayer())
await project.update()
diff --git a/line/index.js b/line/index.js
index 0f0b9213..724e54b4 100644
--- a/line/index.js
+++ b/line/index.js
@@ -4,8 +4,11 @@ import auth0Middleware from "../auth/index.js"
import screenContentMiddleware from '../utilities/checkIfSuspicious.js'
import { isSuspiciousJSON } from '../utilities/checkIfSuspicious.js'
import common_cors from '../utilities/common_cors.json' with {type: 'json'}
-import { respondWithError, getProjectById, findLineInPage, updatePageAndProject, findPageById, handleVersionConflict, withOptimisticLocking } from '../utilities/shared.js'
+import { respondWithError, findLineInPage, updatePageAndProject, findPageById, handleVersionConflict, withOptimisticLocking } from '../utilities/shared.js'
import Line from '../classes/Line/Line.js'
+import Column from '../classes/Column/Column.js'
+import Project from '../classes/Project/Project.js'
+import { ACTIONS, ENTITIES, SCOPES } from '../project/groups/permissions_parameters.js'
const router = express.Router({ mergeParams: true })
@@ -14,61 +17,50 @@ router.use(cors(common_cors))
// Load Line as temp line or from RERUM
router.get('/:lineId', async (req, res) => {
const { projectId, pageId, lineId } = req.params
- if (!lineId) {
- respondWithError(res, 400, 'Line ID is required.')
- return
- }
- if (!projectId || !pageId) {
- respondWithError(res, 400, 'Project ID and Page ID are required.')
- return
+ if (!(projectId && pageId && lineId)) {
+ return respondWithError(res, 400, 'Project ID, Page ID, and Line ID are required.')
}
try {
- if (lineId.startsWith(process.env.RERUMIDPREFIX)) {
- const resolved = await fetch(lineId)
- .then(resp => {
- if (resp.ok) return resp.json()
- else {
- return {"code": resp.status ?? 500, "message": resp.statusText ?? `Communication error with RERUM`}
- }
- }).catch(err => {
- return {"code": 500, "message": `Communication error with RERUM`}
- })
- if (resolved.id || resolved["@id"]) return res.json(resolved)
- else return respondWithError(res, resolved.code, resolved.message)
- }
- const projectData = (await getProjectById(projectId)).data
- if (!projectData) {
- respondWithError(res, 404, `Project with ID '${projectId}' not found`)
- return
- }
- const pageContainingLine = projectData.layers
+ const project = await Project.getById(projectId)
+ if (!project?.data) return respondWithError(res, 404, `Project ${projectId} was not found`)
+ const pageContainingLine = project.data.layers
.flatMap(layer => layer.pages)
.find(page => findLineInPage(page, lineId))
if (!pageContainingLine) {
- respondWithError(res, 404, `Page with ID '${pageId}' not found in project '${projectId}'`)
- return
+ return respondWithError(res, 404, `Line with ID '${lineId}' not found in Page '${pageId}'`)
}
const lineRef = findLineInPage(pageContainingLine, lineId)
- const line = (lineRef.id ?? lineRef).startsWith?.(process.env.RERUMIDPREFIX)
- ? await fetch(lineRef.id ?? lineRef).then(res => res.json())
- : new Line({ lineRef })
- res.json(line?.asJSON?.(true))
+ if (!lineRef) {
+ return respondWithError(res, 404, `Line with ID '${lineId}' not found in Page '${pageId}'`)
+ }
+ const line = new Line(lineRef)
+ if (req.query.text === 'blob') {
+ const textBlob = await line.asTextBlob()
+ return res.status(200).type('text/plain; charset=utf-8').send(textBlob)
+ }
+ const lineJson = await line.asJSON(true)
+ res.status(200).json(lineJson)
} catch (error) {
- res.status(error.status ?? 500).json({ error: error.message })
+ return respondWithError(res, error.status ?? 500, error.message)
}
})
// Add a new line/lines to an existing Page, save it in RERUM if it has body content.
router.post('/', auth0Middleware(), async (req, res) => {
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
try {
- const project = await getProjectById(req.params.projectId, res)
- if (!project) return
- const page = await findPageById(req.params.pageId, req.params.projectId)
- if (!page) return
+ const project = new Project(req.params.projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.CREATE, SCOPES.ALL, ENTITIES.LINE))) {
+ 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, project)
+ if (!req.body || (Array.isArray(req.body) && req.body.length === 0)) {
+ return respondWithError(res, 400, "Request body with line data is required")
+ }
const inputLines = Array.isArray(req.body) ? req.body : [req.body]
// Check each annotation for suspicious content. The body itself will be checked during the recursion.
for (const anno of inputLines) {
@@ -80,49 +72,60 @@ router.post('/', auth0Middleware(), async (req, res) => {
// This feels like a use case for /bulkCreate in RERUM. Make all these lines with one call.
for (const lineData of inputLines) {
newLine = Line.build(req.params.projectId, req.params.pageId, { ...lineData }, user.agent.split('/').pop())
- const existingLine = findLineInPage(page, newLine.id, res)
+ const existingLine = findLineInPage(page, newLine.id)
if (existingLine) {
- respondWithError(res, 409, `Line with ID '${newLine.id}' already exists in page '${req.params.pageId}'`)
- return
+ return respondWithError(res, 409, `Line with ID '${newLine.id}' already exists in page '${req.params.pageId}'`)
}
const savedLine = await newLine.update()
page.items.push(savedLine)
}
- const ifNewContent = (page.items && page.items.length)
+ const pageId = req.params.pageId.split('/').pop()
+ const pageProject = project.data.layers.flatMap(layer => layer.pages).find(p => p.id.split('/').pop() === pageId)
+ const saveWholeColumns = pageProject?.columns
await withOptimisticLocking(
- () => updatePageAndProject(page, project, user._id, ifNewContent),
+ () => updatePageAndProject(page, project, user._id),
(currentVersion) => {
if(!currentVersion || currentVersion.type !== 'AnnotationPage') {
- respondWithError(res, 409, 'Version conflict while updating the page. Please try again.')
- return
+ return respondWithError(res, 409, 'Version conflict while updating the page. Please try again.')
}
currentVersion.items = [...(currentVersion.items ?? []), ...(page.items ?? [])]
- Object.assign(page, currentVersion)
- return updatePageAndProject(page, project, user._id)
- })
- res.status(201).json(newLine.asJSON(true))
+ Object.assign(page, currentVersion)
+ return updatePageAndProject(page, project, user._id)
+ })
+ if (res.headersSent) return
+ // Updating the project again to save updated columns as columns is not handled in updatePageAndProject
+ if(saveWholeColumns) {
+ project.data.layers.flatMap(layer => layer.pages).find(p => p.id.split('/').pop() === pageId).columns = saveWholeColumns
+ await project.update()
+ }
+ const lineJson = await newLine.asJSON(true)
+ res.status(201).json(lineJson)
} catch (error) {
+ if (res.headersSent) return
// Handle version conflicts with optimistic locking
if (error.status === 409) {
return handleVersionConflict(res, error)
}
- respondWithError(res, error.status ?? 500, error.message ?? 'Internal Server Error')
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Internal Server Error')
}
})
// Update an existing line, including in RERUM
router.put('/:lineId', auth0Middleware(), screenContentMiddleware(), async (req, res) => {
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
try {
- const project = await getProjectById(req.params.projectId)
- const page = await findPageById(req.params.pageId, req.params.projectId)
+ const project = new Project(req.params.projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.LINE))) {
+ 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, project)
let oldLine = page.items?.find(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop())
if (!oldLine) {
- respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
- return
+ return respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
}
- if (!(oldLine.id && oldLine.target && oldLine.body)) oldLine = await fetch(oldLine.id).then(res => res.json())
+ if (!(oldLine.id && oldLine.target && oldLine.body)) oldLine = await fetch(oldLine.id).then(resp => resp.json())
const line = new Line(oldLine)
Object.assign(line, req.body)
const updatedLine = await line.update()
@@ -132,65 +135,114 @@ router.put('/:lineId', auth0Middleware(), screenContentMiddleware(), async (req,
}
const lineIndex = page.items.findIndex(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop())
page.items[lineIndex] = updatedLine
+
+ const pageId = req.params.pageId.split('/').pop()
+ const pageProject = project.data.layers.flatMap(layer => layer.pages).find(p => p.id.split('/').pop() === pageId)
+ const oldLineInColumn = pageProject?.columns?.find(col => col.lines.includes(oldLine.id))
+ const saveWholeColumns = pageProject?.columns
+ if (oldLineInColumn) {
+ const column = new Column(oldLineInColumn.id)
+ const columnData = await column.getColumnData()
+ const lineIndexInColumn = columnData.lines.indexOf(oldLine.id)
+ if (lineIndexInColumn !== -1) {
+ columnData.lines[lineIndexInColumn] = updatedLine.id
+ column.data = columnData
+ await column.update()
+ }
+ const columnInPageIndex = saveWholeColumns.findIndex(col => col.lines.includes(oldLine.id))
+ if (columnInPageIndex !== -1) {
+ saveWholeColumns[columnInPageIndex].lines = saveWholeColumns[columnInPageIndex].lines.map(lineId =>
+ lineId === oldLine.id ? updatedLine.id : lineId
+ )
+ }
+ }
await withOptimisticLocking(
- () => updatePageAndProject(page, project, user._id, true),
+ () => updatePageAndProject(page, project, user._id),
(currentVersion) => {
if(!currentVersion || currentVersion.type !== 'AnnotationPage') {
- respondWithError(res, 409, 'Version conflict while updating the page. Please try again.')
+ return respondWithError(res, 409, 'Version conflict while updating the page. Please try again.')
}
const newLineIndex = currentVersion.items.findIndex(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop())
if (newLineIndex === -1) {
- respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
- return
+ return respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
}
currentVersion.items[newLineIndex] = updatedLine
Object.assign(page, currentVersion)
return updatePageAndProject(page, project, user._id)
}
)
- res.json(line.asJSON(true))
+ if (res.headersSent) return
+ // Updating the project again to save updated columns as columns is not handled in updatePageAndProject
+ if(saveWholeColumns) {
+ project.data.layers.flatMap(layer => layer.pages).find(p => p.id.split('/').pop() === pageId).columns = saveWholeColumns
+ await project.update()
+ }
+ const lineJson = await line.asJSON(true)
+ res.status(200).json(lineJson)
} catch (error) {
+ if (res.headersSent) return
// Handle version conflicts with optimistic locking
if (error.status === 409) {
return handleVersionConflict(res, error)
}
- res.status(error.status ?? 500).json({ error: error.message })
+ return respondWithError(res, error.status ?? 500, error.message)
}
})
// Update the text of an existing line
router.patch('/:lineId/text', auth0Middleware(), screenContentMiddleware(), async (req, res) => {
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
try {
+ const project = new Project(req.params.projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.TEXT, ENTITIES.LINE))) {
+ return respondWithError(res, 403, 'You do not have permission to update line text in this project')
+ }
+ if (!project?.data) return respondWithError(res, 404, `Project ${req.params.projectId} was not found`)
if (typeof req.body !== 'string') {
- respondWithError(res, 400, 'Invalid request body. Expected a string.')
- return
+ return respondWithError(res, 400, 'Invalid request body. Expected a string.')
}
- const project = await getProjectById(req.params.projectId)
- 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) {
- respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
- return
+ return respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
}
const line = new Line(oldLine)
const updatedLine = await line.updateText(req.body, {"creator": user._id})
const lineIndex = page.items.findIndex(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop())
page.items[lineIndex] = updatedLine
+
+ const pageId = req.params.pageId.split('/').pop()
+ const pageProject = project.data.layers.flatMap(layer => layer.pages).find(p => p.id.split('/').pop() === pageId)
+ const oldLineInColumn = pageProject?.columns?.find(col => col.lines.includes(oldLine.id))
+ const saveWholeColumns = pageProject?.columns
+ if (oldLineInColumn) {
+ const column = new Column(oldLineInColumn.id)
+ const columnData = await column.getColumnData()
+ const lineIndexInColumn = columnData.lines.indexOf(oldLine.id)
+ if (lineIndexInColumn !== -1) {
+ columnData.lines[lineIndexInColumn] = updatedLine.id
+ column.data = columnData
+ await column.update()
+ }
+ const columnInPageIndex = saveWholeColumns.findIndex(col => col.lines.includes(oldLine.id))
+ if (columnInPageIndex !== -1) {
+ saveWholeColumns[columnInPageIndex].lines = saveWholeColumns[columnInPageIndex].lines.map(lineId =>
+ lineId === oldLine.id ? updatedLine.id : lineId
+ )
+ }
+ }
await withOptimisticLocking(
- () => updatePageAndProject(page, project, user._id, true),
+ () => updatePageAndProject(page, project, user._id),
(currentVersion) => {
if(!currentVersion || currentVersion.type !== 'AnnotationPage') {
if(res.headersSent) return
- respondWithError(res, 409, 'Version conflict while updating the page. Please try again.')
- return
+ return respondWithError(res, 409, 'Version conflict while updating the page. Please try again.')
}
const newLineIndex = currentVersion.items.findIndex(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop())
if (newLineIndex === -1) {
if(res.headersSent) return
- respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
- return
+ return respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
}
currentVersion.items[newLineIndex] = updatedLine
Object.assign(page, currentVersion)
@@ -198,60 +250,100 @@ router.patch('/:lineId/text', auth0Middleware(), screenContentMiddleware(), asyn
}
)
if(res.headersSent) return
- res.json(line.asJSON(true))
+ // Updating the project again to save updated columns as columns is not handled in updatePageAndProject
+ if(saveWholeColumns) {
+ project.data.layers.flatMap(layer => layer.pages).find(p => p.id.split('/').pop() === pageId).columns = saveWholeColumns
+ await project.update()
+ }
+ const lineJson = await line.asJSON(true)
+ res.status(200).json(lineJson)
} catch (error) {
+ if (res.headersSent) return
// Handle version conflicts with optimistic locking
if (error.status === 409) {
- handleVersionConflict(res, error)
- return
+ return handleVersionConflict(res, error)
}
- res.status(error.status ?? 500).json({ error: error.message })
+ return respondWithError(res, error.status ?? 500, error.message)
}
})
// Update the xywh (bounds) of an existing line
router.patch('/:lineId/bounds', auth0Middleware(), async (req, res) => {
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
try {
- if (typeof req.body !== 'object' || !req.body.x || !req.body.y || !req.body.w || !req.body.h) {
- respondWithError(res, 400, 'Invalid request body. Expected an object with x, y, w, and h properties.')
- return
+ const project = new Project(req.params.projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.SELECTOR, ENTITIES.LINE))) {
+ return respondWithError(res, 403, 'You do not have permission to update line bounds in this project')
}
- const project = await getProjectById(req.params.projectId)
- const page = await findPageById(req.params.pageId, req.params.projectId)
- const oldLine = page.items?.find(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop())
- if (!oldLine) {
- respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
- return
+ if (!project?.data) return respondWithError(res, 404, `Project ${req.params.projectId} was not found`)
+ const isValidBound = v => (Number.isInteger(v) && v >= 0) || (typeof v === 'string' && /^\d+$/.test(v))
+ if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body) || !isValidBound(req.body.x) || !isValidBound(req.body.y) || !isValidBound(req.body.w) || !isValidBound(req.body.h)) {
+ 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, 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}'`)
+ }
+ let oldLine = await fetch(findOldLine.id).then(resp => resp.json())
+ delete oldLine.label
const line = new Line(oldLine)
- const updatedLine = await line.updateBounds(req.body)
+ const updatedLine = await line.updateBounds(bounds, { creator: user._id })
const lineIndex = page.items.findIndex(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop())
page.items[lineIndex] = updatedLine
+
+ const pageId = req.params.pageId.split('/').pop()
+ const pageProject = project.data.layers.flatMap(layer => layer.pages).find(p => p.id.split('/').pop() === pageId)
+ const oldLineInColumn = pageProject?.columns?.find(col => col.lines.includes(oldLine.id))
+ const saveWholeColumns = pageProject?.columns
+ if (oldLineInColumn) {
+ const column = new Column(oldLineInColumn.id)
+ const columnData = await column.getColumnData()
+ const lineIndexInColumn = columnData.lines.indexOf(oldLine.id)
+ if (lineIndexInColumn !== -1) {
+ columnData.lines[lineIndexInColumn] = updatedLine.id
+ column.data = columnData
+ await column.update()
+ }
+ const columnInPageIndex = saveWholeColumns.findIndex(col => col.lines.includes(oldLine.id))
+ if (columnInPageIndex !== -1) {
+ saveWholeColumns[columnInPageIndex].lines = saveWholeColumns[columnInPageIndex].lines.map(lineId =>
+ lineId === oldLine.id ? updatedLine.id : lineId
+ )
+ }
+ }
await withOptimisticLocking(
- () => updatePageAndProject(page, project, user._id, true),
+ () => updatePageAndProject(page, project, user._id),
(currentVersion) => {
if(!currentVersion || currentVersion.type !== 'AnnotationPage') {
- respondWithError(res, 409, 'Version conflict while updating the page. Please try again.')
- return
+ return respondWithError(res, 409, 'Version conflict while updating the page. Please try again.')
}
const newLineIndex = currentVersion.items.findIndex(l => l.id.split('/').pop() === req.params.lineId?.split('/').pop())
if (newLineIndex === -1) {
- respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
- return
+ return respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
}
currentVersion.items[newLineIndex] = updatedLine
Object.assign(page, currentVersion)
return updatePageAndProject(page, project, user._id)
}
)
- res.json(line.asJSON(true))
- } catch (error) { // Handle version conflicts with optimistic locking
+ if (res.headersSent) return
+ // Updating the project again to save updated columns as columns is not handled in updatePageAndProject
+ if(saveWholeColumns) {
+ project.data.layers.flatMap(layer => layer.pages).find(p => p.id.split('/').pop() === pageId).columns = saveWholeColumns
+ await project.update()
+ }
+ const lineJson = await line.asJSON(true)
+ res.status(200).json(lineJson)
+ } catch (error) {
+ if (res.headersSent) return
+ // Handle version conflicts with optimistic locking
if (error.status === 409) {
return handleVersionConflict(res, error)
}
- res.status(error.status ?? 500).json({ error: error.message })
+ return respondWithError(res, error.status ?? 500, error.message)
}
})
diff --git a/package-lock.json b/package-lock.json
index a717668e..11c13d2e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,94 +9,42 @@
"version": "0.0.0",
"license": "CC-BY",
"dependencies": {
- "@iiif/helpers": "^1.5.3",
+ "@iiif/helpers": "^1.5.8",
"cookie-parser": "^1.4.7",
- "cors": "^2.8.5",
+ "cors": "^2.8.6",
"debug": "^4.4.3",
- "dompurify": "^3.2.7",
- "dotenv": "^17.2.3",
- "express": "^5.1.0",
+ "dotenv": "^17.3.1",
+ "express": "^5.2.1",
"express-list-endpoints": "^7.1.1",
- "express-oauth2-jwt-bearer": "~1.6.1",
+ "express-oauth2-jwt-bearer": "^1.7.4",
"image-size": "^2.0.2",
- "jsdom": "^27.0.0",
- "mariadb": "^3.4.5",
- "marked": "^16.3.0",
- "mime-types": "^3.0.1",
- "mongodb": "^6.20.0",
+ "mariadb": "^3.5.2",
+ "mime-types": "^3.0.2",
+ "mongodb": "^7.1.0",
"morgan": "^1.10.1",
- "nodemailer": "^7.0.6",
+ "nodemailer": "^8.0.2",
"tpen3-services": "file:"
},
"devDependencies": {
- "@jest/globals": "^30.2.0",
- "jest": "^30.2.0",
- "nodemon": "^3.1.10",
- "sinon": "^21.0.0",
- "supertest": "^7.1.4"
+ "@jest/globals": "^30.3.0",
+ "jest": "^30.3.0",
+ "nodemon": "^3.1.14",
+ "sinon": "^21.0.2",
+ "supertest": "^7.2.2"
},
"engines": {
- "node": ">=22.20.0"
+ "node": ">=24.14.0",
+ "npm": ">=11.0.0"
}
},
- "node_modules/@asamuzakjp/css-color": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz",
- "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==",
- "license": "MIT",
- "dependencies": {
- "@csstools/css-calc": "^2.1.4",
- "@csstools/css-color-parser": "^3.1.0",
- "@csstools/css-parser-algorithms": "^3.0.5",
- "@csstools/css-tokenizer": "^3.0.4",
- "lru-cache": "^11.2.1"
- }
- },
- "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
- "version": "11.2.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
- "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
- "license": "ISC",
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/@asamuzakjp/dom-selector": {
- "version": "6.5.7",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.5.7.tgz",
- "integrity": "sha512-cvdTPsi2qC1c22UppvuVmx/PDwuc6+QQkwt9OnwQD6Uotbh//tb2XDF0OoK2V0F4b8d02LIwNp3BieaDMAhIhA==",
- "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.2"
- }
- },
- "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
- "version": "11.2.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
- "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
- "license": "ISC",
- "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.27.1",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
- "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -105,9 +53,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
- "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -115,22 +63,21 @@
}
},
"node_modules/@babel/core": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
- "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.28.3",
- "@babel/helper-compilation-targets": "^7.27.2",
- "@babel/helper-module-transforms": "^7.28.3",
- "@babel/helpers": "^7.28.4",
- "@babel/parser": "^7.28.4",
- "@babel/template": "^7.27.2",
- "@babel/traverse": "^7.28.4",
- "@babel/types": "^7.28.4",
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -147,14 +94,14 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.28.3",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
- "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.3",
- "@babel/types": "^7.28.2",
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -164,13 +111,13 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.27.2",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
- "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.27.2",
+ "@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
@@ -191,29 +138,29 @@
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
- "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.27.1",
- "@babel/types": "^7.27.1"
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.28.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
- "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.27.1",
- "@babel/traverse": "^7.28.3"
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -223,9 +170,9 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
- "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
"dev": true,
"license": "MIT",
"engines": {
@@ -243,9 +190,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
- "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -263,27 +210,27 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
- "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.27.2",
- "@babel/types": "^7.28.4"
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
- "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.4"
+ "@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -348,13 +295,13 @@
}
},
"node_modules/@babel/plugin-syntax-import-attributes": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
- "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
+ "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1"
+ "@babel/helper-plugin-utils": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -390,13 +337,13 @@
}
},
"node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
- "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
+ "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1"
+ "@babel/helper-plugin-utils": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -516,13 +463,13 @@
}
},
"node_modules/@babel/plugin-syntax-typescript": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
- "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
+ "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1"
+ "@babel/helper-plugin-utils": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -532,33 +479,33 @@
}
},
"node_modules/@babel/template": {
- "version": "7.27.2",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
- "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.27.1",
- "@babel/parser": "^7.27.2",
- "@babel/types": "^7.27.1"
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
- "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.28.3",
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.28.4",
- "@babel/template": "^7.27.2",
- "@babel/types": "^7.28.4",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
"debug": "^4.3.1"
},
"engines": {
@@ -566,14 +513,14 @@
}
},
"node_modules/@babel/types": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
- "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.27.1"
+ "@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -586,144 +533,10 @@
"dev": true,
"license": "MIT"
},
- "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/@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/@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/@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",
- "peer": true,
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@csstools/css-tokenizer": "^3.0.4"
- }
- },
- "node_modules/@csstools/css-syntax-patches-for-csstree": {
- "version": "1.0.14",
- "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz",
- "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT-0",
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "postcss": "^8.4"
- }
- },
- "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",
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@emnapi/core": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
- "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
+ "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -733,9 +546,9 @@
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
- "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
+ "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -755,9 +568,9 @@
}
},
"node_modules/@iiif/helpers": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/@iiif/helpers/-/helpers-1.5.3.tgz",
- "integrity": "sha512-Ofqr6KA5bu4htrXukMrw6Fn10dpVvCD8ksahpf/6EUfa1hjyaIBLB/8JL+5jK5ipO8TPqwMREEFbo/h4r5vY5w==",
+ "version": "1.5.8",
+ "resolved": "https://registry.npmjs.org/@iiif/helpers/-/helpers-1.5.8.tgz",
+ "integrity": "sha512-jNLhsmz5LsK3FhBtC6cIEoUM057zVL8zF0a8aQPFM5i6U/tf3QPFiq2mmm3Lop1ORFIZlEJXvsCwk7d3GucB2w==",
"license": "MIT",
"dependencies": {
"@iiif/presentation-2": "1.0.4",
@@ -771,13 +584,13 @@
"svg-arc-to-cubic-bezier": "^3.2.0"
},
"peerDependencies": {
- "@iiif/parser": "^2.2.3"
+ "@iiif/parser": "^2.2.8"
}
},
"node_modules/@iiif/parser": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/@iiif/parser/-/parser-2.2.4.tgz",
- "integrity": "sha512-42izQ8jhS6vhjhWGxmK6b3kyQOlfjb/fh6mBLloDcvqgk+JS0BbsYsNtftQDvnmWqvliTQYnQ2XLxItvx6gGHQ==",
+ "version": "2.2.9",
+ "resolved": "https://registry.npmjs.org/@iiif/parser/-/parser-2.2.9.tgz",
+ "integrity": "sha512-HJVlnsz6mj56Qda9U7vhXEMxHFuFLeIDTcXqbPEDcuX1iefrWVsuDIY+DxyQO64vBnwme5NWOZbtbO73XNYB6A==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -801,7 +614,6 @@
"resolved": "https://registry.npmjs.org/@iiif/presentation-3/-/presentation-3-2.2.3.tgz",
"integrity": "sha512-xCLbUr9euqegsrxGe65M2fWbv6gKpiUhHXCpOn+V+qtawkMbOSNWbYOISo2aLQdYVg4DGYD0g2bMzSCF33uNOQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/geojson": "^7946.0.10"
}
@@ -861,17 +673,17 @@
}
},
"node_modules/@jest/console": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz",
- "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz",
+ "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/types": "30.2.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
"chalk": "^4.1.2",
- "jest-message-util": "30.2.0",
- "jest-util": "30.2.0",
+ "jest-message-util": "30.3.0",
+ "jest-util": "30.3.0",
"slash": "^3.0.0"
},
"engines": {
@@ -879,39 +691,38 @@
}
},
"node_modules/@jest/core": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz",
- "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz",
+ "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/console": "30.2.0",
+ "@jest/console": "30.3.0",
"@jest/pattern": "30.0.1",
- "@jest/reporters": "30.2.0",
- "@jest/test-result": "30.2.0",
- "@jest/transform": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/reporters": "30.3.0",
+ "@jest/test-result": "30.3.0",
+ "@jest/transform": "30.3.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
"ansi-escapes": "^4.3.2",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"exit-x": "^0.2.2",
"graceful-fs": "^4.2.11",
- "jest-changed-files": "30.2.0",
- "jest-config": "30.2.0",
- "jest-haste-map": "30.2.0",
- "jest-message-util": "30.2.0",
+ "jest-changed-files": "30.3.0",
+ "jest-config": "30.3.0",
+ "jest-haste-map": "30.3.0",
+ "jest-message-util": "30.3.0",
"jest-regex-util": "30.0.1",
- "jest-resolve": "30.2.0",
- "jest-resolve-dependencies": "30.2.0",
- "jest-runner": "30.2.0",
- "jest-runtime": "30.2.0",
- "jest-snapshot": "30.2.0",
- "jest-util": "30.2.0",
- "jest-validate": "30.2.0",
- "jest-watcher": "30.2.0",
- "micromatch": "^4.0.8",
- "pretty-format": "30.2.0",
+ "jest-resolve": "30.3.0",
+ "jest-resolve-dependencies": "30.3.0",
+ "jest-runner": "30.3.0",
+ "jest-runtime": "30.3.0",
+ "jest-snapshot": "30.3.0",
+ "jest-util": "30.3.0",
+ "jest-validate": "30.3.0",
+ "jest-watcher": "30.3.0",
+ "pretty-format": "30.3.0",
"slash": "^3.0.0"
},
"engines": {
@@ -927,9 +738,9 @@
}
},
"node_modules/@jest/diff-sequences": {
- "version": "30.0.1",
- "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
- "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz",
+ "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -937,39 +748,39 @@
}
},
"node_modules/@jest/environment": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz",
- "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz",
+ "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/fake-timers": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/fake-timers": "30.3.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
- "jest-mock": "30.2.0"
+ "jest-mock": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/expect": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz",
- "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz",
+ "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "expect": "30.2.0",
- "jest-snapshot": "30.2.0"
+ "expect": "30.3.0",
+ "jest-snapshot": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/expect-utils": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz",
- "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz",
+ "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -980,18 +791,18 @@
}
},
"node_modules/@jest/fake-timers": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz",
- "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz",
+ "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/types": "30.2.0",
- "@sinonjs/fake-timers": "^13.0.0",
+ "@jest/types": "30.3.0",
+ "@sinonjs/fake-timers": "^15.0.0",
"@types/node": "*",
- "jest-message-util": "30.2.0",
- "jest-mock": "30.2.0",
- "jest-util": "30.2.0"
+ "jest-message-util": "30.3.0",
+ "jest-mock": "30.3.0",
+ "jest-util": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -1008,16 +819,16 @@
}
},
"node_modules/@jest/globals": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz",
- "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz",
+ "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/environment": "30.2.0",
- "@jest/expect": "30.2.0",
- "@jest/types": "30.2.0",
- "jest-mock": "30.2.0"
+ "@jest/environment": "30.3.0",
+ "@jest/expect": "30.3.0",
+ "@jest/types": "30.3.0",
+ "jest-mock": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -1038,32 +849,32 @@
}
},
"node_modules/@jest/reporters": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz",
- "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz",
+ "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^0.2.3",
- "@jest/console": "30.2.0",
- "@jest/test-result": "30.2.0",
- "@jest/transform": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/console": "30.3.0",
+ "@jest/test-result": "30.3.0",
+ "@jest/transform": "30.3.0",
+ "@jest/types": "30.3.0",
"@jridgewell/trace-mapping": "^0.3.25",
"@types/node": "*",
"chalk": "^4.1.2",
"collect-v8-coverage": "^1.0.2",
"exit-x": "^0.2.2",
- "glob": "^10.3.10",
+ "glob": "^10.5.0",
"graceful-fs": "^4.2.11",
"istanbul-lib-coverage": "^3.0.0",
"istanbul-lib-instrument": "^6.0.0",
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^5.0.0",
"istanbul-reports": "^3.1.3",
- "jest-message-util": "30.2.0",
- "jest-util": "30.2.0",
- "jest-worker": "30.2.0",
+ "jest-message-util": "30.3.0",
+ "jest-util": "30.3.0",
+ "jest-worker": "30.3.0",
"slash": "^3.0.0",
"string-length": "^4.0.2",
"v8-to-istanbul": "^9.0.1"
@@ -1094,13 +905,13 @@
}
},
"node_modules/@jest/snapshot-utils": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz",
- "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz",
+ "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/types": "30.2.0",
+ "@jest/types": "30.3.0",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"natural-compare": "^1.4.0"
@@ -1125,14 +936,14 @@
}
},
"node_modules/@jest/test-result": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz",
- "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz",
+ "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/console": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/console": "30.3.0",
+ "@jest/types": "30.3.0",
"@types/istanbul-lib-coverage": "^2.0.6",
"collect-v8-coverage": "^1.0.2"
},
@@ -1141,15 +952,15 @@
}
},
"node_modules/@jest/test-sequencer": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz",
- "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz",
+ "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/test-result": "30.2.0",
+ "@jest/test-result": "30.3.0",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.2.0",
+ "jest-haste-map": "30.3.0",
"slash": "^3.0.0"
},
"engines": {
@@ -1157,24 +968,23 @@
}
},
"node_modules/@jest/transform": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz",
- "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz",
+ "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
- "@jest/types": "30.2.0",
+ "@jest/types": "30.3.0",
"@jridgewell/trace-mapping": "^0.3.25",
"babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.2.0",
+ "jest-haste-map": "30.3.0",
"jest-regex-util": "30.0.1",
- "jest-util": "30.2.0",
- "micromatch": "^4.0.8",
+ "jest-util": "30.3.0",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
@@ -1184,9 +994,9 @@
}
},
"node_modules/@jest/types": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
- "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
+ "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1253,9 +1063,9 @@
}
},
"node_modules/@mongodb-js/saslprep": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz",
- "integrity": "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==",
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
+ "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
"license": "MIT",
"dependencies": {
"sparse-bitfield": "^3.0.3"
@@ -1288,9 +1098,9 @@
}
},
"node_modules/@paralleldrive/cuid2": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
- "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
+ "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1322,9 +1132,9 @@
}
},
"node_modules/@sinclair/typebox": {
- "version": "0.34.41",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
- "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
+ "version": "0.34.48",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
+ "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"dev": true,
"license": "MIT"
},
@@ -1339,9 +1149,9 @@
}
},
"node_modules/@sinonjs/fake-timers": {
- "version": "13.0.5",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
- "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz",
+ "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -1349,9 +1159,9 @@
}
},
"node_modules/@sinonjs/samsam": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz",
- "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==",
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-9.0.2.tgz",
+ "integrity": "sha512-H/JSxa4GNKZuuU41E3b8Y3tbSEx8y4uq4UH1C56ONQac16HblReJomIvv3Ud7ANQHQmkeSowY49Ij972e/pGxQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -1459,12 +1269,12 @@
}
},
"node_modules/@types/node": {
- "version": "24.6.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz",
- "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==",
+ "version": "25.4.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz",
+ "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==",
"license": "MIT",
"dependencies": {
- "undici-types": "~7.13.0"
+ "undici-types": "~7.18.0"
}
},
"node_modules/@types/stack-utils": {
@@ -1474,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",
@@ -1488,18 +1291,18 @@
"license": "MIT"
},
"node_modules/@types/whatwg-url": {
- "version": "11.0.5",
- "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
- "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
"license": "MIT",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/@types/yargs": {
- "version": "17.0.33",
- "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
- "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
+ "version": "17.0.35",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
+ "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1809,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",
@@ -1877,6 +1671,19 @@
"node": ">= 8"
}
},
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -1902,16 +1709,16 @@
"license": "MIT"
},
"node_modules/babel-jest": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
- "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
+ "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/transform": "30.2.0",
+ "@jest/transform": "30.3.0",
"@types/babel__core": "^7.20.5",
"babel-plugin-istanbul": "^7.0.1",
- "babel-preset-jest": "30.2.0",
+ "babel-preset-jest": "30.3.0",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"slash": "^3.0.0"
@@ -1944,9 +1751,9 @@
}
},
"node_modules/babel-plugin-jest-hoist": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz",
- "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz",
+ "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1984,13 +1791,13 @@
}
},
"node_modules/babel-preset-jest": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz",
- "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz",
+ "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "babel-plugin-jest-hoist": "30.2.0",
+ "babel-plugin-jest-hoist": "30.3.0",
"babel-preset-current-node-syntax": "^1.2.0"
},
"engines": {
@@ -2008,13 +1815,16 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.11",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.11.tgz",
- "integrity": "sha512-i+sRXGhz4+QW8aACZ3+r1GAKMt0wlFpeA8M5rOQd0HEYw9zhDrlx9Wc8uQ0IdXakjJRthzglEwfB/yqIjO6iDg==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
+ "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
- "baseline-browser-mapping": "dist/cli.js"
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
"node_modules/basic-auth": {
@@ -2029,21 +1839,6 @@
"node": ">= 0.8"
}
},
- "node_modules/basic-auth/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "license": "MIT"
- },
- "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",
@@ -2058,23 +1853,27 @@
}
},
"node_modules/body-parser": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
- "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
- "debug": "^4.4.0",
+ "debug": "^4.4.3",
"http-errors": "^2.0.0",
- "iconv-lite": "^0.6.3",
+ "iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
- "qs": "^6.14.0",
- "raw-body": "^3.0.0",
- "type-is": "^2.0.0"
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/brace-expansion": {
@@ -2101,9 +1900,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.26.3",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
- "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
@@ -2120,13 +1919,12 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
- "baseline-browser-mapping": "^2.8.9",
- "caniuse-lite": "^1.0.30001746",
- "electron-to-chromium": "^1.5.227",
- "node-releases": "^2.0.21",
- "update-browserslist-db": "^1.1.3"
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@@ -2146,12 +1944,12 @@
}
},
"node_modules/bson": {
- "version": "6.10.4",
- "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
- "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
+ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
"license": "Apache-2.0",
"engines": {
- "node": ">=16.20.1"
+ "node": ">=20.19.0"
}
},
"node_modules/buffer-from": {
@@ -2220,9 +2018,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001747",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz",
- "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==",
+ "version": "1.0.30001777",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
+ "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
"dev": true,
"funding": [
{
@@ -2293,9 +2091,9 @@
}
},
"node_modules/ci-info": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
- "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
+ "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==",
"dev": true,
"funding": [
{
@@ -2309,9 +2107,9 @@
}
},
"node_modules/cjs-module-lexer": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz",
- "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
+ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==",
"dev": true,
"license": "MIT"
},
@@ -2405,9 +2203,9 @@
}
},
"node_modules/collect-v8-coverage": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
- "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
+ "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==",
"dev": true,
"license": "MIT"
},
@@ -2462,15 +2260,16 @@
"license": "MIT"
},
"node_modules/content-disposition": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
- "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
"license": "MIT",
- "dependencies": {
- "safe-buffer": "5.2.1"
- },
"engines": {
- "node": ">= 0.6"
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/content-type": {
@@ -2525,9 +2324,9 @@
"license": "MIT"
},
"node_modules/cors": {
- "version": "2.8.5",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
- "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
@@ -2535,6 +2334,10 @@
},
"engines": {
"node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": {
@@ -2552,46 +2355,6 @@
"node": ">= 8"
}
},
- "node_modules/css-tree": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
- "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
- "license": "MIT",
- "dependencies": {
- "mdn-data": "2.12.2",
- "source-map-js": "^1.0.1"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
- }
- },
- "node_modules/cssstyle": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz",
- "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==",
- "license": "MIT",
- "dependencies": {
- "@asamuzakjp/css-color": "^4.0.3",
- "@csstools/css-syntax-patches-for-csstree": "^1.0.14",
- "css-tree": "^3.1.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/data-urls": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
- "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
- "license": "MIT",
- "dependencies": {
- "whatwg-mimetype": "^4.0.0",
- "whatwg-url": "^15.0.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2609,16 +2372,10 @@
}
}
},
- "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.0",
- "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
- "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
+ "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -2690,28 +2447,19 @@
}
},
"node_modules/diff": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
- "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
+ "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
- "node_modules/dompurify": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
- "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
- "license": "(MPL-2.0 OR Apache-2.0)",
- "optionalDependencies": {
- "@types/trusted-types": "^2.0.7"
- }
- },
"node_modules/dotenv": {
- "version": "17.2.3",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
- "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "version": "17.3.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
+ "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -2748,9 +2496,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.230",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.230.tgz",
- "integrity": "sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==",
+ "version": "1.5.307",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
+ "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==",
"dev": true,
"license": "ISC"
},
@@ -2783,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",
@@ -2942,36 +2678,37 @@
}
},
"node_modules/expect": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz",
- "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz",
+ "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/expect-utils": "30.2.0",
+ "@jest/expect-utils": "30.3.0",
"@jest/get-type": "30.1.0",
- "jest-matcher-utils": "30.2.0",
- "jest-message-util": "30.2.0",
- "jest-mock": "30.2.0",
- "jest-util": "30.2.0"
+ "jest-matcher-utils": "30.3.0",
+ "jest-message-util": "30.3.0",
+ "jest-mock": "30.3.0",
+ "jest-util": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/express": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
- "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
- "body-parser": "^2.2.0",
+ "body-parser": "^2.2.1",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
+ "depd": "^2.0.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
@@ -3011,15 +2748,15 @@
}
},
"node_modules/express-oauth2-jwt-bearer": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.6.1.tgz",
- "integrity": "sha512-fhgIvVZ6iSR/jqyVHBcN9Df7VeBdVhg5d2yN6+HNrSEegmhbh9hFY+TvtvBmsv130fI06EW3Dgp9ApmYwArN6Q==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.7.4.tgz",
+ "integrity": "sha512-teO/eyvU8OkJXiP4cRuoJMrp31nNvjnL47MIkso0D/21AqUGv1O+VEiLisrDA8xjkaCBTufYnV1zepCOCLK4vg==",
"license": "MIT",
"dependencies": {
"jose": "^4.15.5"
},
"engines": {
- "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0"
+ "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0 || ^24.0.0"
}
},
"node_modules/express/node_modules/cookie-signature": {
@@ -3069,9 +2806,9 @@
}
},
"node_modules/finalhandler": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
- "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
@@ -3082,10 +2819,14 @@
"statuses": "^2.0.1"
},
"engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/find-up": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
@@ -3117,9 +2858,9 @@
}
},
"node_modules/form-data": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
- "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3304,9 +3045,10 @@
}
},
"node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3406,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",
@@ -3426,54 +3156,23 @@
"license": "MIT"
},
"node_modules/http-errors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
- "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/http-errors/node_modules/statuses": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
- "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "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==",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
- "agent-base": "^7.1.0",
- "debug": "^4.3.4"
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
},
"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"
+ "node": ">= 0.8"
},
- "engines": {
- "node": ">= 14"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/human-signals": {
@@ -3487,15 +3186,19 @@
}
},
"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==",
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/ignore-by-default": {
@@ -3647,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",
@@ -3707,9 +3404,9 @@
}
},
"node_modules/istanbul-lib-instrument/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3780,16 +3477,16 @@
}
},
"node_modules/jest": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz",
- "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz",
+ "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/core": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/core": "30.3.0",
+ "@jest/types": "30.3.0",
"import-local": "^3.2.0",
- "jest-cli": "30.2.0"
+ "jest-cli": "30.3.0"
},
"bin": {
"jest": "bin/jest.js"
@@ -3807,14 +3504,14 @@
}
},
"node_modules/jest-changed-files": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz",
- "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz",
+ "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==",
"dev": true,
"license": "MIT",
"dependencies": {
"execa": "^5.1.1",
- "jest-util": "30.2.0",
+ "jest-util": "30.3.0",
"p-limit": "^3.1.0"
},
"engines": {
@@ -3822,29 +3519,29 @@
}
},
"node_modules/jest-circus": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz",
- "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz",
+ "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/environment": "30.2.0",
- "@jest/expect": "30.2.0",
- "@jest/test-result": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/environment": "30.3.0",
+ "@jest/expect": "30.3.0",
+ "@jest/test-result": "30.3.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
"chalk": "^4.1.2",
"co": "^4.6.0",
"dedent": "^1.6.0",
"is-generator-fn": "^2.1.0",
- "jest-each": "30.2.0",
- "jest-matcher-utils": "30.2.0",
- "jest-message-util": "30.2.0",
- "jest-runtime": "30.2.0",
- "jest-snapshot": "30.2.0",
- "jest-util": "30.2.0",
+ "jest-each": "30.3.0",
+ "jest-matcher-utils": "30.3.0",
+ "jest-message-util": "30.3.0",
+ "jest-runtime": "30.3.0",
+ "jest-snapshot": "30.3.0",
+ "jest-util": "30.3.0",
"p-limit": "^3.1.0",
- "pretty-format": "30.2.0",
+ "pretty-format": "30.3.0",
"pure-rand": "^7.0.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
@@ -3854,21 +3551,21 @@
}
},
"node_modules/jest-cli": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz",
- "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz",
+ "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/core": "30.2.0",
- "@jest/test-result": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/core": "30.3.0",
+ "@jest/test-result": "30.3.0",
+ "@jest/types": "30.3.0",
"chalk": "^4.1.2",
"exit-x": "^0.2.2",
"import-local": "^3.2.0",
- "jest-config": "30.2.0",
- "jest-util": "30.2.0",
- "jest-validate": "30.2.0",
+ "jest-config": "30.3.0",
+ "jest-util": "30.3.0",
+ "jest-validate": "30.3.0",
"yargs": "^17.7.2"
},
"bin": {
@@ -3887,34 +3584,33 @@
}
},
"node_modules/jest-config": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz",
- "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz",
+ "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/get-type": "30.1.0",
"@jest/pattern": "30.0.1",
- "@jest/test-sequencer": "30.2.0",
- "@jest/types": "30.2.0",
- "babel-jest": "30.2.0",
+ "@jest/test-sequencer": "30.3.0",
+ "@jest/types": "30.3.0",
+ "babel-jest": "30.3.0",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"deepmerge": "^4.3.1",
- "glob": "^10.3.10",
+ "glob": "^10.5.0",
"graceful-fs": "^4.2.11",
- "jest-circus": "30.2.0",
+ "jest-circus": "30.3.0",
"jest-docblock": "30.2.0",
- "jest-environment-node": "30.2.0",
+ "jest-environment-node": "30.3.0",
"jest-regex-util": "30.0.1",
- "jest-resolve": "30.2.0",
- "jest-runner": "30.2.0",
- "jest-util": "30.2.0",
- "jest-validate": "30.2.0",
- "micromatch": "^4.0.8",
+ "jest-resolve": "30.3.0",
+ "jest-runner": "30.3.0",
+ "jest-util": "30.3.0",
+ "jest-validate": "30.3.0",
"parse-json": "^5.2.0",
- "pretty-format": "30.2.0",
+ "pretty-format": "30.3.0",
"slash": "^3.0.0",
"strip-json-comments": "^3.1.1"
},
@@ -3939,16 +3635,16 @@
}
},
"node_modules/jest-diff": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz",
- "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz",
+ "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/diff-sequences": "30.0.1",
+ "@jest/diff-sequences": "30.3.0",
"@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
- "pretty-format": "30.2.0"
+ "pretty-format": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -3968,57 +3664,57 @@
}
},
"node_modules/jest-each": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz",
- "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz",
+ "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
- "@jest/types": "30.2.0",
+ "@jest/types": "30.3.0",
"chalk": "^4.1.2",
- "jest-util": "30.2.0",
- "pretty-format": "30.2.0"
+ "jest-util": "30.3.0",
+ "pretty-format": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-environment-node": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz",
- "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz",
+ "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/environment": "30.2.0",
- "@jest/fake-timers": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/environment": "30.3.0",
+ "@jest/fake-timers": "30.3.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
- "jest-mock": "30.2.0",
- "jest-util": "30.2.0",
- "jest-validate": "30.2.0"
+ "jest-mock": "30.3.0",
+ "jest-util": "30.3.0",
+ "jest-validate": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-haste-map": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
- "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz",
+ "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/types": "30.2.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
- "jest-util": "30.2.0",
- "jest-worker": "30.2.0",
- "micromatch": "^4.0.8",
+ "jest-util": "30.3.0",
+ "jest-worker": "30.3.0",
+ "picomatch": "^4.0.3",
"walker": "^1.0.8"
},
"engines": {
@@ -4029,49 +3725,49 @@
}
},
"node_modules/jest-leak-detector": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz",
- "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz",
+ "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
- "pretty-format": "30.2.0"
+ "pretty-format": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-matcher-utils": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz",
- "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz",
+ "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
- "jest-diff": "30.2.0",
- "pretty-format": "30.2.0"
+ "jest-diff": "30.3.0",
+ "pretty-format": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-message-util": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
- "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
+ "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
- "@jest/types": "30.2.0",
+ "@jest/types": "30.3.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
- "micromatch": "^4.0.8",
- "pretty-format": "30.2.0",
+ "picomatch": "^4.0.3",
+ "pretty-format": "30.3.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -4080,15 +3776,15 @@
}
},
"node_modules/jest-mock": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz",
- "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz",
+ "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/types": "30.2.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
- "jest-util": "30.2.0"
+ "jest-util": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4123,18 +3819,18 @@
}
},
"node_modules/jest-resolve": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz",
- "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz",
+ "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.2.0",
+ "jest-haste-map": "30.3.0",
"jest-pnp-resolver": "^1.2.3",
- "jest-util": "30.2.0",
- "jest-validate": "30.2.0",
+ "jest-util": "30.3.0",
+ "jest-validate": "30.3.0",
"slash": "^3.0.0",
"unrs-resolver": "^1.7.11"
},
@@ -4143,46 +3839,46 @@
}
},
"node_modules/jest-resolve-dependencies": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz",
- "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz",
+ "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==",
"dev": true,
"license": "MIT",
"dependencies": {
"jest-regex-util": "30.0.1",
- "jest-snapshot": "30.2.0"
+ "jest-snapshot": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-runner": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz",
- "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz",
+ "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/console": "30.2.0",
- "@jest/environment": "30.2.0",
- "@jest/test-result": "30.2.0",
- "@jest/transform": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/console": "30.3.0",
+ "@jest/environment": "30.3.0",
+ "@jest/test-result": "30.3.0",
+ "@jest/transform": "30.3.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
"chalk": "^4.1.2",
"emittery": "^0.13.1",
"exit-x": "^0.2.2",
"graceful-fs": "^4.2.11",
"jest-docblock": "30.2.0",
- "jest-environment-node": "30.2.0",
- "jest-haste-map": "30.2.0",
- "jest-leak-detector": "30.2.0",
- "jest-message-util": "30.2.0",
- "jest-resolve": "30.2.0",
- "jest-runtime": "30.2.0",
- "jest-util": "30.2.0",
- "jest-watcher": "30.2.0",
- "jest-worker": "30.2.0",
+ "jest-environment-node": "30.3.0",
+ "jest-haste-map": "30.3.0",
+ "jest-leak-detector": "30.3.0",
+ "jest-message-util": "30.3.0",
+ "jest-resolve": "30.3.0",
+ "jest-runtime": "30.3.0",
+ "jest-util": "30.3.0",
+ "jest-watcher": "30.3.0",
+ "jest-worker": "30.3.0",
"p-limit": "^3.1.0",
"source-map-support": "0.5.13"
},
@@ -4191,32 +3887,32 @@
}
},
"node_modules/jest-runtime": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz",
- "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz",
+ "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/environment": "30.2.0",
- "@jest/fake-timers": "30.2.0",
- "@jest/globals": "30.2.0",
+ "@jest/environment": "30.3.0",
+ "@jest/fake-timers": "30.3.0",
+ "@jest/globals": "30.3.0",
"@jest/source-map": "30.0.1",
- "@jest/test-result": "30.2.0",
- "@jest/transform": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/test-result": "30.3.0",
+ "@jest/transform": "30.3.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
"chalk": "^4.1.2",
"cjs-module-lexer": "^2.1.0",
"collect-v8-coverage": "^1.0.2",
- "glob": "^10.3.10",
+ "glob": "^10.5.0",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.2.0",
- "jest-message-util": "30.2.0",
- "jest-mock": "30.2.0",
+ "jest-haste-map": "30.3.0",
+ "jest-message-util": "30.3.0",
+ "jest-mock": "30.3.0",
"jest-regex-util": "30.0.1",
- "jest-resolve": "30.2.0",
- "jest-snapshot": "30.2.0",
- "jest-util": "30.2.0",
+ "jest-resolve": "30.3.0",
+ "jest-snapshot": "30.3.0",
+ "jest-util": "30.3.0",
"slash": "^3.0.0",
"strip-bom": "^4.0.0"
},
@@ -4225,9 +3921,9 @@
}
},
"node_modules/jest-snapshot": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz",
- "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz",
+ "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4236,20 +3932,20 @@
"@babel/plugin-syntax-jsx": "^7.27.1",
"@babel/plugin-syntax-typescript": "^7.27.1",
"@babel/types": "^7.27.3",
- "@jest/expect-utils": "30.2.0",
+ "@jest/expect-utils": "30.3.0",
"@jest/get-type": "30.1.0",
- "@jest/snapshot-utils": "30.2.0",
- "@jest/transform": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/snapshot-utils": "30.3.0",
+ "@jest/transform": "30.3.0",
+ "@jest/types": "30.3.0",
"babel-preset-current-node-syntax": "^1.2.0",
"chalk": "^4.1.2",
- "expect": "30.2.0",
+ "expect": "30.3.0",
"graceful-fs": "^4.2.11",
- "jest-diff": "30.2.0",
- "jest-matcher-utils": "30.2.0",
- "jest-message-util": "30.2.0",
- "jest-util": "30.2.0",
- "pretty-format": "30.2.0",
+ "jest-diff": "30.3.0",
+ "jest-matcher-utils": "30.3.0",
+ "jest-message-util": "30.3.0",
+ "jest-util": "30.3.0",
+ "pretty-format": "30.3.0",
"semver": "^7.7.2",
"synckit": "^0.11.8"
},
@@ -4258,9 +3954,9 @@
}
},
"node_modules/jest-snapshot/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -4271,49 +3967,36 @@
}
},
"node_modules/jest-util": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
- "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
+ "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/types": "30.2.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
- "picomatch": "^4.0.2"
+ "picomatch": "^4.0.3"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/jest-util/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/jest-validate": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz",
- "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz",
+ "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
- "@jest/types": "30.2.0",
+ "@jest/types": "30.3.0",
"camelcase": "^6.3.0",
"chalk": "^4.1.2",
"leven": "^3.1.0",
- "pretty-format": "30.2.0"
+ "pretty-format": "30.3.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4333,19 +4016,19 @@
}
},
"node_modules/jest-watcher": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz",
- "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz",
+ "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/test-result": "30.2.0",
- "@jest/types": "30.2.0",
+ "@jest/test-result": "30.3.0",
+ "@jest/types": "30.3.0",
"@types/node": "*",
"ansi-escapes": "^4.3.2",
"chalk": "^4.1.2",
"emittery": "^0.13.1",
- "jest-util": "30.2.0",
+ "jest-util": "30.3.0",
"string-length": "^4.0.2"
},
"engines": {
@@ -4353,15 +4036,15 @@
}
},
"node_modules/jest-worker": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz",
- "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz",
+ "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@ungap/structured-clone": "^1.3.0",
- "jest-util": "30.2.0",
+ "jest-util": "30.3.0",
"merge-stream": "^2.0.0",
"supports-color": "^8.1.1"
},
@@ -4402,9 +4085,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4415,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",
@@ -4544,9 +4188,9 @@
}
},
"node_modules/make-dir/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -4567,19 +4211,19 @@
}
},
"node_modules/mariadb": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.4.5.tgz",
- "integrity": "sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.5.2.tgz",
+ "integrity": "sha512-9rztrI4nouxAY/82a+RlzzZ5ie2vxu2eYclkBvTy1ATXH1B9cnvZ0O71Pzsy/mlfDb5P3HhOg0JzQKkDRhctyA==",
"license": "LGPL-2.1-or-later",
"dependencies": {
"@types/geojson": "^7946.0.16",
- "@types/node": "^24.0.13",
+ "@types/node": ">=18",
"denque": "^2.1.0",
- "iconv-lite": "^0.6.3",
+ "iconv-lite": "^0.7.2",
"lru-cache": "^10.4.3"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 18"
}
},
"node_modules/mariadb/node_modules/@types/geojson": {
@@ -4594,18 +4238,6 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
- "node_modules/marked": {
- "version": "16.3.0",
- "resolved": "https://registry.npmjs.org/marked/-/marked-16.3.0.tgz",
- "integrity": "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==",
- "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",
@@ -4615,12 +4247,6 @@
"node": ">= 0.4"
}
},
- "node_modules/mdn-data": {
- "version": "2.12.2",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
- "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
- "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",
@@ -4665,20 +4291,6 @@
"node": ">= 0.6"
}
},
- "node_modules/micromatch": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
"node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
@@ -4702,15 +4314,19 @@
}
},
"node_modules/mime-types": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
- "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
- "node": ">= 0.6"
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/mimic-fn": {
@@ -4724,13 +4340,13 @@
}
},
"node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -4740,36 +4356,36 @@
}
},
"node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mongodb": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
- "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.0.tgz",
+ "integrity": "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.3.0",
- "bson": "^6.10.4",
- "mongodb-connection-string-url": "^3.0.2"
+ "bson": "^7.1.1",
+ "mongodb-connection-string-url": "^7.0.0"
},
"engines": {
- "node": ">=16.20.1"
+ "node": ">=20.19.0"
},
"peerDependencies": {
- "@aws-sdk/credential-providers": "^3.188.0",
- "@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
- "gcp-metadata": "^5.2.0",
- "kerberos": "^2.0.1",
- "mongodb-client-encryption": ">=6.0.0 <7",
+ "@aws-sdk/credential-providers": "^3.806.0",
+ "@mongodb-js/zstd": "^7.0.0",
+ "gcp-metadata": "^7.0.1",
+ "kerberos": "^7.0.0",
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
"snappy": "^7.3.2",
- "socks": "^2.7.1"
+ "socks": "^2.8.6"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
@@ -4796,13 +4412,16 @@
}
},
"node_modules/mongodb-connection-string-url": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
- "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+ "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
"license": "Apache-2.0",
"dependencies": {
- "@types/whatwg-url": "^11.0.2",
- "whatwg-url": "^14.1.0 || ^13.0.0"
+ "@types/whatwg-url": "^13.0.0",
+ "whatwg-url": "^14.1.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
}
},
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
@@ -4888,28 +4507,10 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
- "node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
"node_modules/napi-postinstall": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz",
- "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==",
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
+ "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -4946,31 +4547,32 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.21",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
- "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
+ "version": "2.0.36",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
"dev": true,
"license": "MIT"
},
"node_modules/nodemailer": {
- "version": "7.0.10",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz",
- "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==",
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.2.tgz",
+ "integrity": "sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==",
+ "license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
- "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
"ignore-by-default": "^1.0.1",
- "minimatch": "^3.1.2",
+ "minimatch": "^10.2.1",
"pstree.remy": "^1.1.8",
"semver": "^7.5.3",
"simple-update-notifier": "^2.0.0",
@@ -4989,15 +4591,27 @@
"url": "https://opencollective.com/nodemon"
}
},
+ "node_modules/nodemon/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
"node_modules/nodemon/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
}
},
"node_modules/nodemon/node_modules/has-flag": {
@@ -5011,22 +4625,25 @@
}
},
"node_modules/nodemon/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "brace-expansion": "^5.0.2"
},
"engines": {
- "node": "*"
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/nodemon/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -5227,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",
@@ -5316,16 +4921,17 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -5354,39 +4960,10 @@
"node": ">=8"
}
},
- "node_modules/postcss": {
- "version": "8.5.6",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
- "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "nanoid": "^3.3.11",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
"node_modules/pretty-format": {
- "version": "30.2.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
- "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
+ "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5458,9 +5035,9 @@
"license": "MIT"
},
"node_modules/qs": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
- "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -5482,36 +5059,20 @@
}
},
"node_modules/raw-body": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
- "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"license": "MIT",
"dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.7.0",
- "unpipe": "1.0.0"
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
- "node_modules/raw-body/node_modules/iconv-lite": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
- "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -5532,20 +5093,24 @@
"node": ">=8.10.0"
}
},
- "node_modules/require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
- "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==",
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -5590,30 +5155,10 @@
"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.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safer-buffer": {
@@ -5622,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",
@@ -5645,31 +5178,35 @@
}
},
"node_modules/send": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
- "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"license": "MIT",
"dependencies": {
- "debug": "^4.3.5",
+ "debug": "^4.4.3",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
- "http-errors": "^2.0.0",
- "mime-types": "^3.0.1",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
- "statuses": "^2.0.1"
+ "statuses": "^2.0.2"
},
"engines": {
"node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/serve-static": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
- "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
@@ -5679,6 +5216,10 @@
},
"engines": {
"node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/setprototypeof": {
@@ -5809,9 +5350,9 @@
}
},
"node_modules/simple-update-notifier/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -5822,16 +5363,16 @@
}
},
"node_modules/sinon": {
- "version": "21.0.0",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz",
- "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==",
+ "version": "21.0.2",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.2.tgz",
+ "integrity": "sha512-VHV4UaoxIe5jrMd89Y9duI76T5g3Lp+ET+ctLhLDaZtSznDPah1KKpRElbdBV4RwqWSw2vadFiVs9Del7MbVeQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1",
- "@sinonjs/fake-timers": "^13.0.5",
- "@sinonjs/samsam": "^8.0.1",
- "diff": "^7.0.0",
+ "@sinonjs/fake-timers": "^15.1.1",
+ "@sinonjs/samsam": "^9.0.2",
+ "diff": "^8.0.3",
"supports-color": "^7.2.0"
},
"funding": {
@@ -5859,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",
@@ -6019,13 +5551,13 @@
}
},
"node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-regex": "^6.0.1"
+ "ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
@@ -6092,9 +5624,9 @@
}
},
"node_modules/superagent": {
- "version": "10.2.3",
- "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz",
- "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==",
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz",
+ "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6102,30 +5634,41 @@
"cookiejar": "^2.1.4",
"debug": "^4.3.7",
"fast-safe-stringify": "^2.1.1",
- "form-data": "^4.0.4",
+ "form-data": "^4.0.5",
"formidable": "^3.5.4",
"methods": "^1.1.2",
"mime": "2.6.0",
- "qs": "^6.11.2"
+ "qs": "^6.14.1"
},
"engines": {
"node": ">=14.18.0"
}
},
"node_modules/supertest": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz",
- "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==",
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz",
+ "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==",
"dev": true,
"license": "MIT",
"dependencies": {
+ "cookie-signature": "^1.2.2",
"methods": "^1.1.2",
- "superagent": "^10.2.3"
+ "superagent": "^10.3.0"
},
"engines": {
"node": ">=14.18.0"
}
},
+ "node_modules/supertest/node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -6146,16 +5689,10 @@
"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.11",
- "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
- "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
+ "version": "0.11.12",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
+ "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6198,7 +5735,7 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "deprecated": "Glob versions prior to v9 are no longer supported",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -6217,9 +5754,9 @@
}
},
"node_modules/test-exclude/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -6229,24 +5766,6 @@
"node": "*"
}
},
- "node_modules/tldts": {
- "version": "7.0.16",
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz",
- "integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==",
- "license": "MIT",
- "dependencies": {
- "tldts-core": "^7.0.16"
- },
- "bin": {
- "tldts": "bin/cli.js"
- }
- },
- "node_modules/tldts-core": {
- "version": "7.0.16",
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz",
- "integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==",
- "license": "MIT"
- },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -6286,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",
@@ -6367,9 +5862,9 @@
"license": "MIT"
},
"node_modules/undici-types": {
- "version": "7.13.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
- "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/unpipe": {
@@ -6417,9 +5912,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
- "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
"funding": [
{
@@ -6471,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",
@@ -6493,49 +5976,6 @@
"makeerror": "1.0.12"
}
},
- "node_modules/webidl-conversions": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
- "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
- "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==",
- "license": "MIT",
- "dependencies": {
- "iconv-lite": "0.6.3"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "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",
@@ -6667,42 +6107,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/ws": {
- "version": "8.18.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
- "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
- "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",
diff --git a/package.json b/package.json
index 779d1ffc..4e9c8b3b 100644
--- a/package.json
+++ b/package.json
@@ -33,33 +33,34 @@
"inviteMemberTests": "node --import ./env-loader.js --experimental-vm-modules node_modules/jest/bin/jest.js -t inviteMemberTests "
},
"dependencies": {
- "@iiif/helpers": "^1.5.3",
+ "@iiif/helpers": "^1.5.8",
"cookie-parser": "^1.4.7",
- "cors": "^2.8.5",
+ "cors": "^2.8.6",
"debug": "^4.4.3",
- "dompurify": "^3.2.7",
- "dotenv": "^17.2.3",
- "express": "^5.1.0",
+ "dotenv": "^17.3.1",
+ "express": "^5.2.1",
"express-list-endpoints": "^7.1.1",
- "express-oauth2-jwt-bearer": "~1.6.1",
+ "express-oauth2-jwt-bearer": "^1.7.4",
"image-size": "^2.0.2",
- "jsdom": "^27.0.0",
- "mariadb": "^3.4.5",
- "marked": "^16.3.0",
- "mime-types": "^3.0.1",
- "mongodb": "^6.20.0",
+ "mariadb": "^3.5.2",
+ "mime-types": "^3.0.2",
+ "mongodb": "^7.1.0",
"morgan": "^1.10.1",
- "nodemailer": "^7.0.6",
+ "nodemailer": "^8.0.2",
"tpen3-services": "file:"
},
"devDependencies": {
- "@jest/globals": "^30.2.0",
- "jest": "^30.2.0",
- "nodemon": "^3.1.10",
- "sinon": "^21.0.0",
- "supertest": "^7.1.4"
+ "@jest/globals": "^30.3.0",
+ "jest": "^30.3.0",
+ "nodemon": "^3.1.14",
+ "sinon": "^21.0.2",
+ "supertest": "^7.2.2"
+ },
+ "overrides": {
+ "@asamuzakjp/css-color": "~3.2.0"
},
"engines": {
- "node": ">=22.20.0"
+ "node": ">=24.14.0",
+ "npm": ">=11.0.0"
}
}
diff --git a/page/index.js b/page/index.js
index f83ff5e0..da2d61a9 100644
--- a/page/index.js
+++ b/page/index.js
@@ -7,12 +7,45 @@ import common_cors from '../utilities/common_cors.json' with {type: 'json'}
let router = express.Router({ mergeParams: true })
import Project from '../classes/Project/Project.js'
import Line from '../classes/Line/Line.js'
+import Column from '../classes/Column/Column.js'
import { findPageById, respondWithError, getLayerContainingPage, updatePageAndProject, handleVersionConflict } from '../utilities/shared.js'
+import { isSuspiciousValueString } from "../utilities/checkIfSuspicious.js"
+import { ACTIONS, ENTITIES, SCOPES } from '../project/groups/permissions_parameters.js'
router.use(
cors(common_cors)
)
+/**
+ * Splits a list of items into groups based on non-date IDs.
+ *
+ * @param {Array} items - The list of items to be split
+ * @returns {Array} An array of objects, each containing a non-date ID as the key and an array of date IDs as the value
+ */
+function splitFilterIds(items) {
+ const isDateId = (id) => /^\d+$/.test(id)
+ let result = []
+ let currentKey = null
+ let currentDates = []
+
+ for (const item of items) {
+ const id = item.id
+ if (!isDateId(id)) {
+ if (currentKey && currentDates.length > 0) {
+ result.push({ [currentKey]: currentDates })
+ }
+ currentKey = id
+ currentDates = []
+ } else {
+ currentDates.push(id)
+ }
+ }
+ if (currentKey && currentDates.length > 0) {
+ result.push({ [currentKey]: currentDates })
+ }
+ return result
+}
+
// This is a nested route for pages within a layer. It may be used
// directly from /project/:projectId/page or with /layer/:layerId/page
// depending on the context of the request.
@@ -20,66 +53,56 @@ router.route('/:pageId')
.get(async (req, res) => {
const { projectId, pageId } = req.params
try {
- const page = await findPageById(pageId, projectId, true)
- if (!page) {
- respondWithError(res, 404, 'No page found with that ID.')
- return
- }
- if (page.id?.startsWith(process.env.RERUMIDPREFIX)) {
- // If the page is a RERUM document, we need to fetch it from the server
- res.status(200).json(page)
- return
- }
- // build as AnnotationPage
- const pageAsAnnotationPage = {
- '@context': 'http://www.w3.org/ns/anno.jsonld',
- id: page.id,
- type: 'AnnotationPage',
- label: { none: [page.label] },
- target: page.target,
- partOf: [{
- id: page.partOf,
- type: "AnnotationCollection"
- }],
- items: page.items ?? [],
- prev: page.prev ?? null,
- next: page.next ?? null
- }
- res.status(200).json(pageAsAnnotationPage)
+ const page = await findPageById(pageId, projectId)
+ const pageJson = await page.asJSON(true)
+ res.status(200).json(pageJson)
} catch (error) {
return respondWithError(res, error.status ?? 500, error.message ?? 'Internal Server Error')
}
})
.put(auth0Middleware(), screenContentMiddleware(), async (req, res) => {
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
const { projectId, pageId } = req.params
const update = req.body
if (!update || typeof update !== 'object' || Object.keys(update).length === 0) {
- respondWithError(res, 400, 'No update data provided.')
- return
+ return respondWithError(res, 400, 'No update data provided.')
}
- const project = await Project.getById(projectId)
- if (!project) {
- respondWithError(res, 404, `Project with ID '${projectId}' not found`)
- return
+ if (update.items && !Array.isArray(update.items)) {
+ return respondWithError(res, 400, 'Items must be an array')
}
+ if (Array.isArray(update.items) && update.items.some(item => (typeof item !== 'object' && typeof item !== 'string') || item === null)) {
+ return respondWithError(res, 400, 'Each item must be an object')
+ }
+ const project = new Project(projectId)
+ try {
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.PAGE))) {
+ return respondWithError(res, 403, 'You do not have permission to update this page')
+ }
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Error checking permissions')
+ }
+ if (!project?.data) return respondWithError(res, 404, `Project ${projectId} was not found`)
const layerId = getLayerContainingPage(project, pageId)?.id
if (!layerId) {
- respondWithError(res, 404, `Layer containing page with ID '${pageId}' not found in project '${projectId}'`)
- return
+ return respondWithError(res, 404, `Layer containing page with ID '${pageId}' not found in project '${projectId}'`)
}
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
- if (!page) {
- respondWithError(res, 404, 'No page found with that ID.')
- return
- }
+
+ const itemsProvided = Array.isArray(update.items) && update.items.length > 0
+ let splitIds = itemsProvided ? splitFilterIds(update.items) : []
+ const updatedItemsList = []
+ let pageInProject = project.data.layers.map(layer => layer.pages.find(p => p.id.split('/').pop() === pageId)).find(p => p)
+ let pageColumnsUpdate = pageInProject?.columns ? [...pageInProject.columns] : null
+ let pageItemIds = page.items ? page.items.map(item => item.id) : []
+ const deletedIds = itemsProvided ? pageItemIds.filter(id => !update.items?.map(item => item.id).includes(id)) : []
+ updatedItemsList.push(...deletedIds.map(id => ({ [id]: null })))
// Only update top-level properties that are present in the request
Object.keys(update).forEach(key => {
page[key] = update[key]
@@ -91,17 +114,122 @@ router.route('/:pageId')
else page[key] = null
}
})
- if (update.items) {
- page.items = await Promise.all(page.items.map(async item => {
+
+ if (itemsProvided) {
+ page.items = await Promise.all(page.items.map(async (item) => {
const line = item.id?.startsWith?.('http')
? new Line(item)
: Line.build(projectId, pageId, item, user.agent.split('/').pop())
line.creator ??= user.agent.split('/').pop()
- return await line.update()
+ const updatedLine = await line.update()
+ if (item.id !== updatedLine.id && !/^\d+$/.test(item.id)) {
+ updatedItemsList.push({ [item.id]: updatedLine.id })
+ }
+ splitIds.forEach(pair => {
+ const oldKey = Object.keys(pair)[0]
+ const dateIds = pair[oldKey]
+ if (oldKey === item.id) {
+ const newDateIds = dateIds.map(dateId => {
+ if (dateId === item.id) {
+ return updatedLine.id
+ }
+ return dateId
+ })
+ pair[updatedLine.id] = newDateIds
+ delete pair[oldKey]
+ } else if (dateIds.includes(item.id)) {
+ const newDateIds = dateIds.map(dateId => {
+ if (dateId === item.id) {
+ return updatedLine.id
+ }
+ return dateId
+ })
+ pair[oldKey] = newDateIds
+ }
+ })
+ return updatedLine
}))
}
+ if (itemsProvided) {
+ for (const updatePair of updatedItemsList) {
+ const oldId = Object.keys(updatePair)[0]
+ const newId = updatePair[oldId]
+ if (pageInProject.columns && pageInProject.columns.length > 0) {
+ for (const column of pageInProject.columns) {
+ if (column.lines.includes(oldId)) {
+ const columnDB = new Column(column.id)
+ const columnData = await columnDB.getColumnData()
+ const lineIndex = columnData.lines.indexOf(oldId)
+ if (lineIndex !== -1 && newId !== null && newId !== oldId) {
+ columnData.lines[lineIndex] = newId
+ }
+ if (newId === null) {
+ columnData.lines = columnData.lines.filter(lineId => lineId !== oldId)
+ }
+ columnDB.data = columnData
+ await columnDB.update()
+ pageColumnsUpdate = pageColumnsUpdate.map(col => {
+ if (col.id === column.id) {
+ return {
+ id: column.id,
+ label: column.label,
+ lines: columnData.lines
+ }
+ }
+ return col
+ })
+ if (columnData.lines.length === 0) {
+ await columnDB.delete()
+ pageColumnsUpdate = pageColumnsUpdate.filter(col => col.id !== column.id)
+ }
+ }
+ }
+ }
+ if (!oldId.startsWith(process.env.RERUMIDPREFIX)) {
+ const splitIdsEntry = splitIds.find(pair => Object.keys(pair)[0] === newId)
+ const newColumnRecord = await Column.createNewColumn(pageId, projectId, null, [newId, ...(splitIdsEntry ? splitIdsEntry[newId] : [])])
+ const newColumn = {
+ id: newColumnRecord._id,
+ label: newColumnRecord.label,
+ lines: newColumnRecord.lines
+ }
+ pageColumnsUpdate = pageColumnsUpdate ? [...pageColumnsUpdate, newColumn] : [newColumn]
+ } else {
+ const columnToUpdate = pageColumnsUpdate.find(col => col.lines.includes(newId))
+ if (columnToUpdate) {
+ const columnDB = new Column(columnToUpdate.id)
+ const columnData = await columnDB.getColumnData()
+ const splitIdsEntry = splitIds.find(pair => Object.keys(pair)[0] === newId)
+ if (splitIdsEntry) {
+ const dateIds = splitIdsEntry[newId]
+ columnData.lines.push(...dateIds)
+ columnDB.data = columnData
+ await columnDB.update()
+ pageColumnsUpdate = pageColumnsUpdate.map(col => {
+ if (col.id === columnToUpdate.id) {
+ return {
+ id: columnToUpdate.id,
+ label: columnToUpdate.label,
+ lines: columnData.lines
+ }
+ }
+ return col
+ })
+ }
+ }
+ }
+ }
+ }
+
await updatePageAndProject(page, project, user._id)
- res.status(200).json(page)
+ if (pageColumnsUpdate) {
+ const pageInProject = project.data.layers.map(layer => layer.pages.find(p => p.id.split('/').pop() === pageId)).find(p => p)
+ pageInProject.columns = pageColumnsUpdate
+ await updatePrevAndNextColumns(pageInProject)
+ await project.update()
+ }
+ const pageJson = await page.asJSON(true)
+ res.status(200).json(pageJson)
} catch (error) {
// Handle version conflicts with optimistic locking
if (error.status === 409) {
@@ -113,9 +241,317 @@ router.route('/:pageId')
}
})
.all((req, res, next) => {
- respondWithError(res, 405, 'Improper request method, please use GET.')
+ return respondWithError(res, 405, 'Improper request method. Supported: GET, PUT.')
})
-// router.use('/:pageId/line', lineRouter)
+ /**
+ * Updates the prev and next pointers for all columns in the given page.
+ *
+ * @param {Object} page - The page object containing columns
+ * @returns {Promise}
+ */
+ async function updatePrevAndNextColumns(page) {
+ if (!page.columns || page.columns.length === 0) return
+ const allColumnIds = page.columns.map(column => column.id)
+ for (let i = 0; i < page.columns.length; i++) {
+ const column = page.columns[i]
+ const columnDB = new Column(column.id)
+ const columnData = await columnDB.getColumnData()
+ columnData.prev = i > 0 ? allColumnIds[i - 1] : null
+ columnData.next = i < page.columns.length - 1 ? allColumnIds[i + 1] : null
+ columnDB.data = columnData
+ await columnDB.update()
+ }
+ }
+
+router.route('/:pageId/column')
+ .post(auth0Middleware(), async (req, res) => {
+ const user = req.user
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+
+ const { projectId, pageId } = req.params
+ if (!projectId) return respondWithError(res, 400, "Project ID is required")
+ if (!pageId) return respondWithError(res, 400, "Page ID is required")
+
+ const project = new Project(projectId)
+ try {
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.PAGE))) {
+ return respondWithError(res, 403, 'You do not have permission to update columns on this page')
+ }
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Error checking permissions')
+ }
+
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
+ const { label, annotations } = req.body
+ if (typeof label !== 'string' || !label?.trim() || !Array.isArray(annotations)) {
+ return respondWithError(res, 400, 'Invalid column data provided.')
+ }
+ if (annotations.length === 0) {
+ return respondWithError(res, 400, 'Columns must contain at least one annotation.')
+ }
+ if (isSuspiciousValueString(label)) {
+ return respondWithError(res, 400, "Suspicious column label will not be processed.")
+ }
+ try {
+ if (!project?.data) return respondWithError(res, 404, `Project ${projectId} was not found`)
+
+ const page = project.data.layers.map(layer => layer.pages.find(p => p.id.split('/').pop() === pageId)).find(p => p)
+ if (!page) return respondWithError(res, 404, "Page not found in project")
+
+ const pageItemIds = page.items?.map(item => item.id) || []
+ const invalidAnnotations = annotations.filter(id => !pageItemIds.includes(id))
+ if (invalidAnnotations.length > 0) {
+ return respondWithError(res, 400, `The following annotations do not exist on this page: ${invalidAnnotations.join(', ')}`)
+ }
+
+ const existingLabels = page.columns ? page.columns.map(column => column.label) : []
+ if (existingLabels.includes(label)) {
+ return respondWithError(res, 400, `A column with the label '${label}' already exists.`)
+ }
+
+ if (page.columns && page.columns.length > 0) {
+ for (const column of page.columns) {
+ const overlappingAnnotations = column.lines.filter(annId => annotations.includes(annId))
+ if (column.lines.length === overlappingAnnotations.length) {
+ const columnDB = new Column(column.id)
+ await columnDB.delete()
+ page.columns = page.columns.filter(col => col.id !== column.id)
+ continue
+ }
+ if (overlappingAnnotations.length > 0) {
+ const columnDB = new Column(column.id)
+ const columnData = await columnDB.getColumnData()
+ columnData.lines = columnData.lines.filter(annId => !annotations.includes(annId))
+ columnDB.data = columnData
+ await columnDB.update()
+ column.lines = column.lines.filter(annId => !annotations.includes(annId))
+ }
+ }
+ }
+
+ const newColumnRecord = await Column.createNewColumn(pageId, projectId, label, annotations)
+ const newColumn = {
+ id: newColumnRecord._id,
+ label: newColumnRecord.label,
+ lines: newColumnRecord.lines
+ }
+
+ page.columns = [...(page.columns || []), newColumn]
+
+ await updatePrevAndNextColumns(page)
+ await project.update()
+ res.status(201).json(newColumnRecord)
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Internal Server Error')
+ }
+ })
+ .put(auth0Middleware(), async (req, res) => {
+ const user = req.user
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+
+ const { projectId, pageId } = req.params
+ if (!projectId) return respondWithError(res, 400, "Project ID is required")
+ if (!pageId) return respondWithError(res, 400, "Page ID is required")
+
+ const project = new Project(projectId)
+ try {
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.PAGE))) {
+ return respondWithError(res, 403, 'You do not have permission to merge columns on this page')
+ }
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Error checking permissions')
+ }
+
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
+ const { newLabel, columnLabelsToMerge } = req.body
+ if (typeof newLabel !== 'string' || !newLabel?.trim() || !Array.isArray(columnLabelsToMerge) || columnLabelsToMerge.length < 2) {
+ return respondWithError(res, 400, 'Invalid column merge data provided.')
+ }
+ if (isSuspiciousValueString(newLabel)) {
+ return respondWithError(res, 400, "Suspicious column label will not be processed.")
+ }
+ try {
+ if (!project?.data) return respondWithError(res, 404, `Project ${projectId} was not found`)
+
+ const page = project.data.layers.map(layer => layer.pages.find(p => p.id.split('/').pop() === pageId)).find(p => p)
+ if (!page) return respondWithError(res, 404, "Page not found in project")
+
+ if (!page.columns || page.columns.length === 0) {
+ return respondWithError(res, 404, "No columns exist on this page")
+ }
+
+ const columnsToMerge = page.columns.filter(column => columnLabelsToMerge.includes(column.label))
+ if (columnsToMerge.length !== columnLabelsToMerge.length) {
+ return respondWithError(res, 404, 'One or more columns to merge not found.')
+ }
+
+ const uniqueLabels = new Set(page.columns.map(column => column.label).filter(label => !columnLabelsToMerge.includes(label)))
+ if (uniqueLabels.has(newLabel)) {
+ return respondWithError(res, 400, `A column with the label '${newLabel}' already exists.`)
+ }
+ const mergedLines = columnsToMerge.flatMap(column => column.lines)
+ const allColumnLines = page.columns ? page.columns.flatMap(column => !columnLabelsToMerge.includes(column.label) ? column.lines : []) : []
+ const duplicateAnnotations = mergedLines.filter(annId => allColumnLines.includes(annId))
+ if (duplicateAnnotations.length > 0) {
+ return respondWithError(res, 400, `The following annotations are already assigned to other columns: ${duplicateAnnotations.join(', ')}`)
+ }
+ const mergedColumnRecord = await Column.createNewColumn(pageId, projectId, newLabel, mergedLines)
+ const mergedColumn = {
+ id: mergedColumnRecord._id,
+ label: mergedColumnRecord.label,
+ lines: mergedColumnRecord.lines
+ }
+
+ for (const label of columnLabelsToMerge) {
+ const columnToDelete = page.columns.find(column => column.label === label)
+ if (columnToDelete) {
+ const columnDB = new Column(columnToDelete.id)
+ await columnDB.delete()
+ }
+ }
+
+ page.columns = page.columns.filter(column => !columnLabelsToMerge.includes(column.label))
+ page.columns.push(mergedColumn)
+
+ await updatePrevAndNextColumns(page)
+ await project.update()
+ res.status(200).json(mergedColumnRecord)
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Internal Server Error')
+ }
+ })
+ .patch(auth0Middleware(), async (req, res) => {
+ const user = req.user
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+
+ const { projectId, pageId } = req.params
+ if (!projectId) return respondWithError(res, 400, "Project ID is required")
+ if (!pageId) return respondWithError(res, 400, "Page ID is required")
+
+ const project = new Project(projectId)
+ try {
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.PAGE))) {
+ return respondWithError(res, 403, 'You do not have permission to update columns on this page')
+ }
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Error checking permissions')
+ }
+
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
+ const { columnLabel, annotationIdsToAdd } = req.body
+ if (typeof columnLabel !== 'string' || !columnLabel?.trim() || !Array.isArray(annotationIdsToAdd) || annotationIdsToAdd.length === 0) {
+ return respondWithError(res, 400, 'Invalid column update data provided.')
+ }
+ if(isSuspiciousValueString(columnLabel)) {
+ return respondWithError(res, 400, "Suspicious column label will not be processed.")
+ }
+ try {
+ if (!project?.data) return respondWithError(res, 404, `Project ${projectId} was not found`)
+
+ const page = project.data.layers.map(layer => layer.pages.find(p => p.id.split('/').pop() === pageId)).find(p => p)
+ if (!page) return respondWithError(res, 404, "Page not found in project")
+
+ if (!page.columns || page.columns.length === 0) {
+ return respondWithError(res, 404, "No columns exist on this page")
+ }
+
+ const columnToUpdate = page.columns.find(column => column.label === columnLabel)
+ if (!columnToUpdate) {
+ return respondWithError(res, 404, 'Column to update not found.')
+ }
+
+ const pageItemIds = page.items?.map(item => item.id) || []
+ const invalidAnnotations = annotationIdsToAdd.filter(id => !pageItemIds.includes(id))
+ if (invalidAnnotations.length > 0) {
+ return respondWithError(res, 400, `The following annotations do not exist on this page: ${invalidAnnotations.join(', ')}`)
+ }
+
+ const allColumnLines = page.columns ? page.columns.flatMap(column => column.lines) : []
+ const duplicateAnnotations = annotationIdsToAdd.filter(annId => allColumnLines.includes(annId))
+ if (duplicateAnnotations.length > 0) {
+ return respondWithError(res, 400, `The following annotations are already assigned to other columns: ${duplicateAnnotations.join(', ')}`)
+ }
+
+ const columnDB = new Column(columnToUpdate.id)
+ const columnData = await columnDB.getColumnData()
+ const newLines = Array.from(new Set([...columnData.lines, ...annotationIdsToAdd]))
+ columnData.lines = newLines
+ columnDB.data = columnData
+ await columnDB.update()
+
+ columnToUpdate.lines = newLines
+
+ await project.update()
+ res.status(200).json({ message: "Column updated successfully." })
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Internal Server Error')
+ }
+ })
+ .all((req, res, next) => {
+ return respondWithError(res, 405, 'Improper request method. Supported: POST, PUT, PATCH.')
+ })
+
+router.route('/:pageId/clear-columns')
+ .delete(auth0Middleware(), async (req, res) => {
+ const user = req.user
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+
+ const { projectId, pageId } = req.params
+ if (!projectId) return respondWithError(res, 400, "Project ID is required")
+ if (!pageId) return respondWithError(res, 400, "Page ID is required")
+ try {
+ const project = new Project(projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.DELETE, SCOPES.ALL, ENTITIES.PAGE))) {
+ return respondWithError(res, 403, 'You do not have permission to clear columns on this page')
+ }
+ if (!project?.data) return respondWithError(res, 404, `Project ${projectId} was not found`)
+
+ const page = project.data.layers.map(layer => layer.pages.find(p => p.id.split('/').pop() === pageId)).find(p => p)
+ if (!page) return respondWithError(res, 404, "Page not found in project")
+
+ if (!page.columns || page.columns.length === 0) {
+ return res.status(204).send()
+ }
+
+ const columnIds = page.columns.map(column => column.id)
+ for(const columnId of columnIds) {
+ const columnDB = new Column(columnId)
+ await columnDB.delete()
+ }
+ delete page.columns
+
+ await project.update()
+ res.status(204).send()
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Internal Server Error')
+ }
+ })
+ .all((req, res, next) => {
+ return respondWithError(res, 405, 'Improper request method, please use DELETE.')
+ })
+
+// Fully resolved page endpoint - returns page with fully populated annotation data
+router.route('/:pageId/resolved')
+ .get(async (req, res) => {
+ const { projectId, pageId } = req.params
+ try {
+ const pageData = await findPageById(pageId, projectId)
+ await pageData.resolvePageItems()
+ const pageJson = await pageData.asJSON(true)
+ res.status(200).json(pageJson)
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Internal Server Error')
+ }
+ })
+ .all((req, res) => {
+ return respondWithError(res, 405, 'Improper request method, please use GET.')
+ })
export default router
diff --git a/project/customMetadataRouter.js b/project/customMetadataRouter.js
index a7d7214d..3609bd8d 100644
--- a/project/customMetadataRouter.js
+++ b/project/customMetadataRouter.js
@@ -74,17 +74,23 @@ function deepUpsert(target, source) {
}
// GET /project/:id/custom - Returns array of namespace keys
-router.route("/:id/custom").get(async (req, res) => {
+router.route("/:id/custom").get(auth0Middleware(), async (req, res) => {
+ const user = req.user
const { id } = req.params
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
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)
- // Fetch the project from database
- const projectData = await database.findOne({ _id: id }, "projects")
+ // Check if user has read access to the project options
+ if (!await project.checkUserAccess(user._id, ACTIONS.READ, SCOPES.OPTIONS, ENTITIES.PROJECT)) {
+ return respondWithError(res, 403, "You do not have permission to read this project's metadata")
+ }
+
+ const projectData = project.data
if (!projectData) {
return respondWithError(res, 404, `No TPEN3 project with ID '${id}' found`)
@@ -109,7 +115,7 @@ router.route("/:id/custom").post(auth0Middleware(), async (req, res) => {
const payload = req.body
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!id) return respondWithError(res, 400, "No TPEN3 ID provided")
if (!validateID(id)) return respondWithError(res, 400, "The TPEN3 project ID provided is invalid")
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
@@ -132,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`)
@@ -164,7 +170,7 @@ router.route("/:id/custom").put(auth0Middleware(), async (req, res) => {
const { id } = req.params
const payload = req.body
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!id) return respondWithError(res, 400, "No TPEN3 ID provided")
if (!validateID(id)) return respondWithError(res, 400, "The TPEN3 project ID provided is invalid")
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
@@ -188,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`)
@@ -228,7 +234,7 @@ router.route("/:id/custom").put(auth0Middleware(), async (req, res) => {
})
router.route("/:id/custom").all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use GET, POST, or PUT instead")
+ return respondWithError(res, 405, "Improper request method. Use GET, POST, or PUT instead")
})
export default router
diff --git a/project/customRolesRouter.js b/project/customRolesRouter.js
index e618fd7c..1e7fc206 100644
--- a/project/customRolesRouter.js
+++ b/project/customRolesRouter.js
@@ -12,7 +12,7 @@ const router = express.Router({ mergeParams: true })
router.get('/:projectId/customRoles', auth0Middleware(), async (req, res) => {
const { projectId } = req.params
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
try {
const project = new Project(projectId)
if (!project) {
@@ -29,7 +29,7 @@ router.get('/:projectId/customRoles', auth0Middleware(), async (req, res) => {
res.status(200).json(customRoles)
} catch (error) {
console.error(error)
- respondWithError(res, error.status ?? 500, error.message ?? "Internal Server Error")
+ return respondWithError(res, error.status ?? 500, error.message ?? "Internal Server Error")
}
})
@@ -38,7 +38,7 @@ router.post('/:projectId/addCustomRoles', auth0Middleware(), async (req, res) =>
const { projectId } = req.params
let customRoles = req.body.roles ?? req.body
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!Object.keys(customRoles).length) {
return respondWithError(res, 400, "Custom roles must be provided as a JSON Object with keys as roles and values as arrays of permissions or space-delimited strings.")
}
@@ -52,7 +52,7 @@ router.post('/:projectId/addCustomRoles', auth0Middleware(), async (req, res) =>
await group.addCustomRoles(customRoles)
res.status(201).json({ message: 'Custom roles added successfully.' })
} catch (error) {
- respondWithError(res, error.status ?? 500, error.message ?? 'Error adding custom roles.')
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Error adding custom roles.')
}
})
@@ -61,7 +61,7 @@ router.put('/:projectId/updateCustomRoles', auth0Middleware(), async (req, res)
const { projectId } = req.params
let roles = req.body.roles ?? req.body
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!Object.keys(roles).length) {
return respondWithError(res, 400, "Custom roles must be provided as a JSON Object with keys as roles and values as arrays of permissions or space-delimited strings.")
}
@@ -75,7 +75,7 @@ router.put('/:projectId/updateCustomRoles', auth0Middleware(), async (req, res)
await group.updateCustomRoles(roles)
res.status(200).json({ message: 'Custom roles set successfully.' })
} catch (error) {
- respondWithError(res, error.status ?? 500, error.message ?? 'Error setting custom roles.')
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Error setting custom roles.')
}
})
@@ -84,7 +84,7 @@ router.delete('/:projectId/removeCustomRoles', auth0Middleware(), async (req, re
const { projectId } = req.params
let rolesToRemove = req.body.roles ?? req.body
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (typeof rolesToRemove === 'object' && !Array.isArray(rolesToRemove)) {
rolesToRemove = Object.keys(rolesToRemove)
}
@@ -103,7 +103,7 @@ router.delete('/:projectId/removeCustomRoles', auth0Middleware(), async (req, re
await group.removeCustomRoles(rolesToRemove)
res.status(200).json({ message: 'Custom roles removed successfully.' })
} catch (error) {
- respondWithError(res, error.status ?? 500, error.message ?? 'Error removing custom roles.')
+ return respondWithError(res, error.status ?? 500, error.message ?? 'Error removing custom roles.')
}
})
diff --git a/project/import28Router.js b/project/import28Router.js
index 2445c754..054c6fb8 100644
--- a/project/import28Router.js
+++ b/project/import28Router.js
@@ -68,7 +68,7 @@ router.route("/import28/:uid").get(
const jsessionid = req.cookies?.JSESSIONID
const uid = req.params?.uid
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!jsessionid) return respondWithError(res, 400, "Missing jsessionid in query")
if (!uid) return respondWithError(res, 400, "Missing uid in query")
@@ -123,7 +123,7 @@ router.route("/import28/selectedproject/:selectedProjectId").get(
const jsessionid = req.cookies?.JSESSIONID
const selectedProjectId = req.params?.selectedProjectId
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!jsessionid) return respondWithError(res, 400, "Missing jsessionid in query")
if (!selectedProjectId) return respondWithError(res, 400, "Missing selectedProjectId in query")
@@ -162,15 +162,18 @@ router.route("/import28/selectedproject/:selectedProjectId").get(
tools = await new Tools().validateAllTools(tools)
let importData
if (!checkURL.valid)
- return res.status(checkURL.status).json({message: checkURL.message, resolvedPayload: checkURL.resolvedPayload})
+ return respondWithError(res, checkURL.status, checkURL.message)
+ //return res.status(checkURL.status).json({message: checkURL.message, resolvedPayload: checkURL.resolvedPayload})
try {
importData = await ProjectFactory.fromManifestURL(manifestURL, user.agent.split('/').pop(), tools, true)
} catch (error) {
- res.status(error.status ?? 500).json({
- status: error.status ?? 500,
- message: error.message,
- data: error.resolvedPayload
- })
+ return respondWithError(res, error.status ?? 500, error.message ?? "An error occurred during project import")
+
+ // res.status(error.status ?? 500).json({
+ // status: error.status ?? 500,
+ // message: error.message,
+ // data: error.resolvedPayload
+ // })
}
await ProjectFactory.importTPEN28(parsedData, importData, req.cookies.userToken, req.protocol)
diff --git a/project/index.js b/project/index.js
index eb5eb760..b5aecee9 100644
--- a/project/index.js
+++ b/project/index.js
@@ -13,6 +13,7 @@ import memberUpgradeRouter from "./memberUpgradeRouter.js"
import memberDeclineInviteRouter from "./memberDeclineInviteRouter.js"
import projectCopyRouter from "./projectCopyRouter.js"
import customMetadataRouter from "./customMetadataRouter.js"
+import memberLeaveRouter from "./memberLeaveRouter.js"
const router = express.Router({ mergeParams: true })
router.use(cors(common_cors))
@@ -29,6 +30,7 @@ router.use(metadataRouter)
router.use(projectToolsRouter)
router.use(projectCopyRouter)
router.use(customMetadataRouter)
+router.use(memberLeaveRouter)
// Nested route for layers within a project
router.use('/:projectId/layer', layerRouter)
diff --git a/project/memberDeclineInviteRouter.js b/project/memberDeclineInviteRouter.js
index 57dc0ff8..f0180be6 100644
--- a/project/memberDeclineInviteRouter.js
+++ b/project/memberDeclineInviteRouter.js
@@ -6,7 +6,7 @@ import User from "../classes/User/User.js"
const router = express.Router({ mergeParams: true })
/**
- * A user is declining from the E-mail they recieved. It is unauthenticated.
+ * A user is declining from the E-mail they received. It is unauthenticated.
* Their member entry should be removed from the Group.
* Their temporary user should be removed from the db.
*
@@ -30,12 +30,12 @@ router.route("/:projectId/collaborator/:collaboratorId/decline").get(async (req,
} catch (error) {
return respondWithError(
res,
- error.status || error.code || 500,
+ error.status ?? 500,
error.message ?? "There was an error declining the invitation."
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use GET instead")
+ return respondWithError(res, 405, "Improper request method. Use GET instead")
})
export default router
diff --git a/project/memberLeaveRouter.js b/project/memberLeaveRouter.js
new file mode 100644
index 00000000..16a4642a
--- /dev/null
+++ b/project/memberLeaveRouter.js
@@ -0,0 +1,41 @@
+import express from "express"
+import { respondWithError } from "../utilities/shared.js"
+import auth0Middleware from "../auth/index.js"
+import Project from "../classes/Project/Project.js"
+
+const router = express.Router({ mergeParams: true })
+
+/**
+ * Allows an authenticated member to voluntarily leave a project.
+ *
+ * @route POST /project/:id/leave
+ * @param {string} id - The project the user wants to leave
+ * @returns {Object} Success message with project id
+ */
+router.route("/:id/leave").post(auth0Middleware(), async (req, res) => {
+ const { id: projectId } = req.params
+ const user = req.user
+
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+ if (!projectId) return respondWithError(res, 400, "Project ID is required")
+
+ try {
+ const project = await Project.getById(projectId)
+ if (!project?.data) {
+ return respondWithError(res, 404, "Project not found")
+ }
+
+ await project.removeMember(user._id, true)
+
+ res.status(200).json({
+ message: "Successfully left the project",
+ projectId: projectId
+ })
+
+ } catch (error) {
+ return respondWithError(res, error.status ?? 500,
+ error.message ?? "Error leaving project")
+ }
+})
+
+export default router
diff --git a/project/memberRouter.js b/project/memberRouter.js
index 608c2937..1e21f581 100644
--- a/project/memberRouter.js
+++ b/project/memberRouter.js
@@ -13,8 +13,11 @@ const router = express.Router({ mergeParams: true })
router.route("/:id/invite-member").post(auth0Middleware(), async (req, res) => {
const user = req.user
const { id: projectId } = req.params
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
const { email, roles } = req.body
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
if (!email) return respondWithError(res, 400, "Invitee's email is required")
if (!isValidEmail(email)) return respondWithError(res, 400, "Invitee email is invalid")
try {
@@ -23,10 +26,10 @@ router.route("/:id/invite-member").post(auth0Middleware(), async (req, res) => {
const response = await project.sendInvite(email, roles)
res.status(200).json(response)
} else {
- res.status(403).send("You do not have permission to invite members to this project")
+ return respondWithError(res, 403, "You do not have permission to invite members to this project")
}
} catch (error) {
- res.status(error.status || 500).send(error.message.toString())
+ return respondWithError(res, error.status ?? 500, error.message ?? "Error inviting member")
}
})
@@ -34,9 +37,12 @@ router.route("/:id/invite-member").post(auth0Middleware(), async (req, res) => {
router.route("/:id/remove-member").post(auth0Middleware(), async (req, res) => {
const user = req.user
const { id: projectId } = req.params
- const { userId } = req.body
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!projectId) return respondWithError(res, 400, "Project ID is required")
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
+ const { userId } = req.body
if (!userId) return respondWithError(res, 400, "User ID is required")
try {
const project = new Project(projectId)
@@ -44,7 +50,7 @@ router.route("/:id/remove-member").post(auth0Middleware(), async (req, res) => {
await project.removeMember(userId)
res.sendStatus(204)
} else {
- res.status(403).send("You do not have permission to remove members from this project")
+ return respondWithError(res, 403, "You do not have permission to remove members from this project")
}
} catch (error) {
return respondWithError(res, error.status ?? 500, error.message ?? "Error removing member from project.")
@@ -56,17 +62,16 @@ router.route("/:projectId/collaborator/:collaboratorId/addRoles").post(auth0Midd
const { projectId, collaboratorId } = req.params
const roles = req.body.roles ?? req.body
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!roles) return respondWithError(res, 400, "Provide role(s) to add")
try {
- const projectObj = new Project(projectId)
+ const projectObj = await Project.getById(projectId)
if (!(await projectObj.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.MEMBER))) {
return respondWithError(res, 403, "You do not have permission to add roles to members.")
}
const groupId = projectObj.data.group
const group = new Group(groupId)
await group.addMemberRoles(collaboratorId, roles)
- await group.update()
res.status(200).send(`Roles added to member ${collaboratorId}.`)
} catch (error) {
return respondWithError(res, error.status ?? 500, error.message ?? "Error adding roles to member.")
@@ -77,10 +82,10 @@ router.route("/:projectId/collaborator/:collaboratorId/setRoles").put(auth0Middl
const { projectId, collaboratorId } = req.params
const roles = req.body.roles ?? req.body
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!roles) return respondWithError(res, 400, "Provide role(s) to update")
try {
- const projectObj = new Project(projectId)
+ const projectObj = await Project.getById(projectId)
if (!(await projectObj.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.MEMBER))) {
return respondWithError(res, 403, "You do not have permission to update member roles.")
}
@@ -96,18 +101,18 @@ router.route("/:projectId/collaborator/:collaboratorId/removeRoles").post(auth0M
const { projectId, collaboratorId } = req.params
const roles = req.body.roles ?? req.body
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!roles) return respondWithError(res, 400, "Provide role(s) to remove")
if (roles.includes("OWNER")) return respondWithError(res, 400, "The OWNER role cannot be removed.")
try {
- const projectObj = new Project(projectId)
+ const projectObj = await Project.getById(projectId)
if (!(await projectObj.checkUserAccess(user._id, ACTIONS.DELETE, SCOPES.ALL, ENTITIES.MEMBER))) {
return respondWithError(res, 403, "You do not have permission to remove roles from members.")
}
const groupId = projectObj.data.group
const group = new Group(groupId)
await group.removeMemberRoles(collaboratorId, roles)
- res.status(204).send(`Roles [${roles}] removed from member ${collaboratorId}.`)
+ res.sendStatus(204)
} catch (error) {
return respondWithError(res, error.status ?? 500, error.message ?? "Error removing roles from member.")
}
@@ -116,24 +121,24 @@ router.route("/:projectId/collaborator/:collaboratorId/removeRoles").post(auth0M
// Switch project owner
router.route("/:projectId/switch/owner").post(auth0Middleware(), async (req, res) => {
const { projectId } = req.params
- const { newOwnerId } = req.body
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
+ const { newOwnerId } = req.body
if (!newOwnerId) return respondWithError(res, 400, "Provide the ID of the new owner.")
if (user._id === newOwnerId) return respondWithError(res, 400, "Cannot transfer ownership to the current owner.")
try {
- const projectObj = new Project(projectId)
+ const projectObj = await Project.getById(projectId)
if (!(await projectObj.checkUserAccess(user._id, ACTIONS.ALL, SCOPES.ALL, ENTITIES.ALL))) {
return respondWithError(res, 403, "You do not have permission to transfer ownership.")
}
const group = new Group(projectObj.data.group)
- if (user._id === newOwnerId) {
- return respondWithError(res, 400, "Cannot transfer ownership to the current owner.")
- }
const currentRoles = await group.getMemberRoles(user._id)
- Object.keys(currentRoles).length === 1 && await group.addMemberRoles(user._id, ["CONTRIBUTOR"])
- await group.addMemberRoles(newOwnerId, ["OWNER"], true)
- await group.removeMemberRoles(user._id, ["OWNER"], true)
+ Object.keys(currentRoles).length === 1 && await group.addMemberRoles(user._id, ["CONTRIBUTOR"], false, false)
+ await group.addMemberRoles(newOwnerId, ["OWNER"], true, false)
+ await group.removeMemberRoles(user._id, ["OWNER"], true, false)
await group.update()
res.status(200).json({ message: `Ownership successfully transferred to member ${newOwnerId}.` })
} catch (error) {
diff --git a/project/memberUpgradeRouter.js b/project/memberUpgradeRouter.js
index 74c1944b..22b76164 100644
--- a/project/memberUpgradeRouter.js
+++ b/project/memberUpgradeRouter.js
@@ -1,5 +1,5 @@
import express from "express"
-import { validateID, respondWithError } from "../utilities/shared.js"
+import { respondWithError } from "../utilities/shared.js"
import Project from "../classes/Project/Project.js"
import Group from "../classes/Group/Group.js"
import User from "../classes/User/User.js"
@@ -46,12 +46,12 @@ router.route("/:projectId/collaborator/:collaboratorId/agent/:agentId").get(asyn
} catch (error) {
return respondWithError(
res,
- error.status || error.code || 500,
+ error.status ?? 500,
error.message ?? "An error occurred."
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use GET instead")
+ return respondWithError(res, 405, "Improper request method. Use GET instead")
})
export default router
diff --git a/project/metadataRouter.js b/project/metadataRouter.js
index 12fcd19e..1c07f4c5 100644
--- a/project/metadataRouter.js
+++ b/project/metadataRouter.js
@@ -11,7 +11,7 @@ router.route("/:projectId/metadata").put(auth0Middleware(), screenContentMiddlew
const { projectId } = req.params
const metadata = req.body
const user = req.user
- if (!user) return respondWithError(res, 401, "Unauthenticated request")
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
if (!metadata || !Array.isArray(metadata)) {
return respondWithError(res, 400, "Invalid metadata provided. Expected an array of objects with 'label' and 'value'.")
}
diff --git a/project/projectCopyRouter.js b/project/projectCopyRouter.js
index d89033f0..cae1df18 100644
--- a/project/projectCopyRouter.js
+++ b/project/projectCopyRouter.js
@@ -2,91 +2,112 @@ import express from "express"
import { respondWithError } from "../utilities/shared.js"
import auth0Middleware from "../auth/index.js"
import ProjectFactory from "../classes/Project/ProjectFactory.js"
+import Project from "../classes/Project/Project.js"
+import { ACTIONS, ENTITIES, SCOPES } from "./groups/permissions_parameters.js"
const router = express.Router({ mergeParams: true })
router.route("/:projectId/copy").post(auth0Middleware(), async (req, res) => {
const user = req.user
if (!user) {
- return respondWithError(res, 401, "Unauthorized: User not authenticated")
+ return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
}
try {
const { projectId } = req.params
+ const projectObj = new Project(projectId)
+ if (!(await projectObj.checkUserAccess(user._id, ACTIONS.READ, SCOPES.ALL, ENTITIES.PROJECT))) {
+ return respondWithError(res, 403, "You do not have permission to copy this project")
+ }
const project = await ProjectFactory.copyProject(projectId, user._id)
res.status(201).json(project)
} catch (error) {
- respondWithError(
+ return respondWithError(
res,
- error.status ?? error.code ?? 500,
+ error.status ?? 500,
error.message ?? "Unknown server error"
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use POST instead")
+ return respondWithError(res, 405, "Improper request method. Use POST instead")
})
router.route("/:projectId/copy-without-annotations").post(auth0Middleware(), async (req, res) => {
const user = req.user
if (!user) {
- return respondWithError(res, 401, "Unauthorized: User not authenticated")
+ return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
}
try {
const { projectId } = req.params
+ const projectObj = new Project(projectId)
+ if (!(await projectObj.checkUserAccess(user._id, ACTIONS.READ, SCOPES.ALL, ENTITIES.PROJECT))) {
+ return respondWithError(res, 403, "You do not have permission to copy this project")
+ }
const project = await ProjectFactory.cloneWithoutAnnotations(projectId, user._id)
res.status(201).json(project)
} catch (error) {
- respondWithError(
+ return respondWithError(
res,
- error.status ?? error.code ?? 500,
+ error.status ?? 500,
error.message ?? "Unknown server error"
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use POST instead")
+ return respondWithError(res, 405, "Improper request method. Use POST instead")
})
router.route("/:projectId/copy-with-group").post(auth0Middleware(), async (req, res) => {
const user = req.user
if (!user) {
- return respondWithError(res, 401, "Unauthorized: User not authenticated")
+ return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
}
try {
const { projectId } = req.params
+ const projectObj = new Project(projectId)
+ if (!(await projectObj.checkUserAccess(user._id, ACTIONS.READ, SCOPES.ALL, ENTITIES.PROJECT))) {
+ return respondWithError(res, 403, "You do not have permission to copy this project")
+ }
const project = await ProjectFactory.cloneWithGroup(projectId, user._id)
res.status(201).json(project)
} catch (error) {
- respondWithError(
+ return respondWithError(
res,
- error.status ?? error.code ?? 500,
+ error.status ?? 500,
error.message ?? "Unknown server error"
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use POST instead")
+ return respondWithError(res, 405, "Improper request method. Use POST instead")
})
router.route("/:projectId/copy-with-customizations").post(auth0Middleware(), async (req, res) => {
const user = req.user
- const { modules } = req.body
if (!user) {
- return respondWithError(res, 401, "Unauthorized: User not authenticated")
+ return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+ }
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
}
+ const { modules } = req.body
if (!modules || typeof modules !== 'object') {
return respondWithError(res, 400, "Bad Request: 'modules' must be an object")
}
try {
const { projectId } = req.params
+ const projectObj = new Project(projectId)
+ if (!(await projectObj.checkUserAccess(user._id, ACTIONS.READ, SCOPES.ALL, ENTITIES.PROJECT))) {
+ return respondWithError(res, 403, "You do not have permission to copy this project")
+ }
const project = await ProjectFactory.cloneWithCustomizations(projectId, user._id, modules)
res.status(201).json(project)
} catch (error) {
- respondWithError(
+ return respondWithError(
res,
- error.status ?? error.code ?? 500,
+ error.status ?? 500,
error.message ?? "Unknown server error"
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use POST instead")
+ return respondWithError(res, 405, "Improper request method. Use POST instead")
})
export default router
diff --git a/project/projectCreateRouter.js b/project/projectCreateRouter.js
index a37c3773..cb1c24b9 100644
--- a/project/projectCreateRouter.js
+++ b/project/projectCreateRouter.js
@@ -7,12 +7,13 @@ import Project from "../classes/Project/Project.js"
import screenContentMiddleware from "../utilities/checkIfSuspicious.js"
import { isSuspiciousJSON, isSuspiciousValueString, hasSuspiciousProjectData } from "../utilities/checkIfSuspicious.js"
import Tools from "../classes/Tools/Tools.js"
+import { ACTIONS, ENTITIES, SCOPES } from "./groups/permissions_parameters.js"
const router = express.Router({ mergeParams: true })
router.route("/create").post(auth0Middleware(), screenContentMiddleware(), async (req, res) => {
const user = req.user
- if (!user?.agent) return respondWithError(res, 401, "Unauthenticated user")
+ if (!user?.agent) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
let project = req.body
try {
if (hasSuspiciousProjectData(project)) return respondWithError(res, 400, "Suspicious project data will not be processed.")
@@ -24,35 +25,30 @@ router.route("/create").post(auth0Middleware(), screenContentMiddleware(), async
} catch (error) {
console.log("Project creation error")
console.error(error)
- respondWithError(
+ return respondWithError(
res,
- error.status ?? error.code ?? 500,
+ error.status ?? 500,
error.message ?? "Unknown server error"
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use POST instead")
+ return respondWithError(res, 405, "Improper request method. Use POST instead")
})
router.route("/import").post(auth0Middleware(), async (req, res) => {
let { createFrom } = req.query
let user = req.user
+ if (!user?.agent) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
createFrom = createFrom?.toLowerCase()
if (!createFrom)
- return res.status(400).json({
- message:
- "Query string 'createFrom' is required, specify manifest source as 'URL' or 'DOC' "
- })
+ return respondWithError(res, 400, "Query string 'createFrom' is required, specify manifest source as 'URL' or 'DOC'")
if (createFrom === "url") {
const manifestURL = req?.body?.url
let tools = req?.body?.tools ?? []
tools = await new Tools().validateAllTools(tools)
let checkURL = await validateURL(manifestURL)
if (!checkURL.valid)
- return res.status(checkURL.status).json({
- message: checkURL.message,
- resolvedPayload: checkURL.resolvedPayload
- })
+ return respondWithError(res, checkURL.status, checkURL.message)
try {
if (isSuspiciousJSON(checkURL.resolvedPayload)) return respondWithError(res, 400, "Suspicious data will not be processed.")
const result = await ProjectFactory.fromManifestURL(
@@ -64,11 +60,12 @@ router.route("/import").post(auth0Middleware(), async (req, res) => {
} catch (error) {
console.log("project import error")
console.error(error)
- res.status(error.status ?? 500).json({
- status: error.status ?? 500,
- message: error.message,
- data: error.resolvedPayload
- })
+ return respondWithError(res, error.status ?? 500, error.message ?? "Error importing project")
+ // res.status(error.status ?? 500).json({
+ // status: error.status ?? 500,
+ // message: error.message,
+ // data: error.resolvedPayload
+ // })
}
} else if (createFrom === "tpen28url") {
const manifestURL = req?.body?.url
@@ -76,10 +73,7 @@ router.route("/import").post(auth0Middleware(), async (req, res) => {
tools = await new Tools().validateAllTools(tools)
let checkURL = await validateURL(manifestURL)
if (!checkURL.valid)
- return res.status(checkURL.status).json({
- message: checkURL.message,
- resolvedPayload: checkURL.resolvedPayload
- })
+ return respondWithError(res, checkURL.status, checkURL.message)
try {
if (isSuspiciousJSON(checkURL.resolvedPayload)) return respondWithError(res, 400, "Suspicious data will not be processed.")
const result = await ProjectFactory.fromManifestURL(
@@ -92,24 +86,21 @@ router.route("/import").post(auth0Middleware(), async (req, res) => {
} catch (error) {
console.log("TPEN 2.8 project import error")
console.error(error)
- res.status(error.status ?? 500).json({
- status: error.status ?? 500,
- message: error.message,
- data: error.resolvedPayload
- })
+ return respondWithError(res, error.status ?? 500, error.message ?? "Error importing TPEN 2.8 project")
}
} else {
- res.status(400).json({
- message: `Import from ${createFrom} is not available. Create from URL instead`
- })
+ return respondWithError(res, 400, `Import from ${createFrom} is not available. Create from URL instead`)
}
}).all((req, res) => {
- respondWithError(res, 405, "Improper request method. Use POST instead")
+ return respondWithError(res, 405, "Improper request method. Use POST instead")
})
router.route("/import-image").post(auth0Middleware(), async (req, res) => {
const user = req.user
- if (!user?.agent) return respondWithError(res, 401, "Unauthenticated user")
+ if (!user?.agent) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
try {
const { imageUrls, projectLabel } = req.body
if (!imageUrls || !projectLabel) {
@@ -124,15 +115,15 @@ router.route("/import-image").post(auth0Middleware(), async (req, res) => {
} catch (error) {
console.log("Create project from image error")
console.error(error)
- respondWithError(
+ return respondWithError(
res,
- error.status ?? error.code ?? 500,
+ error.status ?? 500,
error.message ?? "Unknown server error"
)
}
}
).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use POST instead")
+ return respondWithError(res, 405, "Improper request method. Use POST instead")
})
/**
@@ -140,26 +131,32 @@ router.route("/import-image").post(auth0Middleware(), async (req, res) => {
*/
router.route("/:projectId/label").patch(auth0Middleware(), screenContentMiddleware(), async (req, res) => {
const user = req.user
- if (!user?.agent) return respondWithError(res, 401, "Unauthenticated user")
+ if (!user?.agent) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
const projectId = req.params.projectId
if (!projectId) return respondWithError(res, 400, "Project ID is required")
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
const { label } = req.body
if (typeof label !== "string" || !label?.trim()) return respondWithError(res, 400, "JSON with a 'label' property required in the request body. It cannot be null or blank and must be a string.")
try {
let project = new Project(projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.METADATA, ENTITIES.PROJECT))) {
+ return respondWithError(res, 403, "You do not have permission to update this project's label")
+ }
const loadedProject = await project.loadProject()
if (!loadedProject) return respondWithError(res, 404, "Project not found")
project = await project.setLabel(label)
res.status(200).json(project)
} catch (error) {
- respondWithError(
+ return respondWithError(
res,
- error.status ?? error.code ?? 500,
+ error.status ?? 500,
error.message ?? "Unknown server error"
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use PATCH instead")
+ return respondWithError(res, 405, "Improper request method. Use PATCH instead")
})
export default router
diff --git a/project/projectReadRouter.js b/project/projectReadRouter.js
index 8227b62b..e82a7d06 100644
--- a/project/projectReadRouter.js
+++ b/project/projectReadRouter.js
@@ -92,22 +92,53 @@ 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 || !Array.isArray(userRoleNames)) return false
+ const rolePermissions = projectData.roles ?? {}
+ return userRoleNames.some(role => {
+ 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 === "*") &&
+ (permScope === scope || permScope === "*") &&
+ (permEntity === entity || permEntity === "*")
+ })
+ })
+}
+
router.route("/:id/manifest").get(auth0Middleware(), async (req, res) => {
const { id } = req.params
const user = req.user
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
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 = await ProjectFactory.loadAsUser(id, null)
- const collaboratorIdList = Object.keys(project.collaborators)
- if (!collaboratorIdList.includes(user._id)) {
+ 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")
}
- if (!await new Project(id).checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.PROJECT)) {
- return respondWithError(res, 403, "You do not have permission to export this 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) {
@@ -118,14 +149,20 @@ router.route("/:id/manifest").get(auth0Middleware(), async (req, res) => {
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use GET instead")
+ return respondWithError(res, 405, "Improper request method. Use GET instead")
})
router.route("/:id/deploymentStatus").get(auth0Middleware(), async (req, res) => {
+ const user = req.user
const { id } = req.params
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
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's deployment status")
+ }
const { status } = await ProjectFactory.checkManifestUploadAndDeployment(id)
if (!status) {
return respondWithError(res, 404, `No deployment status found for project with ID '${id}'`)
@@ -142,23 +179,31 @@ router.route("/:id/deploymentStatus").get(auth0Middleware(), async (req, res) =>
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use GET instead")
+ return respondWithError(res, 405, "Improper request method. Use GET instead")
})
router.route("/:id").get(auth0Middleware(), async (req, res) => {
const user = req.user
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
let id = req.params.id
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 = await ProjectFactory.loadAsUser(id, user._id)
- if (!project) {
+ // loadAsUser() returns an Error object (not throws) on DB failure
+ const projectData = await ProjectFactory.loadAsUser(id, user._id)
+ 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)) {
+ 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)
- const filteredProject = filterProjectInterfaces(project, namespacesToInclude)
+ const filteredProject = filterProjectInterfaces(projectData, namespacesToInclude)
res.status(200).json(filteredProject)
} catch (error) {
@@ -169,7 +214,7 @@ router.route("/:id").get(auth0Middleware(), async (req, res) => {
)
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use GET instead")
+ return respondWithError(res, 405, "Improper request method. Use GET instead")
})
export default router
diff --git a/project/projectToolsRouter.js b/project/projectToolsRouter.js
index 6bf81ac0..2eb9a027 100644
--- a/project/projectToolsRouter.js
+++ b/project/projectToolsRouter.js
@@ -2,13 +2,21 @@ import express from "express"
import { respondWithError } from "../utilities/shared.js"
import validateURL from "../utilities/validateURL.js"
import Tools from "../classes/Tools/Tools.js"
+import auth0Middleware from "../auth/index.js"
+import Project from "../classes/Project/Project.js"
+import { ACTIONS, ENTITIES, SCOPES } from "./groups/permissions_parameters.js"
const router = express.Router({ mergeParams: true })
//Add Tool to Project
-router.route("/:projectId/tool").post(async (req, res) => {
- const { label, toolName, url, location, custom } = req?.body
- let { enabled, tagName } = custom
+router.route("/:projectId/tool").post(auth0Middleware(), async (req, res) => {
+ const user = req.user
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
+ const { label, toolName, url, location, custom } = req.body
+ let { enabled, tagName } = custom ?? {}
if (!label || !toolName || !location) {
return respondWithError(res, 400, "label, toolName, and location are required fields.")
}
@@ -17,6 +25,10 @@ router.route("/:projectId/tool").post(async (req, res) => {
if (!projectId || !validateURL(projectId)) {
return respondWithError(res, 400, "A valid project ID is required.")
}
+ const project = new Project(projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.TOOLS))) {
+ return respondWithError(res, 403, "You do not have permission to add tools to this project")
+ }
const tools = new Tools(projectId)
await tools.validateToolArray(req?.body)
if (await tools.checkIfToolExists(toolName)) {
@@ -42,9 +54,14 @@ router.route("/:projectId/tool").post(async (req, res) => {
res.status(200).json(addedTool)
} catch (error) {
console.error("Error adding tool:", error)
- respondWithError(res, error.status || 500, error.message || "An error occurred while adding the tool.")
+ return respondWithError(res, error.status || 500, error.message || "An error occurred while adding the tool.")
+ }
+}).delete(auth0Middleware(), async (req, res) => {
+ const user = req.user
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
}
-}).delete(async (req, res) => {
const { toolName } = req.body
if (!toolName) {
return respondWithError(res, 400, "toolName is a required field.")
@@ -57,6 +74,10 @@ router.route("/:projectId/tool").post(async (req, res) => {
if (!projectId || !validateURL(projectId)) {
return respondWithError(res, 400, "A valid project ID is required.")
}
+ const project = new Project(projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.DELETE, SCOPES.ALL, ENTITIES.TOOLS))) {
+ return respondWithError(res, 403, "You do not have permission to remove tools from this project")
+ }
const tools = new Tools(projectId)
if (!await tools.checkToolPattern(toolName)) {
return respondWithError(res, 400, "toolName must be in 'lowercase-with-hyphens' format.")
@@ -71,14 +92,19 @@ router.route("/:projectId/tool").post(async (req, res) => {
res.status(200).json(removedTool)
} catch (error) {
console.error("Error removing tool:", error)
- respondWithError(res, error.status || 500, error.message || "An error occurred while removing the tool.")
+ return respondWithError(res, error.status || 500, error.message || "An error occurred while removing the tool.")
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use POST to add a tool or DELETE to remove a tool.")
+ return respondWithError(res, 405, "Improper request method. Use POST to add a tool or DELETE to remove a tool.")
})
// Toggle Tool State in Project
-router.route("/:projectId/toggleTool").put(async (req, res) => {
+router.route("/:projectId/toggleTool").put(auth0Middleware(), async (req, res) => {
+ const user = req.user
+ if (!user) return respondWithError(res, 401, "Not authenticated. Please provide a valid, unexpired Bearer token")
+ if (!req.body || typeof req.body !== 'object') {
+ return respondWithError(res, 400, "Request body is required")
+ }
const { toolName } = req.body
if (!toolName) {
return respondWithError(res, 400, "toolName is a required field.")
@@ -91,6 +117,10 @@ router.route("/:projectId/toggleTool").put(async (req, res) => {
if (!projectId || !validateURL(projectId)) {
return respondWithError(res, 400, "A valid project ID is required.")
}
+ const project = new Project(projectId)
+ if (!(await project.checkUserAccess(user._id, ACTIONS.UPDATE, SCOPES.ALL, ENTITIES.TOOLS))) {
+ return respondWithError(res, 403, "You do not have permission to toggle tools in this project")
+ }
const tools = new Tools(projectId)
if (!await tools.checkToolPattern(toolName)) {
return respondWithError(res, 400, "toolName must be in 'lowercase-with-hyphens' format.")
@@ -102,10 +132,10 @@ router.route("/:projectId/toggleTool").put(async (req, res) => {
res.status(200).json(toggledTool)
} catch (error) {
console.error("Error toggling tool state:", error)
- respondWithError(res, error.status || 500, error.message || "An error occurred while toggling the tool state.")
+ return respondWithError(res, error.status || 500, error.message || "An error occurred while toggling the tool state.")
}
}).all((_, res) => {
- respondWithError(res, 405, "Improper request method. Use PUT instead")
+ return respondWithError(res, 405, "Improper request method. Use PUT instead")
})
export default router
diff --git a/public/API.html b/public/API.html
index 3b2b4d40..1c0ea621 100644
--- a/public/API.html
+++ b/public/API.html
@@ -686,7 +686,7 @@ GET /project/:projectId/page/:pageid