diff --git a/package.json b/package.json new file mode 100644 index 0000000000..11eff3bfa3 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "code-challenge", + "version": "1.0.0", + "description": "Note that if you fork this repository, your responses may be publicly linked to this repo. Please submit your application along with the solutions attached or linked.", + "homepage": "https://github.com/thphucle/code-challenge#readme", + "bugs": { + "url": "https://github.com/thphucle/code-challenge/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/thphucle/code-challenge.git" + }, + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/readme.md b/readme.md index 1ff4bc95b4..2f34808113 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,73 @@ # 99Tech Code Challenge #1 # -Note that if you fork this repository, your responses may be publicly linked to this repo. -Please submit your application along with the solutions attached or linked. +# Problem 4: Three Ways to Sum to n -It is important that you minimally attempt the problems, even if you do not arrive at a working solution. +**Location**: `src/problem4/` -## Submission ## -You can either provide a link to an online repository, attach the solution in your application, or whichever method you prefer. -We're cool as long as we can view your solution without any pain. +This problem implements three unique approaches to calculate the summation of integers from 1 to n: + +- **Implementation A**: Recursive approach +- **Implementation B**: Iterative/Loop approach +- **Implementation C**: Mathematical formula approach + +--- + +# Problem 5: A Crude Server + +**Location**: `src/problem5/` + +An Express backend with TypeScript providing RESTful CRUD operations for resource management using SQLite. + +**Features**: + +- CRUD operations (Create, Read, Update, Delete) +- Full TypeScript type safety +- SQLite database for persistence +- Filtering & search capabilities +- Pagination support (limit and offset) +- Comprehensive error handling +- CORS support + +**Project Structure**: + +``` +src/ +├── server.ts # Main Express server entry point +├── controllers/ # Business logic for CRUD operations +├── database/ # SQLite database configuration +├── middlewares/ # Error handling middleware +├── repositories/ # SQL command execution layer +├── routes/ # API route definitions +└── types/ # TypeScript type definitions +``` + +**How to run**: + +1. Navigate to `src/problem5/` +2. Install dependencies: `npm install` +3. Configure environment variables (see README.md for details) +4. Build TypeScript: `npm run build` (if applicable) +5. Start the server: `npm start` +6. Test API endpoints with your preferred HTTP client (Postman, curl, etc.) + +**API Endpoints**: + +- `GET /resource` - Get all resources with optional filtering and pagination +- `POST /resource` - Create a new resource +- `GET /resource/:id` - Get a specific resource +- `PUT /resource/:id` - Update a resource +- `DELETE /resource/:id` - Delete a resource + +--- + +# Problem 6: Scoreboard API Service - Architecture + +**Location**: `src/problem6/` + +A specification about backend service designed to manage user scores and provide real-time leaderboard updates using Redis and PostgreSQL. + +**Core Components**: + +- `LeaderboardService.ts` - Manages leaderboard operations +- `RedisClient.ts` - Redis connection +- `README.md` - The final specification diff --git a/src/problem2/index.html b/src/problem2/index.html deleted file mode 100644 index 4058a68bff..0000000000 --- a/src/problem2/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Fancy Form - - - - - - - - -
-
Swap
- - - - - - - -
- - - - diff --git a/src/problem2/script.js b/src/problem2/script.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem2/style.css b/src/problem2/style.css deleted file mode 100644 index 915af91c72..0000000000 --- a/src/problem2/style.css +++ /dev/null @@ -1,8 +0,0 @@ -body { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - min-width: 360px; - font-family: Arial, Helvetica, sans-serif; -} diff --git a/src/problem3/.keep b/src/problem3/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem4/README.md b/src/problem4/README.md new file mode 100644 index 0000000000..f149d43bf0 --- /dev/null +++ b/src/problem4/README.md @@ -0,0 +1,123 @@ +# Problem 4: Three Ways to Sum to n + +## Problem Description + +Provide 3 unique implementations of a function that calculates the summation of all integers from 1 to n. + +### Specifications + +- **Input**: `n` - any integer +- **Output**: summation to n (e.g., `sum_to_n(5) === 1 + 2 + 3 + 4 + 5 === 15`) +- **Assumptions**: + - Result will always be less than `Number.MAX_SAFE_INTEGER` + - Negative integers should return 0 + - Non-integer inputs should be truncated to the nearest lower integer + +--- + +## Implementation A: Recursion + +```typescript +function sum_to_n_a(n: number): number { + if (n < 0) + return 0; + + n = Math.trunc(n); + + if (n <= 1) + return n; + else + return n + sum_to_n_a(n - 1); +} +``` + +### Complexity Analysis + +| Metric | Value | +|-------- |-------| +| **Time Complexity** | O(n) | +| **Space Complexity** | O(n) | + +### Efficiency Comment + +Least efficient due to function call overhead and risk of stack overflow for large n values. However, it is elegant and easy to understand. Not recommended for production use with large inputs. + +--- + +## Implementation B: Loop (Iterative) + +```typescript +function sum_to_n_b(n: number): number { + if (n < 0) + return 0; + + n = Math.trunc(n); + + let sum: number = 0; + + for (let i = 1; i <= n; i++) + sum += i; + + return sum; +} +``` + +### Complexity Analysis + +| Metric | Value | +|-------- |-------| +| **Time Complexity** | O(n) | +| **Space Complexity** | O(1) | + +### Efficiency Comment + +More efficient than recursion. No stack overflow risk. Provides a good balance between readability and performance for moderate values of n. Suitable for general use cases. + +--- + +## Implementation C: Mathematical Formula + +```typescript +function sum_to_n_c(n: number): number { + if (n < 0) + return 0; + + n = Math.trunc(n); + + return (n * (n + 1)) / 2; +} +``` + +### Complexity Analysis + +| Metric | Value | +|-------- |-------| +| **Time Complexity** | O(1) | +| **Space Complexity** | O(1) | + +### Efficiency Comment + +**Most efficient**. Uses the mathematical formula for arithmetic series: n(n+1)/2. Delivers instant results regardless of input size with no loops or recursion. Recommended for production use. + +--- + +## Summary Comparison + +| Method | Time | Space | Best For | +|-------- |------|------- |---------- | +| **Recursion** | O(n) | O(n) | Educational purposes, small n values | +| **Loop** | O(n) | O(1) | Balanced approach, moderate n values | +| **Formula** | O(1) | O(1) | **Production code, optimal solution** | + +--- + +## Example Usage + +```typescript +console.log(sum_to_n_a(5)); // Output: 15 +console.log(sum_to_n_b(5)); // Output: 15 +console.log(sum_to_n_c(5)); // Output: 15 + +console.log(sum_to_n_a(-3)); // Output: 0 +console.log(sum_to_n_b(3.7)); // Output: 6 (truncates to 3) +``` diff --git a/src/problem5/.gitignore b/src/problem5/.gitignore new file mode 100644 index 0000000000..419f8b3e29 --- /dev/null +++ b/src/problem5/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.log +.env +.DS_Store +resources.db diff --git a/src/problem5/README.md b/src/problem5/README.md new file mode 100644 index 0000000000..527c958a95 --- /dev/null +++ b/src/problem5/README.md @@ -0,0 +1,409 @@ +# Express CRUD Backend Server + +A Express backend with TypeScript that provides RESTful CRUD operations for resource management. +The server uses SQLite for data persistence. + +## Features + +- **CRUD Operations**: Create, Read, Update, Delete resources +- **TypeScript**: Full type safety with TypeScript +- **SQLite Database**: Persistent data storage +- **Filtering & Search**: Filter resources by category, status, and search by name/description +- **Pagination**: Limit and offset support for listing resources +- **Error Handling**: Comprehensive error handling and validation +- **CORS Support**: Cross-Origin Resource Sharing enabled +- **RESTful API**: Standard REST conventions + +## Project Structure + +``` graph +problem5/ +├── src/ +│ ├── server.ts # Main Express server +│ ├── controllers/ +│ │ └── resourceController.ts # CRUD operations logic +│ ├── database/ +│ │ └── index.ts # SQLite database configuration +│ ├── middlewares/ +│ │ └── errorHandler.ts # Error handling middleware +│ ├── repositories/ +│ │ └── resourceRepository.ts # Repositories to run sql command +│ ├── routes/ +│ │ └── resourceRoutes.ts # API route definitions +│ └── types/ +│ └── index.ts # TypeScript type definitions +├── package.json +├── tsconfig.json +├── .gitignore +└── README.md +``` + +## Prerequisites + +- Node.js (v14 or higher) +- npm or yarn + +## Configuration + +Set environment variables by creating a `.env` file or exporting them before running the server. + +The server uses environment variables for configuration: + +- `PORT`: Server port (default: `3000`) + +## Installation + +1. Navigate to the problem5 folder: + + ```bash + cd src/problem5 + ``` + +2. Install dependencies: + + ```bash + npm install + ``` + +## Building & Running + +### Development Mode + +Start the server with hot-reload using ts-node: + +```bash +npm run dev +``` + +This will start the server at `http://localhost:3000`. + +### Production Mode + +First, build the TypeScript to JavaScript: + +```bash +npm run build +``` + +Then start the compiled server: + +```bash +npm start +``` + +## API Endpoints + +### Base URL + +```url +http://localhost:3000/api/resource +``` + +### 1. Create a Resource + +**Endpoint**: `POST /api/resource` + +**Request Body**: + +```json +{ + "name": "Resource Name", + "description": "Resource Description", + "category": "Category Name", + "status": "active" +} +``` + +**Response** (201 Created): + +```json +{ + "success": true, + "data": { + "data": { + "id": 1, + "name": "Resource Name", + "description": "Resource Description", + "category": "Category Name", + "status": "active", + "createdAt": "2024-05-28T10:00:00.000Z", + "updatedAt": "2024-05-28T10:00:00.000Z" + } + }, + "message": "Resource created successfully" +} +``` + +### 2. List Resources with Filters + +**Endpoint**: `GET /api/resource` + +**Query Parameters**: + +- `category` (optional): Filter by category +- `status` (optional): Filter by status (`active` or `inactive`) +- `search` (optional): Search in name and description +- `limit` (optional, default: 10): Number of results per page +- `offset` (optional, default: 0): Pagination offset + +**Example**: + +```url +GET /api/resource?category=electronics&status=active&search=laptop&limit=20&offset=0 +``` + +**Response** (200 OK): + +```json +{ + "success": true, + "data": { + "total": 1, + "data": [ + { + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "category": "electronics", + "status": "active", + "createdAt": "2024-05-28T10:00:00.000Z", + "updatedAt": "2024-05-28T10:00:00.000Z" + } + ] + } + "message": "Retrieved 1 resources" +} +``` + +### 3. Get Resource Details + +**Endpoint**: `GET /api/resource/:id` + +**Response** (200 OK): + +```json +{ + "success": true, + "data": { + "data": { + "id": 1, + "name": "Resource Name", + "description": "Resource Description", + "category": "Category Name", + "status": "active", + "createdAt": "2024-05-28T10:00:00.000Z", + "updatedAt": "2024-05-28T10:00:00.000Z" + } + }, + "message": "Resource retrieved successfully" +} +``` + +**Error Response** (404 Not Found): + +```json +{ + "success": false, + "error": "Resource not found" +} +``` + +### 4. Update Resource + +**Endpoint**: `PUT /api/resource/:id` + +**Request Body** (all fields optional): + +```json +{ + "name": "Updated Name", + "description": "Updated Description", + "category": "Updated Category", + "status": "inactive" +} +``` + +**Response** (200 OK): + +```json +{ + "success": true, + "data": { + "data": { + "id": 1, + "name": "Updated Name", + "description": "Updated Description", + "category": "Updated Category", + "status": "inactive", + "createdAt": "2024-05-28T10:00:00.000Z", + "updatedAt": "2024-05-28T10:30:00.000Z" + } + }, + "message": "Resource updated successfully" +} +``` + +### 5. Delete Resource + +**Endpoint**: `DELETE /api/resource/:id` + +**Response** (200 OK): + +```json +{ + "success": true, + "message": "Resource deleted successfully" +} +``` + +**Error Response** (404 Not Found): + +```json +{ + "success": false, + "error": "Resource not found" +} +``` + +### Health Check + +**Endpoint**: `GET /health` + +**Response** (200 OK): + +```json +{ + "success": true, + "message": "Server is running", + "timestamp": "2024-05-28T10:00:00.000Z" +} +``` + +## Testing the API + +### Using cURL + +**Create a resource**: + +```bash +curl -X POST http://localhost:3000/api/resource \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Laptop", + "description": "High-performance laptop", + "category": "electronics", + "status": "active" + }' +``` + +**List all resources**: + +```bash +curl http://localhost:3000/api/resource +``` + +**Filter resources by category**: + +```bash +curl "http://localhost:3000/api/resource?category=electronics" +``` + +**Search resources**: + +```bash +curl "http://localhost:3000/api/resource?search=laptop" +``` + +**Get a specific resource**: + +```bash +curl http://localhost:3000/api/resource/1 +``` + +**Update a resource**: + +```bash +curl -X PUT http://localhost:3000/api/resource/1 \ + -H "Content-Type: application/json" \ + -d '{ + "status": "inactive" + }' +``` + +**Delete a resource**: + +```bash +curl -X DELETE http://localhost:3000/api/resource/1 +``` + +### Using Postman + +#### Create a Postman collection by below steps + +1. Import the endpoints listed above into Postman +2. Set the request type (GET, POST, PUT, DELETE) +3. Add appropriate headers (Content-Type: application/json for POST/PUT) +4. Add request body where needed +5. Send the request and view the response + +#### Import from my example Postman collection + +```url +https://speeding-trinity-9694.postman.co/workspace/My-Workspace~8a74d211-49df-43f3-808f-d866d7ad7556/folder/4360852-a4714a13-ed3d-4766-b44e-236f481dcd92?action=share&creator=4360852&ctx=documentation&active-environment=4360852-c5ccdcc2-640a-4d8f-a662-0640f14b1e60 +``` + +## Error Handling + +The API returns appropriate HTTP status codes: + +- `200 OK`: Successful GET, PUT, DELETE +- `201 Created`: Successful POST +- `400 Bad Request`: Invalid input or missing required fields +- `404 Not Found`: Resource doesn't exist +- `500 Internal Server Error`: Server error + +All error responses follow this format, includes error code above: + +```json +{ + "success": false, + "error": "Error message describing the issue" +} +``` + +## Database + +The server uses SQLite with the following schema: + +**resources table**: + +```sql +CREATE TABLE resources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + category TEXT, + status TEXT DEFAULT 'active', + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP +) +``` + +The database file (`resources.db`) is automatically created on the first run in the project root. + +## Troubleshooting + +### Port already in use + +If port 3000 is already in use, set a different PORT: + +```bash +PORT=3001 npm run dev +``` + +### Database locked + +If you get a database locked error: + +1. Ensure only one server instance is running +2. Delete `resources.db` and restart (this will clear all data) diff --git a/src/problem5/package-lock.json b/src/problem5/package-lock.json new file mode 100644 index 0000000000..2d73e7f446 --- /dev/null +++ b/src/problem5/package-lock.json @@ -0,0 +1,2683 @@ +{ + "name": "express-crud-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "express-crud-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express": "^4.18.2", + "sqlite3": "^5.1.6" + }, + "devDependencies": { + "@types/cors": "^2.8.13", + "@types/express": "^4.17.17", + "@types/node": "^20.3.1", + "@types/sqlite3": "^3.1.8", + "ts-node": "^10.9.1", + "typescript": "^5.1.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sqlite3": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.11.tgz", + "integrity": "sha512-KYF+QgxAnnAh7DWPdNDroxkDI3/MspH1NMx6m/N/6fT1G6+jvsw4/ZePt8R8cr7ta58aboeTfYFBDxTJ5yv15w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "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", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar 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", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/src/problem5/package.json b/src/problem5/package.json new file mode 100644 index 0000000000..10a9715d5f --- /dev/null +++ b/src/problem5/package.json @@ -0,0 +1,29 @@ +{ + "name": "problem5", + "version": "1.0.0", + "description": "Express backend with CRUD operations using TypeScript", + "main": "dist/server.js", + "scripts": { + "start": "node dist/server.js", + "dev": "ts-node src/server.ts", + "build": "tsc", + "clean": "rm -rf dist" + }, + "keywords": ["express", "crud", "typescript"], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.18.2", + "sqlite3": "^5.1.6", + "cors": "^2.8.5", + "body-parser": "^1.20.2" + }, + "devDependencies": { + "@types/express": "^4.17.17", + "@types/node": "^20.3.1", + "@types/cors": "^2.8.13", + "typescript": "^5.1.3", + "ts-node": "^10.9.1", + "@types/sqlite3": "^3.1.8" + } +} diff --git a/src/problem5/src/controllers/resourceController.ts b/src/problem5/src/controllers/resourceController.ts new file mode 100644 index 0000000000..390845e974 --- /dev/null +++ b/src/problem5/src/controllers/resourceController.ts @@ -0,0 +1,236 @@ +import { Request, Response } from 'express'; +import * as resourceRepository from '../repositories/resourceRepository'; +import { Resource, CreateResourceRequest, UpdateResourceRequest, ResourceFilter, ApiResponse } from '../types'; + +// Create +export async function createResource(req: Request, res: Response): Promise { + try { + const { name, description, category, status = 'active' } = req.body as CreateResourceRequest; + + if (!name || !name.trim()) { + res.status(400).json({ + success: false, + error: 'Name is required and cannot be empty', + } as ApiResponse); + return; + } + const result = await resourceRepository.createResource(name.trim(), description || '', category || '', status); + + const resource = await resourceRepository.getResourceById(result.id); + + res.status(201).json({ + success: true, + data: { data: resource }, + message: 'Resource created successfully', + } as ApiResponse); + } catch (error) { + console.error('Error creating resource:', error); + res.status(500).json({ + success: false, + error: 'Internal server error', + } as ApiResponse); + } +} + +// List +export async function listResources(req: Request, res: Response): Promise { + try { + const { query, params, limit, offset } = prepareQueryParam(req); + const { data: resources, total } = await resourceRepository.listResources(query, params, limit, offset); + + res.json({ + success: true, + data: { + total, + data: resources, + }, + message: `Retrieved ${resources.length} resources`, + } as ApiResponse); + } catch (error) { + console.error('Error listing resources:', error); + res.status(500).json({ + success: false, + error: 'Internal server error', + } as ApiResponse); + } +} + +// Get by ID +export async function getResource(req: Request, res: Response): Promise { + try { + const { id } = req.params; + + const numId = parseInt(id, 10); + if (isNaN(numId)) { + res.status(400).json({ + success: false, + error: 'Invalid resource ID', + } as ApiResponse); + return; + } + + const resource = await resourceRepository.getResourceById(numId); + if (!resource) { + res.status(404).json({ + success: false, + error: 'Resource not found', + } as ApiResponse); + return; + } + + res.json({ + success: true, + data: { data: resource }, + message: 'Resource retrieved successfully', + } as ApiResponse); + } catch (error) { + console.error('Error getting resource:', error); + res.status(500).json({ + success: false, + error: 'Internal server error', + } as ApiResponse); + } +} + +// Update +export async function updateResource(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const updates = req.body as UpdateResourceRequest; + const { numId, success, error, errorCode } = await validateResource(id); + + if (!success) { + res.status(errorCode || 500).json({ + success: false, + error: error || 'Internal server error', + } as ApiResponse); + return; + } + + // Dynamic update query + const fields: string[] = []; + const params: any[] = []; + + if (updates.name !== undefined) { + if (!updates.name.trim()) { + res.status(400).json({ + success: false, + error: 'Name cannot be empty', + } as ApiResponse); + return; + } + fields.push('name = ?'); + params.push(updates.name.trim()); + } + + if (updates.description !== undefined) { + fields.push('description = ?'); + params.push(updates.description); + } + + if (updates.category !== undefined) { + fields.push('category = ?'); + params.push(updates.category); + } + + if (updates.status !== undefined) { + fields.push('status = ?'); + params.push(updates.status); + } + + if (fields.length === 0) { + res.status(400).json({ + success: false, + error: 'No fields to update', + } as ApiResponse); + return; + } + + fields.push('updatedAt = CURRENT_TIMESTAMP'); + + await resourceRepository.updateResource(numId, { fields, params }); + + const updated_resource = await resourceRepository.getResourceById(numId); + + res.json({ + success: true, + data: { data: updated_resource }, + message: 'Resource updated successfully', + } as ApiResponse); + } catch (error) { + console.error('Error updating resource:', error); + res.status(500).json({ + success: false, + error: 'Internal server error', + } as ApiResponse); + } +} + +// Delete +export async function deleteResource(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const { numId, success, error, errorCode } = await validateResource(id); + if (!success) { + res.status(errorCode || 500).json({ + success: false, + error: error || 'Internal server error', + } as ApiResponse); + return; + } + + await resourceRepository.deleteResource(numId); + + res.json({ + success: true, + message: 'Resource deleted successfully', + } as ApiResponse); + } catch (error) { + console.error('Error deleting resource:', error); + res.status(500).json({ + success: false, + error: 'Internal server error', + } as ApiResponse); + } +} + +// Prepare query parameters +function prepareQueryParam(req: Request) { + const { category, status, search, limit = 10, offset = 0 } = req.query as ResourceFilter; + + let query = 'SELECT * FROM resources WHERE 1=1'; + const params: any[] = []; + + if (category) { + query += ' AND category = ?'; + params.push(category); + } + + if (status) { + query += ' AND status = ?'; + params.push(status); + } + + if (search) { + query += ' AND (name LIKE ? OR description LIKE ?)'; + const search_term = `%${search}%`; + params.push(search_term, search_term); + } + + return { query, params, limit, offset }; +} + +// Validate resource existence +async function validateResource(id: string): Promise<{ numId: number; success: boolean; error?: string, errorCode?: number }> { + const numId = parseInt(id, 10); + if (isNaN(numId)) { + return { numId: NaN, success: false, error: 'Invalid resource ID', errorCode: 400 }; + } + + const existing = await resourceRepository.getResourceById(numId); + if (!existing) { + return { numId, success: false, error: 'Resource not found', errorCode: 404 }; + } + + return { numId, success: true }; +} \ No newline at end of file diff --git a/src/problem5/src/database/index.ts b/src/problem5/src/database/index.ts new file mode 100644 index 0000000000..70600aa783 --- /dev/null +++ b/src/problem5/src/database/index.ts @@ -0,0 +1,102 @@ +import sqlite3 from 'sqlite3'; +import path from 'path'; + +const DB_PATH = path.join(__dirname, '../../resources.db'); + +let db: sqlite3.Database; + +export function initializeDatabase(): Promise { + return new Promise((resolve, reject) => { + db = new sqlite3.Database(DB_PATH, (err) => { + if (err) { + reject(err); + } + console.log('Connected to SQLite database'); + createTables() + .then(() => resolve()) + .catch(reject); + }); + }); +} + +function createTables(): Promise { + return new Promise((resolve, reject) => { + db.serialize(() => { + db.run(` + CREATE TABLE IF NOT EXISTS resources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + category TEXT, + status TEXT DEFAULT 'active', + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `, + function (err){ + if (err) { + reject(err); + } + console.log('Resources table created or already exists'); + resolve(); + } + ); + }); + }); +} + +export function getDatabase(): sqlite3.Database { + if (!db) { + throw new Error('Database not initialized. Call initializeDatabase() first.'); + } + return db; +} + +export function run(sql: string, params: any[] = []): Promise<{ id: number; changes: number }> { + return new Promise((resolve, reject) => { + db.run(sql, params, function (err) { + if (err) { + reject(err); + } + resolve({ id: this.lastID, changes: this.changes }); + }); + }); +} + +export function get(sql: string, params: any[] = []): Promise { + return new Promise((resolve, reject) => { + db.get(sql, params, (err, row) => { + if (err) { + reject(err); + } + resolve(row); + }); + }); +} + +export function all(sql: string, params: any[] = []): Promise { + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err) { + reject(err); + } + resolve(rows || []); + }); + }); +} + +export function closeDatabase(): Promise { + return new Promise((resolve, reject) => { + if (db) { + db.close((err) => { + if (err) { + reject(err); + } + console.log('Database connection closed'); + resolve(); + }); + } else { + resolve(); + } + }); +} diff --git a/src/problem5/src/middlewares/errorHandler.ts b/src/problem5/src/middlewares/errorHandler.ts new file mode 100644 index 0000000000..bc20d9cee8 --- /dev/null +++ b/src/problem5/src/middlewares/errorHandler.ts @@ -0,0 +1,20 @@ +import { Request, Response, NextFunction } from 'express'; + +export function errorHandler(err: any, req: Request, res: Response, next: NextFunction): void { + console.error('Error:', err); + + const statusCode = err.statusCode || 500; + const message = err.message || 'Internal server error'; + + res.status(statusCode).json({ + success: false, + error: message, + }); +} + +export function notFoundHandler(req: Request, res: Response): void { + res.status(404).json({ + success: false, + error: 'Route not found', + }); +} diff --git a/src/problem5/src/repositories/resourceRepository.ts b/src/problem5/src/repositories/resourceRepository.ts new file mode 100644 index 0000000000..794d424353 --- /dev/null +++ b/src/problem5/src/repositories/resourceRepository.ts @@ -0,0 +1,43 @@ +import * as db from '../database'; +import { Resource } from '../types'; + +export async function createResource(name: string, description?: string, category?: string, status?: string): Promise<{ id: number }> { + const result = await db.run(` + INSERT INTO resources (name, description, category, status) + VALUES (?, ?, ?, ?); + `, + [name, description, category, status] + ); + return { id: result.id }; +} + +export async function listResources(query: string, params: any[], limit: number, offset: number): Promise<{ data: Resource[]; total: number }> { + const count_result = await db.get( + query.replace('SELECT *', 'SELECT COUNT(*) as count'), + params + ); + const total = count_result?.count || 0; + + query += ' ORDER BY createdAt DESC LIMIT ? OFFSET ?'; + + params.push(limit, offset); + + const data = await db.all(query, params); + + return { data, total }; +} + +export async function getResourceById(id: number): Promise { + const resource = await db.get('SELECT * FROM resources WHERE id = ?', [id]); + return resource; +} + +export async function updateResource(id: number, updates: { fields: string[], params: any[] }): Promise { + updates.params.push(id); + const query = `UPDATE resources SET ${updates.fields.join(', ')} WHERE id = ?`; + await db.run(query, updates.params); +} + +export async function deleteResource(id: number): Promise { + await db.run('DELETE FROM resources WHERE id = ?', [id]); +} diff --git a/src/problem5/src/routes/resourceRoutes.ts b/src/problem5/src/routes/resourceRoutes.ts new file mode 100644 index 0000000000..a28b3a8d7d --- /dev/null +++ b/src/problem5/src/routes/resourceRoutes.ts @@ -0,0 +1,13 @@ +import { Router } from 'express'; +import * as resourceController from '../controllers/resourceController'; + +const router = Router(); + +// CRUD Routes +router.post('/', resourceController.createResource); +router.get('/', resourceController.listResources); +router.get('/:id', resourceController.getResource); +router.put('/:id', resourceController.updateResource); +router.delete('/:id', resourceController.deleteResource); + +export default router; diff --git a/src/problem5/src/server.ts b/src/problem5/src/server.ts new file mode 100644 index 0000000000..41e5d0b9f2 --- /dev/null +++ b/src/problem5/src/server.ts @@ -0,0 +1,59 @@ +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import bodyParser from 'body-parser'; +import * as database from './database'; +import resourceRoutes from './routes/resourceRoutes'; +import { errorHandler, notFoundHandler } from './middlewares/errorHandler'; + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use((req: Request, res: Response, next: NextFunction) => { + console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); + next(); +}); + +// Health check +app.get('/health', (req: Request, res: Response) => { + res.json({ + success: true, + message: 'Server is running', + timestamp: new Date().toISOString(), + }); +}); + +// Routes +app.use('/api/resource', resourceRoutes); + +// Error handler +app.use(notFoundHandler); +app.use(errorHandler); + +// Start server +async function startServer(): Promise { + try { + await database.initializeDatabase(); + console.log('Database initialized successfully'); + + app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); + console.log(`Health check: http://localhost:${PORT}/health`); + console.log(`API base: http://localhost:${PORT}/api/resource`); + }); + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +} + +process.on('SIGINT', async () => { + console.log('Shutting down gracefully...'); + await database.closeDatabase(); + process.exit(0); +}); + +startServer(); diff --git a/src/problem5/src/types/index.ts b/src/problem5/src/types/index.ts new file mode 100644 index 0000000000..933ed88397 --- /dev/null +++ b/src/problem5/src/types/index.ts @@ -0,0 +1,41 @@ +export interface Resource { + id: number; + name: string; + description: string; + category: string; + status: 'active' | 'inactive'; + createdAt: string; + updatedAt: string; +} + +export interface CreateResourceRequest { + name: string; + description: string; + category: string; + status?: 'active' | 'inactive'; +} + +export interface UpdateResourceRequest { + name?: string; + description?: string; + category?: string; + status?: 'active' | 'inactive'; +} + +export interface ResourceFilter { + category?: string; + status?: 'active' | 'inactive'; + search?: string; + limit?: number; + offset?: number; +} + +export interface ApiResponse { + success: boolean; + data?: { + data: T; + total?: number; + }; + error?: string; + message?: string; +} diff --git a/src/problem5/tsconfig.json b/src/problem5/tsconfig.json new file mode 100644 index 0000000000..2e60854c7b --- /dev/null +++ b/src/problem5/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/problem6/.env b/src/problem6/.env new file mode 100644 index 0000000000..9203b95a8e --- /dev/null +++ b/src/problem6/.env @@ -0,0 +1,23 @@ +// API Configuration +API_PORT=3000 + +// Database Configuration (Audit Logs & User Accounts) +DATABASE_URL=postgresql://user:pass@localhost:5432/scoreboard + +// Redis Configuration (Leaderboard - Primary Database) +REDIS_URL=redis://localhost:6379 +LEADERBOARD_KEY=game:leaderboard +PLAYER_META_PREFIX=player:meta: +PLAYER_META_TTL_SECONDS=2592000 // 30 days + +// JWT Configuration +JWT_SECRET= +JWT_EXPIRY=3600 // 1 hour in seconds + +// Rate Limiting Configuration +RATE_LIMIT_PER_MINUTE=100 +RATE_LIMIT_PER_IP=500 // global limit +RATE_LIMIT_WINDOW=60 // seconds + +// Duplicate Action Detection (Using Redis) +DUPLICATE_ACTION_WINDOW=300 // 5 minutes \ No newline at end of file diff --git a/src/problem1/.keep b/src/problem6/.keep similarity index 100% rename from src/problem1/.keep rename to src/problem6/.keep diff --git a/src/problem6/LeaderboardService.ts b/src/problem6/LeaderboardService.ts new file mode 100644 index 0000000000..a1884947e6 --- /dev/null +++ b/src/problem6/LeaderboardService.ts @@ -0,0 +1,204 @@ +import Redis from 'ioredis'; +import WebSocket from 'ws'; + +interface LeaderboardEntry { + rank: number; + username: string; + score: number; +} + +interface PlayerStats { + username: string; + rank: number; + score: number; +} + +interface PlayerMetadata { + [key: string]: string | number; +} + +class LeaderboardService { + private readonly leaderboardKey: string; + private readonly playerMetaPrefix: string; + private readonly playerMetaTtlSeconds: number; + private readonly redis: Redis; + private readonly wss: WebSocket.Server; + + constructor(redisCli: Redis, webSocketServer: WebSocket.Server) { + this.redis = redisCli; + this.wss = webSocketServer; + this.leaderboardKey = process.env.LEADERBOARD_KEY || "game:leaderboard"; + this.playerMetaPrefix = process.env.PLAYER_META_PREFIX || "player:meta:"; + this.playerMetaTtlSeconds = process.env.PLAYER_META_TTL_SECONDS || 60 * 60 * 24 * 30; + } + + private getMetaKey(username: string): string { + return `${this.playerMetaPrefix}${username}`; + } + + private toLeaderboardEntries( + rawEntries: (string | number)[], + startRank: number = 1 + ): LeaderboardEntry[] { + const entries: LeaderboardEntry[] = []; + + for (let i = 0; i < rawEntries.length; i += 2) { + entries.push({ + rank: startRank + i / 2, + username: rawEntries[i] as string, + score: Number.parseInt(rawEntries[i + 1] as string, 10), + }); + } + + return entries; + } + + private broadcastToClients(message: string): void { + this.wss.clients.forEach((client: any) => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); + } + }); + } + + private createBroadcastMessage(type: string, data: any): string { + return JSON.stringify({ + type, + data, + timestamp: Date.now(), + }); + } + + + async setScore(username: string, score: number): Promise { + await this.redis.zadd(this.leaderboardKey, score, username); + } + + async incrementScore(username: string, amount: number): Promise { + const newScore = await this.redis.zincrby( + this.leaderboardKey, + amount, + username + ); + await this.broadcastLeaderboard(); + await this.broadcastScoreChange(username, newScore); + } + + async removePlayer(username: string): Promise { + await this.redis.zrem(this.leaderboardKey, username); + } + + async resetLeaderboard(): Promise { + await this.redis.del(this.leaderboardKey); + } + + async getTopN(limit: number = 10): Promise { + const rawEntries = await this.redis.zrange( + this.leaderboardKey, + 0, + limit - 1, + "REV", + "WITHSCORES" + ); + + return this.toLeaderboardEntries(rawEntries, 1); + } + + async getRankRange( + fromRank: number, + toRank: number + ): Promise { + const rawEntries = await this.redis.zrange( + this.leaderboardKey, + fromRank - 1, + toRank - 1, + "REV", + "WITHSCORES" + ); + + return this.toLeaderboardEntries(rawEntries, fromRank); + } + + async getPlayerStats(username: string): Promise { + const score = await this.redis.zscore(this.leaderboardKey, username); + if (score === null) { + return null; + } + + let rank: number; + + try { + const descendingRank = await this.redis.call( + "ZRANK", + this.leaderboardKey, + username, + "REV" + ); + rank = (descendingRank as number) + 1; + } catch { + const [ascendingRank, totalPlayers] = await Promise.all([ + this.redis.zrank(this.leaderboardKey, username), + this.redis.zcard(this.leaderboardKey), + ]); + rank = (totalPlayers as number) - (ascendingRank as number); + } + + return { + username, + rank, + score: Number.parseInt(score as string, 10), + }; + } + + async getTotalPlayers(): Promise { + return this.redis.zcard(this.leaderboardKey) as Promise; + } + + async getByScoreRange( + minScore: number, + maxScore: number + ): Promise { + const rawEntries = await this.redis.zrange( + this.leaderboardKey, + maxScore, + minScore, + "BYSCORE", + "REV", + "WITHSCORES" + ); + + return this.toLeaderboardEntries(rawEntries, 1); + } + + async setPlayerMeta( + username: string, + metadata: PlayerMetadata + ): Promise { + if (!metadata || Object.keys(metadata).length === 0) { + return; + } + + const metaKey = this.getMetaKey(username); + await this.redis.hset(metaKey, metadata); + await this.redis.expire(metaKey, this.playerMetaTtlSeconds); + } + + async getPlayerMeta(username: string): Promise { + return this.redis.hgetall( + this.getMetaKey(username) + ) as Promise; + } + + async broadcastLeaderboard(): Promise { + const leaderboard = await this.getTopN(); + const message = this.createBroadcastMessage("LEADERBOARD_UPDATE", leaderboard); + this.broadcastToClients(message); + } + + async broadcastScoreChange(username: string, newScore: number): Promise { + const message = this.createBroadcastMessage("SCORE_CHANGED", { username, newScore }); + this.broadcastToClients(message); + } +} + +export { LeaderboardService, LeaderboardEntry, PlayerStats, PlayerMetadata }; diff --git a/src/problem6/README.md b/src/problem6/README.md new file mode 100644 index 0000000000..3c4431c014 --- /dev/null +++ b/src/problem6/README.md @@ -0,0 +1,880 @@ +# Scoreboard API Service - Specification + +## Overview + +The Scoreboard API Service is a backend module designed to manage user scores and provide real-time updates to a web-based scoreboard displaying the top 10 users. This specification outlines the architecture, API contracts, security measures, and implementation guidelines for the engineering team. + +### Key Features + +- Real-time score updates via WebSocket or polling +- Secure score increment with authorization checks +- Top 10 leaderboard retrieval +- Prevention of unauthorized score manipulation +- Audit logging for score changes +- Rate limiting and DDoS protection + +### Architecture Highlights + +- **Redis as Primary Leaderboard Database:** Sorted Sets (ZSET) provide O(log N) operations, in-memory performance, and atomic operations for score updates +- **PostgreSQL for Audit Logs:** Maintains full history of score changes for compliance and debugging +- **Hybrid Database Strategy:** Fast leaderboard operations in Redis + reliable audit trail in PostgreSQL +- **No Cache Invalidation:** Redis Sorted Sets serve as the single source of truth, eliminating cache coherency issues + +### Implementation Notes - Redis-Based Approach + +| Aspect | Traditional DB Approach | Redis Approach | +|--------|------------------------|-----------------| +| **Leaderboard Query** | `SELECT * FROM users ORDER BY score DESC LIMIT 10` (O(n log n)) | `ZRANGE game:leaderboard 0 9 REV WITHSCORES` (O(log n)) | +| **Score Update** | Transaction with read lock + update | `ZINCRBY game:leaderboard 10 username` (atomic) | +| **Response Time** | 50-200ms (disk I/O + sorting) | <5ms (in-memory) | +| **Rank Lookup** | Complex query or separate index | `ZRANK game:leaderboard username REV` (O(log n)) | +| **Cache Management** | Requires invalidation logic | N/A (single source of truth) | +| **Consistency Model** | Strong consistency | Strong consistency (atomic operations) | +| **Scalability** | Vertical (server hardware) | Horizontal (Redis cluster/replication) | + +### Why Redis Sorted Sets for Leaderboards? + +Redis Sorted Sets are purpose-built for ranking scenarios: + +- **Natural fit:** Members with scores, sorted by score automatically +- **Atomic operations:** No race conditions, no need for complex locking +- **Fast access patterns:** O(log n) for all operations (add, remove, rank, range) +- **Built-in scoring:** Numeric scores with comparison built-in +- **Perfect for real-time:** Sub-millisecond response times for any query + +--- + +## System Architecture + +### High-Level Components + +``` +┌─────────────────────────────────────────────────────────┐ +│ Client (Website) │ +│ (Scoreboard Display + UI) │ +└────────────────────────┬────────────────────────────────┘ + │ + ┌──────────────────────────────┐ + │ │ + HTTP WebSocket + GET /leaderboard /ws/leaderboard + PUT /score/update │ + │ │ + │ │ +┌────────▼──────────────────────────────▼──────────────────┐ +│ Scoreboard API Service │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Authentication & Authorization │ │ +│ │ (JWT Validation, User Identity Verification) │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Score Update Service │ │ +│ │ • Validate action completion │ │ +│ │ • Atomic score increment (ZINCRBY) │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Leaderboard Service │ │ +│ │ • Fetch top N users (ZRANGE REV) │ │ +│ │ • Get player rank (ZRANK) │ │ +│ │ • Real-time broadcast │ │ +│ │ • Player metadata (HSET/HGETALL) │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Audit & Logging Service │ │ +│ │ • Log score changes to PostgreSQL │ │ +│ │ • Track suspicious activities │ │ +│ │ • Compliance records │ │ +│ └─────────────────────────────────────────────────────┘ │ +└────────────────────┬─────────────────────────────────────┘ + │ + ┌───────────┴───────────┐ + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │PostgreSQL │ │Redis (Primary) │ + │Database │ │Leaderboard │ + │• User accounts │ │• Sorted Sets │ + │• Audit logs │ │• Player metadata│ + │• Metadata │ │• Real-time data │ + └─────────────────┘ └─────────────────┘ +``` + +--- + +## API Endpoints + +### 1. **Get Leaderboard (Top 10)** + +**Endpoint:** `GET /api/leaderboard` + +**Description:** Retrieve the top 10 users with the highest scores. + +**Request:** + +```http +GET /api/leaderboard HTTP/1.1 +``` + +**Response (200 OK):** + +```json +{ + "success": true, + "data": { + "leaderboard": [ + { + "rank": 1, + "userId": 1, + "username": "alice", + "score": 9850, + "lastUpdated": "2026-05-28T14:35:22Z" + }, + { + "rank": 2, + "userId": 2, + "username": "bob", + "score": 9720, + "lastUpdated": "2026-05-28T14:30:10Z" + }, + { + "rank": 3, + "userId": 3, + "username": "charlie", + "score": 9500, + "lastUpdated": "2026-05-28T14:20:05Z" + } + // ... 7 more users (up to rank 10) + ], + "generatedAt": "2026-05-28T14:36:00Z" + } +} +``` + +--- + +### 2. **Update User Score** + +**Endpoint:** `POST /api/score/update` + +**Description:** Increment a user's score upon action completion. This endpoint requires authentication and validates the action before updating the score. + +**Authentication:** Bearer Token (JWT) + +**Request:** + +```http +POST /api/score/update HTTP/1.1 +Host: api.scoreboard.local +Content-Type: application/json +Authorization: Bearer + +{ + "userId": 1, + "actionId": "action_update_score", + "actionTimestamp": "2026-05-28T14:35:15Z", + "point": 10, + "deviceInfo": { + "ip": "192.168.1.100", + "userAgent": "Mozilla/5.0..." + } +} +``` + +**Response (200 OK):** + +```json +{ + "success": true, + "data": { + "userId": 1, + "newScore": 9850, + "scoreIncrement": 10, + "totalActionsCompleted": 985, + "leaderboardRank": 1, + "updatedAt": "2026-05-28T14:35:22Z" + } +} +``` + +**Response (400 Bad Request):** + +```json +{ + "success": false, + "error": "Invalid action ID or missing required fields", + "code": "INVALID_REQUEST" +} +``` + +**Response (401 Unauthorized):** + +```json +{ + "success": false, + "error": "Invalid or expired authentication token", + "code": "AUTH_FAILED" +} +``` + +**Response (403 Forbidden):** + +```json +{ + "success": false, + "error": "User ID in token does not match request user ID", + "code": "USER_MISMATCH" +} +``` + +**Response (409 Conflict):** + +```json +{ + "success": false, + "error": "Duplicate action submission detected", + "code": "DUPLICATE_ACTION" +} +``` + +**Response (429 Too Many Requests):** + +```json +{ + "success": false, + "error": "Rate limit exceeded. Maximum 100 requests per minute", + "code": "RATE_LIMIT_EXCEEDED" +} +``` + +--- + +### 3. **WebSocket Connection (Real-time Updates)** + +**Endpoint:** `WS /ws/leaderboard` + +**Description:** Establish a WebSocket connection to receive real-time leaderboard updates when scores change. + +**Connection Request:** + +```javascript +ws = new WebSocket('ws://api.scoreboard.local/ws/leaderboard?token='); + +ws.onopen = () => { + console.log('Connected to leaderboard updates'); +}; + +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + console.log('Leaderboard updated:', message); +}; +``` + +**Message Format (Leaderboard Update):** + +```json +{ + "type": "LEADERBOARD_UPDATE", + "timestamp": "2026-05-28T14:35:22Z", + "data": [ + { + "rank": 1, + "userId": 1, + "username": "alice", + "score": 9850 + }, + // ... more users + ] +} +``` + +**Message Format (Score Change):** + +```json +{ + "type": "SCORE_CHANGED", + "timestamp": "2026-05-28T14:35:22Z", + "data": { + "username": "alice", + "newScore": 9850, + } +} +``` + +--- + +## Database Schema + +### Redis Data Structures (Leaderboard - Primary Database) + +#### Sorted Set: Leaderboard Data + +``` +Key: game:leaderboard +Type: Sorted Set (ZSET) +Data: {username: score, username: score, ...} + +Example: + username: "alice" score: 9850 + username: "bob" score: 9720 + username: "charlie" score: 9500 + +Useful Commands: + - ZADD game:leaderboard // Add/update score + - ZINCRBY game:leaderboard // Increment score (atomic) + - ZRANGE game:leaderboard 0 9 REV WITHSCORES // Get top 10 + - ZRANK game:leaderboard REV // Get rank (1-indexed) + - ZSCORE game:leaderboard // Get user's score + - ZCARD game:leaderboard // Total players + - ZREM game:leaderboard // Remove player +``` + +#### Hash Set: Player Metadata (Optional) + +``` +Key: player:meta:{username} +Type: Hash (HSET) +Data: {field: value, field: value, ...} +TTL: 1 days (automatically expires) + +Example for username "alice": +Key: player:meta:alice +Fields: + - email: alice@example.com + - region: US-East + - joinDate: 2024-01-15 + +Useful Commands: + - HSET player:meta:alice field1 value1 field2 value2 + - HGETALL player:meta:alice // Get all metadata + - EXPIRE player:meta:alice 2592000 // Set TTL (30 days) + - HDEL player:meta:alice field1 // Remove field +``` + +### PostgreSQL Database (Audit & Account Management) + +#### Users Table + +```sql +CREATE TABLE users ( + id INT PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT true, + INDEX idx_username (username), + INDEX idx_created_at (created_at) +); +``` + +#### Actions Table + +```sql +CREATE TABLE actions ( + id INT PRIMARY KEY, + user_id INT NOT NULL, + action_type VARCHAR(100) NOT NULL, + action_data JSON, + score_increment INT DEFAULT 10, + completed_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + INDEX idx_user_id (user_id), + INDEX idx_completed_at (completed_at DESC), + UNIQUE KEY unique_action_per_user (id, user_id) +); +``` + +#### Score History Table (Audit Log) + +```sql +CREATE TABLE score_history ( + id INT PRIMARY KEY, + user_id INT NOT NULL, + action_id INT NOT NULL, + score_change INT NOT NULL, + new_score INT NOT NULL, + old_score INT NOT NULL, + username VARCHAR(255) NOT NULL, + change_reason VARCHAR(255), + ip_address VARCHAR(45), + user_agent VARCHAR(500), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (action_id) REFERENCES actions(id), + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at DESC), + INDEX idx_username (username) +); +``` + +### Why Redis for Leaderboard? + +**Performance Benefits:** + +- O(log N) operations for sorted set queries (vs O(n log n) for SQL sorting) +- In-memory storage = microsecond response times (vs milliseconds for disk I/O) +- Atomic operations prevent race conditions +- Natural data structure fit (sorted sets are perfect for leaderboards) +- No complex joins or transactions needed + +**Trade-offs:** + +- Redis data persists in memory (use RDB snapshots or AOF for durability) +- Limited to available RAM (but leaderboard is typically small data) +- Audit logs still stored in PostgreSQL for compliance/history + +--- + +## Security & Authorization + +### Authentication Flow + +1. **User Login:** User authenticates via login endpoint (not in this spec) and receives JWT token +2. **Token Content:** JWT includes: + - `userId` - Unique user identifier + - `username` - Username + - `iat` - Issued at timestamp + - `exp` - Expiration timestamp (recommend: 1 hour) + - `scope` - Permissions (e.g., "score:update") + +### Authorization Checks + +**For Score Update Endpoint:** + +``` +1. Validate JWT token + - Check signature using secret key + - Verify token has not expired + - Return 401 if invalid/expired + +2. Verify user identity + - Extract userId from token + - Compare with userId in request body + - Return 403 if mismatch + +3. Rate limiting check + - Check if user exceeded rate limit (e.g., 100 requests/minute) + - Return 429 if exceeded + +4. Action validation + - Verify action exists and belongs to this user + - Check action was not already processed + - Return 409 if duplicate action detected + +5. Timestamp validation + - Verify actionTimestamp is within acceptable window (e.g., ±5 minutes) + - Prevent replay attacks + - Return 400 if timestamp is invalid + +6. IP/Device validation (optional) + - Compare current IP with user's profile + - Flag suspicious activities for audit log + - Allow but log if different from usual pattern +``` + +### Security Measures + +1. **Input Validation** + - Validate all input parameters against expected types/formats + - Sanitize strings to prevent injection attacks + +2. **Rate Limiting** + - Per-user rate limiting: 100 requests per minute + - Per-IP rate limiting: 500 requests per minute + - Use Redis or in-memory store to track request counts + +3. **Duplicate Detection** + - Track recently processed actionIds in Redis (TTL: 5 minutes) + - Prevent duplicate score increments from retry requests + +4. **Audit Logging** + - Log all score changes with full context + - Store IP address, user agent, timestamp + - Flag suspicious patterns (e.g., rapid score increases) + +5. **HTTPS Only** + - All endpoints must use HTTPS/WSS (WebSocket Secure) + - Enforce HTTPS redirect for HTTP requests + +6. **CORS Configuration** + - Restrict CORS to trusted domains only + - Set appropriate headers: `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` + +--- + +## Execution Flow Diagrams + +### Score Update Flow + +```mermaid +sequenceDiagram + participant Client as Client (Web) + participant API as Score Update API + participant Auth as Auth Service + participant Redis as Redis (Leaderboard DB) + participant PostgreSQL as PostgreSQL (Audit Log) + participant WS as WebSocket Handler + + Client->>API: POST /api/score/update (with JWT) + + API->>Auth: Validate JWT token + alt Token Invalid/Expired + Auth-->>API: 401 Unauthorized + API-->>Client: 401 Response + else Token Valid + Auth-->>API: Token verified + + API->>PostgreSQL: Check action exists & not processed + alt Action Already Processed + PostgreSQL-->>API: Duplicate detected + API-->>Client: 409 Conflict + else Action Valid + PostgreSQL-->>API: Action retrieved + + API->>API: Calculate score increment (10 points) + + API->>Redis: ZINCRBY game:leaderboard 10 username + Redis-->>API: New score (atomic operation) + + API->>Redis: ZRANK game:leaderboard username REV + Redis-->>API: User's rank + + API->>PostgreSQL: Insert score history record (async/background) + + API->>WS: Broadcast leaderboard update to all clients + WS-->>Client: WebSocket message (LEADERBOARD_UPDATE) + + API-->>Client: 200 OK (new score + rank from Redis) + end + end +``` + +**Key Improvements with Redis:** + +- Score increment is atomic (no race conditions with ZINCRBY) +- Rank lookup is instant with ZRANK REV +- No cache invalidation needed +- Direct read from primary source (Redis Sorted Set) + +### Leaderboard Retrieval Flow + +```mermaid +sequenceDiagram + participant Client as Client (Web) + participant API as Leaderboard API + participant Redis as Redis (Primary DB) + + Client->>API: GET /api/leaderboard + + API->>Redis: ZRANGE game:leaderboard 0 9 REV WITHSCORES + + Redis-->>API: Top 10 users with scores (microseconds) + + API->>API: Format response + + API-->>Client: 200 OK (fresh data from Redis) +``` + +**Note:** No caching layer needed - Redis Sorted Sets are optimized for this exact use case and provide sub-millisecond response times. + +### WebSocket Real-time Update Flow + +```mermaid +sequenceDiagram + participant Client1 as Client 1 + participant Client2 as Client 2 + participant WSServer as WebSocket Server + participant API as Score Update API + participant Redis as Redis (Leaderboard DB) + + Client1->>WSServer: WS /ws/leaderboard (subscribe) + WSServer-->>Client1: Connected + Client2->>WSServer: WS /ws/leaderboard (subscribe) + WSServer-->>Client2: Connected + + Client1->>API: POST /api/score/update (user1 score +10) + + API->>Redis: ZINCRBY game:leaderboard 10 username + Redis-->>API: New score + + API->>Redis: ZRANGE game:leaderboard 0 9 REV WITHSCORES + Redis-->>API: Current top 10 data + + API->>WSServer: Broadcast LEADERBOARD_UPDATE + WSServer-->>Client1: LEADERBOARD_UPDATE message + Client1->>Client1: Update UI with new leaderboard + WSServer-->>Client2: LEADERBOARD_UPDATE message + Client2->>Client2: Update UI with new leaderboard + + API->>WSServer: Broadcast SCORE_CHANGED + WSServer-->>Client1: SCORE_CHANGED message + Client1->>Client1: Update UI with new score + +``` + +**Benefits:** + +- Single source of truth (Redis, not fragmented cache + DB) +- Consistent data across all clients +- No cache coherency issues +- Instant updates without synchronization delays + +--- + +## Implementation Guidelines + +### Technology Stack (Recommendations) + +- **Framework:** Node.js (Express) +- **Leaderboard Database:** Redis (Primary - Sorted Sets for leaderboard, Hashes for metadata) +- **Audit & Account Database:** PostgreSQL (ACID transactions for audit logs and user accounts) +- **Real-time Communication:** Socket.io or native WebSockets +- **Authentication:** JWT with RS256 algorithm (asymmetric signing) +- **Rate Limiting:** Redis (with sliding window algorithm) +- **Implementation Reference:** See [LeaderboardService.ts](LeaderboardService.ts), [RedisCLient.ts](RedisCLient.ts) for implementation example + +### Configuration + +```bash +// API Configuration +API_PORT=3000 + +// Database Configuration (Audit Logs & User Accounts) +DATABASE_URL=postgresql://user:pass@localhost:5432/scoreboard + +// Redis Configuration (Leaderboard - Primary Database) +REDIS_URL=redis://localhost:6379 +LEADERBOARD_KEY=game:leaderboard +PLAYER_META_PREFIX=player:meta: +PLAYER_META_TTL_SECONDS=2592000 // 30 days + +// JWT Configuration +JWT_SECRET= +JWT_EXPIRY=3600 // 1 hour in seconds + +// Rate Limiting Configuration +RATE_LIMIT_PER_MINUTE=100 +RATE_LIMIT_PER_IP=500 // global limit +RATE_LIMIT_WINDOW=60 // seconds + +// Duplicate Action Detection (Using Redis) +DUPLICATE_ACTION_WINDOW=300 // 5 minutes +``` + +### Error Handling + +All error responses must follow this format: + +```json +{ + "success": false, + "error": "Human-readable error message", + "code": "ERROR_CODE", + "timestamp": "2026-05-28T14:35:22Z" +} +``` + +### Logging + +Log all significant events: + +- User authentication (success/failure) +- Score updates (userId, amount, timestamp) +- Suspicious activities (duplicate actions, unusual patterns) +- API errors and exceptions +- Performance metrics (response times, cache hit rates) + +--- + +## Improvement Suggestions & Considerations + +### 1. **Score Decay / Time-Based Ranking** + +**Current Issue:** Older high scores remain at the top indefinitely. + +**Suggestion:** Implement a score decay mechanism where scores gradually decrease over time, encouraging continuous engagement. Or use a time-weighted leaderboard (top scores in last 7 days). + +``` +Formula: adjusted_score = score * decay_factor ^ (days_since_update) +where decay_factor = 0.95 (5% decay per day) +``` + +### 2. **Anti-Cheat Mechanisms** + +**Current Issue:** Specification assumes valid action completion, but doesn't validate actions on backend. + +**Suggestions:** + +- Implement server-side action validation +- Track action completion patterns and flag anomalies (e.g., 1000 actions in 1 minute) +- Use machine learning for fraud detection +- Implement CAPTCHA for users with suspicious patterns + +### 3. **Tiered Scoring System** + +**Current Issue:** All actions give fixed score increment (10 points). + +**Suggestion:** Implement difficulty-based scoring: + +``` +- Easy action: +5 points +- Medium action: +10 points +- Hard action: +25 points +- Rare achievement: +100 points +``` + +### 4. **Pagination for Leaderboard** + +**Current Issue:** Only returns top 10 users. + +**Suggestion:** + +- Add pagination support: `GET /api/leaderboard?page=1&limit=10` +- Add user's current rank and nearby users: `GET /api/leaderboard/around-me` + +### 5. **Redis Persistence & Durability** + +**Current Implementation:** Redis stores leaderboard in memory. + +**Recommendation:** + +- Enable Redis RDB snapshots: `BGSAVE` every 60 seconds +- Or enable AOF (Append-Only File) for write durability +- Or use Redis replication with a secondary replica +- Consider "redis:latest" with persistent volume for production + +```bash +// Redis configuration for durability +save 60 1000 // Snapshot every 60 sec if 1000 keys changed +appendonly yes // Enable AOF +appendfsync everysec // Fsync every second +``` + +### 6. **Multiple Leaderboards in Redis** + +**Suggestion:** Support multiple leaderboards using different keys: + +```javascript +// Global leaderboard +game:leaderboard:global + +// Season leaderboards +game:leaderboard:season:season-1 +game:leaderboard:season:season-2 + +// Team leaderboards +game:leaderboard:team:team-001 +game:leaderboard:team:team-002 + +// All commands work the same way with different keys +ZADD game:leaderboard:season:season-1 100 username +ZRANGE game:leaderboard:season:season-1 0 9 REV WITHSCORES +``` + +### 7. **Seasonal/Reset Mechanics** + +**Current Issue:** Leaderboard grows indefinitely without reset. + +**Suggestion:** + +- Reset leaderboard monthly/quarterly using separate Redis keys +- Archive previous season leaderboards to PostgreSQL +- Show "All-Time" vs "This Season" options + +```javascript +// Season management +game:leaderboard:2026-05 // Current season +game:leaderboard:2026-04 // Previous season (archived to PostgreSQL) + +// At season end: +// 1. Backup current leaderboard to PostgreSQL +// 2. Create new leaderboard key for new season +// 3. Keep old key for historical queries +``` + +### 8. **Data Consistency & Redis Replication** + +**Current Implementation:** Single Redis instance. + +**Recommendation for High Availability:** + +- Deploy Redis cluster with replication (primary + replicas) +- Use Sentinel for automatic failover +- Ensure RDB snapshots are replicated across nodes + +``` + ┌─────────────────┐ + │ Redis Primary │ + │ (Leaderboard) │ + └────────┬────────┘ + │ + ┌──────┴──────┐ + │ │ + ▼ ▼ +┌──────────┐ ┌──────────┐ +│ Replica 1│ │ Replica 2│ +└──────────┘ └──────────┘ +``` + +### 9. **Performance Optimization** + +**Suggestions (Redis-specific):** + +- Monitor Redis memory usage (`INFO memory`) +- Use Redis SCAN instead of KEYS for large datasets +- Implement connection pooling to Redis +- Use pipelining for batch operations to reduce network round trips +- Monitor Redis CPU usage and response times + +```javascript +// Pipelining example - reduces network round trips +const pipeline = redisClient.pipeline(); +for (let i = 0; i < 100; i++) { + pipeline.zadd('game:leaderboard', score[i], username[i]); +} +await pipeline.exec(); +``` + +### 10. **Testing Strategy** + +**Recommendations:** + +- Unit tests for leaderboard service (Redis operations) +- Integration tests for API endpoints with real Redis +- Load testing for concurrent score updates (ZINCRBY stress test) +- Security testing (SQL injection, XSS, CSRF, JWT validation) +- End-to-end tests for the full flow (Score update → WebSocket broadcast) +- Redis failover tests (test behavior when Redis is unavailable) + +### 11. **Monitoring & Alerting** + +**Suggestions:** + +- Monitor API response times (target: <100ms for 95th percentile with Redis) +- Alert on error rates exceeding threshold +- Monitor Redis memory usage (alert if >80% of max memory) +- Monitor Redis eviction policies +- Alert on suspicious score patterns (e.g., 100 score increases in 1 second) +- Track WebSocket connection counts and message volumes +- Monitor PostgreSQL audit log write latency + +--- + +## Summary + +This specification provides a robust, secure, and scalable foundation for the Scoreboard API Service using a hybrid architecture: + +**Redis Layer:** High-performance leaderboard operations (Sorted Sets) +**PostgreSQL Layer:** Reliable audit logs and user account management + +The implementation should prioritize: + +1. **Security:** Strong authentication, authorization, and input validation +2. **Performance:** Redis ZSET operations for instant leaderboard updates (<10ms) +3. **Reliability:** Audit logging to PostgreSQL for compliance and debugging +4. **Scalability:** Stateless API design with Redis replication for high availability +5. **Data Integrity:** PostgreSQL transactions for account management + Redis atomicity for scores + +Follow the API contracts closely to ensure client compatibility. Implement all security measures to prevent unauthorized score manipulation. diff --git a/src/problem6/RedisClient.ts b/src/problem6/RedisClient.ts new file mode 100644 index 0000000000..2024b7fc84 --- /dev/null +++ b/src/problem6/RedisClient.ts @@ -0,0 +1,104 @@ +import Redis from "ioredis"; + +class RedisClient { + private client: Redis; + private host: string; + private port: number; + + constructor() { + this.host = process.env.REDIS_HOST || "127.0.0.1"; + this.port = Number.parseInt(process.env.REDIS_PORT || "6379", 10); + + this.client = new Redis({ + host: this.host, + port: this.port, + lazyConnect: true, + retryStrategy: this.retryStrategy.bind(this), + }); + + this.setupEventHandlers(); + } + + private retryStrategy(retryCount: number): number | null { + if (retryCount > 5) { + console.error("Redis connection failed after 5 retries."); + console.error("Make sure your Redis server is running."); + return null; + } + + return Math.min(retryCount * 300, 3000); + } + + private setupEventHandlers(): void { + this.client.on("connect", () => { + console.log(`Redis connected at ${this.host}:${this.port}`); + }); + + this.client.on("error", (error) => { + console.error("Redis error:", error.message); + }); + } + + async connect(): Promise { + await this.client.connect(); + } + + async disconnect(): Promise { + await this.client.disconnect(); + } + + zadd(key: string, score: number, member: string): Promise { + return this.client.zadd(key, score, member); + } + + zrange( + key: string, + start: number, + stop: number, + ...args: string[] + ): Promise<(string | number)[]> { + return this.client.zrange(key, start, stop, ...args); + } + + zscore(key: string, member: string): Promise { + return this.client.zscore(key, member); + } + + zincrby(key: string, increment: number, member: string): Promise { + return this.client.zincrby(key, increment, member); + } + + zrem(key: string, member: string): Promise { + return this.client.zrem(key, member); + } + + zrank(key: string, member: string): Promise { + return this.client.zrank(key, member); + } + + zcard(key: string): Promise { + return this.client.zcard(key); + } + + del(key: string): Promise { + return this.client.del(key); + } + + hset(key: string, data: Record): Promise { + return this.client.hset(key, data); + } + + hgetall(key: string): Promise> { + return this.client.hgetall(key); + } + + expire(key: string, seconds: number): Promise { + return this.client.expire(key, seconds); + } + + call(command: string, ...args: (string | number)[]): Promise { + return this.client.call(command, ...args); + } +} + +export { RedisClient }; \ No newline at end of file