diff --git a/.circleci b/.circleci deleted file mode 100644 index 9b06d3c..0000000 --- a/.circleci +++ /dev/null @@ -1,39 +0,0 @@ -version: 2.1 -orbs: - node: circleci/node@1.1.6 -jobs: - build-and-test: - executor: - name: node/default - steps: - - checkout - - node/with-cache: - steps: - - run: npm install - deploy: - executor: - name: node/default - steps: - - checkout - - node/with-cache: - steps: - - run: sudo apt-get update && sudo apt-get install python-pip python-dev build-essential - - run: sudo pip install awscli - - run: aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile superhero-deploy - - run: aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile superhero-deploy - - run: aws configure set region $AWS_DEFAULT_REGION --profile superhero-deploy - - - run: npm install - - run: npm run deploy - -workflows: - build-and-test: - jobs: - - build-and-test - deploy-master: - jobs: - - deploy: - filters: - branches: - only: - - master diff --git a/.env-sample b/.env-sample deleted file mode 100644 index 6339c3a..0000000 --- a/.env-sample +++ /dev/null @@ -1 +0,0 @@ -MONGO_URL='mongodb://mlab2020:abc123def!@ds031617.mlab.com:31617/learningmongo' \ No newline at end of file diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..4c1bf72 --- /dev/null +++ b/.env.sample @@ -0,0 +1,36 @@ +# 📦 .env.sample +# Copy this file to .env and fill in the actual values +# Command: cp .env.sample .env + +# === MongoDB Configuration === + +# MongoDB connection URI for development/production environments. +# Format: mongodb://:@:/ +# Example: mongodb://user:pass@localhost:27017/mydatabase +MONGO_URL='mongodb://your_username:your_password@host:port/database_name' + +# Set the environment +# NODE_ENV can be 'development', 'production', or 'test' +# When set to `"test"`, a test database is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database. +NODE_ENV=test + +# === Notes === +# Do NOT use real credentials in this file. +# In production, make sure this file is excluded from version control. +# For test, the in-memory MongoDB server will be used automatically if NODE_ENV=test + +# === CORS Configuration === +# CORS (Cross-Origin Resource Sharing) settings +# By default, CORS is disabled. +# Default methods: GET, POST, PUT, DELETE +ALLOWED_ORIGINS= +ALLOWED_METHODS= +ALLOWED_HEADERS= + +# === JWT Configuration === +# JWT (JSON Web Token) secret key for signing tokens +# A cryptographically secure secret used to sign and verify JSON Web Tokens (JWTs). +# This is required for authentication to work correctly. +# 🔐 Use a long, random string—at least 32 characters, ideally generated using a password manager or Node.js: +# $ node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" +JWT_SECRET= \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..92f3f43 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +name: Backend Unit Tests + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + # Install dependencies (without using lock file) + - name: Install dependencies + run: npm install + + # Ensure consistent installs with npm ci + - name: Run npm ci (ensure clean node_modules) + run: npm ci + + - name: Run tests + run: npm test + env: + NODE_ENV: test + JWT_SECRET: ${{ secrets.JWT_SECRET }} diff --git a/.gitignore b/.gitignore index d8223fc..dd4b88e 100755 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ build/Release # Dependency directories node_modules/ jspm_packages/ +package-lock.json +yarn.lock # Typescript v1 declaration files typings/ @@ -67,3 +69,6 @@ typings/ .elasticbeanstalk/* !.elasticbeanstalk/*.cfg.yml !.elasticbeanstalk/*.global.yml + +# Test Coverage +coverage \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5c5bb8f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +node_modules +.github \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..204110e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "avoid" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5294c53 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "accessibility.signals.chatRequestSent": { + "sound": "off", + "announcement": "off" + } +} diff --git a/AWS.md b/AWS.md deleted file mode 100644 index 9f3612c..0000000 --- a/AWS.md +++ /dev/null @@ -1,25 +0,0 @@ -# AWS Deployment - -This file describes the process of deploying this repository to AWS. - -## Add Environment Variables on AWS for you environment "Configuration" - -In the aws web console, navigate to beanstalk environments. - -- Click on your environment - -- Click "Configuration" - -- Under the "Category" column is a row with the value of "Software" - -- On the right side of that row is and "Edit" button. Click it to get to the "Modify Software" page. - -- On the "Modify Software" page, at the bottom is "Environment properties" where we will add our name/value pair. - -- We will use `MONGO_URL` as the name and we will add our mongo url as the value. - -## AWS eb cli - -Mac `brew update && brew install awsebcli && eb --version` - -Windows diff --git a/HEROKU.md b/HEROKU.md deleted file mode 100644 index a06e5ba..0000000 --- a/HEROKU.md +++ /dev/null @@ -1,73 +0,0 @@ -# Deploy to Heroku - -Be sure you have the `heroku` cli installed. - -- Mac `brew tap heroku/brew && brew install heroku` - -- Use installer - -## Heroku Login - -`heroku login` is required to authenticate your machine to connect to your heroku account. - -## Heroku Commands - -Run a local heroku server using your code. - -`heroku local web` - -## Deploy to Heroku as Public Web App - -First observer what your git remotes are. - -`git remote -v` - -Then connect your repo with heroku using - -`heroku create` - -Observer that you now have a new remote after executing `heroku create` - -`git remote -v` - -Amazing! - -Push to the remote heroku - -`git push heroku` - -Open the remotely hosted app in your browser. - -`heroku open` - -Amazing! But we have one more command to run to configure the environment variable for MONGO. - -First confirm that the environment variable is not set: - -`heroku config:get MONGO_URL` - -Our mongo url which we have configured in the `.env` file will be the same url we use. - -To set the value on the server while keeping the setting private we will use the `heroku config` command. - -Note that the url value on the right side of this assignment must be enclosed in quotes. - -If you do not have access to your own mongo db url, for now you can use the shared mongo url that is included here for demonstration purposes only. - -`heroku config:set MONGO_URL='mongodb://mlab2020:abc123def!@ds031617.mlab.com:31617/learningmongo'` - -Heroku will restart your web server. - -In your browser, refresh the app web page or run the command to open the heroku-host web url for your rep. - -`heroku open` - -## Connect to Watch the Log file from the remote Heroku Server - -`heroku logs --tail` - -## In Case of Issues - -Be sure there is a process on heroku setup to run your server by running this command: - -`heroku ps:scale web=1` diff --git a/README.md b/README.md index 417953d..7ee8bcb 100755 --- a/README.md +++ b/README.md @@ -1,81 +1,54 @@ -# Node.js and Express Tutorial: Building and RESTful APIs - -## Requirements - -Identify your mongo db url used previously. Ask JR for a shared mongo db url if needed. - -We will configure the mongo db url on aws as well as in our local environment. - -## Objectives - -Be able to do the following: - -### AWS - -- understand changes required for production deployment to aws - -- update your week-10 storefront api for deployment to aws - -- deploy your updated repository to aws - -- add your mongo db url as an environment variable on aws - -### Heroku - -- understand heroku deployment - -- install the heroku cli - -- login to heroku from the cli - -- configure environment variables on heroku - -## Overview - -This readme outlines all of the changes included in this repository that allow aws deployment. - -After an overview, you will make changes to your week 10 repository and deploy it to aws. - -You will commit your changes to your week 10 repository and create a new PR. - -## AWS and Heroku Deployment - -New this week is AWS and Heroku deployment and the changes that were made to support the deployment process. - -Below we list out changes required from the original development-only version we previously built. - -### package.json - -- Changed the "start" script to use "node" in place of "nodemon". Nodemon is for development only. - -- Added a "dev" script to support using nodemon in development. - -- Added a "zip-for-aws" script to zip content for deployment to aws. - -- Installed `env-cmd` and `archiver` node modules - -`env-cmd` allows us to have a `.env` file in development to configure our MONGO db url safely where the setting is not shared in git. - -`archiver` supports a script to run to generate the `zip` file that aws requires for the aws web console upload. - -### index.js - -- Updated "port" settings to allow the production server to set the port value. - -- Added configuration check for MONGO_URL environment variable and start DB only when configured. - -### AWS and Heroku - -See AWS.md and HEROKU.md - -## Previously in the repo - -We built this repo in week 10 and then updated it for week 12 as an introduction to connecting an express node app to a mongo database. - -This app was configured originally only for development and required additional work this week for production readiness. - -Previous versions: - -- Week 10: - -- Week 12: +# Node.js and Express Backend + +[![Backend API - CI Tests](https://github.com/pakeku/backend-api/actions/workflows/tests.yml/badge.svg)](https://github.com/pakeku/backend-api/actions/workflows/tests.yml) +[![Known Vulnerabilities](https://snyk.io/test/github/pakeku/backend-api/badge.svg)](https://snyk.io/test/github/pakeku/backend-api) +[![code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat&logo=prettier)](https://prettier.io/) +[![ESLint](https://img.shields.io/badge/linting-eslint-blue.svg?style=flat&logo=eslint)](https://eslint.org/) +[![TypeScript](https://img.shields.io/badge/language-typescript-blue.svg?style=flat&logo=typescript)](https://www.typescriptlang.org/) +[![Swagger UI](https://img.shields.io/badge/docs-Swagger_UI-blue?logo=swagger)](http://localhost:3000/api-docs) + +## Configuration + +You can define your environmental variables in a `.env` file at the root of the project. (Start by copying `.env.sample` → `.env`).\ +**⚠️ Important:** Never commit your `.env` file to version control. + +| Variable | Required | Description | Example | +| ----------------- | ---------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `MONGO_URL` | ✅ Yes | Connection string for MongoDB. Obtain this from your MongoDB provider. | `mongodb+srv://:@/?retryWrites=true&w=majority` | +| `PORT` | ❌ No | Port for the Express server to listen on. Defaults to `3000`. | `8080` | +| `ALLOWED_ORIGINS` | ❌ No | Comma-separated list of allowed origins for CORS. | `http://localhost:3000,https://your-frontend.com` | +| `ALLOWED_METHODS` | ❌ No | Comma-separated list of allowed HTTP methods for CORS. | `GET,POST,PUT,DELETE` | +| `ALLOWED_HEADERS` | ❌ No | Comma-separated list of allowed request headers for CORS. | `Content-Type,Authorization` | +| `NODE_ENV` | ⚠️ Depends | Application environment: `development`, `production`, or `test`. `MONGO_URL` not required in `test`. | `development` | +| `JWT_SECRET` | ✅ Yes | Secret key used for signing/verifying JWTs. Must be secure and private. | `Use: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"` | + +## Getting Started + +1. Install Dependencies + +```bash +npm run install +``` + +2. Create .env file and gather your variable values. + +```bash +cp .env.sample .env +``` + +3. Run script: + +```json +"scripts": { + "prebuild":"rm -rf dist", + "build":"tsc", + "start": "node ./src/index.js", + "dev": "env-cmd nodemon ./src/index.ts", + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts --no-ignore", + "format": "prettier --write ." + } +``` diff --git a/aws-api-request.js b/aws-api-request.js deleted file mode 100644 index 8101169..0000000 --- a/aws-api-request.js +++ /dev/null @@ -1,178 +0,0 @@ -const crypto = require('crypto'), - https = require('https'), - zlib = require('zlib'); -const { encode } = require('punycode'); - -function awsApiRequest(options) { - return new Promise((resolve, reject) => { - let region = options.region || awsApiRequest.region || process.env.AWS_DEFAULT_REGION, - service = options.service, - accessKey = options.accessKey || awsApiRequest.accessKey || process.env.AWS_ACCESS_KEY_ID, - secretKey = options.secretKey || awsApiRequest.secretKey || process.env.AWS_SECRET_ACCESS_KEY, - sessionToken = options.sessionToken || awsApiRequest.sessionToken || process.env.AWS_SESSION_TOKEN, - method = options.method || 'GET', - path = options.path || '/', - querystring = options.querystring || {}, - payload = options.payload || '', - host = options.host || `${service}.${region}.amazonaws.com`, - headers = options.headers || {}; - - function hmacSha256(data, key, hex=false) { - return crypto.createHmac('sha256', key).update(data).digest(hex ? 'hex' : undefined); - } - - function sha256(data) { - return crypto.createHash('sha256').update(data).digest('hex'); - } - - //Thanks to https://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-javascript - function createSigningKey(secretKey, dateStamp, region, serviceName) { - let kDate = hmacSha256(dateStamp, 'AWS4' + secretKey); - let kRegion = hmacSha256(region, kDate); - let kService = hmacSha256(serviceName, kRegion); - let kSigning = hmacSha256('aws4_request', kService); - return kSigning; - } - - function createSignedHeaders(headers) { - return Object.keys(headers).sort().map(h => h.toLowerCase()).join(';'); - } - - function createStringToSign(timestamp, region, service, canonicalRequest) { - let stringToSign = 'AWS4-HMAC-SHA256\n'; - stringToSign += timestamp + '\n'; - stringToSign += timestamp.substr(0,8) + '/' + region + '/' + service + '/aws4_request\n'; - stringToSign += sha256(canonicalRequest); - return stringToSign; - } - - function createCanonicalRequest(method, path, querystring, headers, payload) { - let canonical = method + '\n'; - - //Changed this from double encoding the path to single encoding it, to make S3 paths with spaces work. However, the documentation said to - //double encode it...? The only time we actually encode a path other than / is when uploading to S3 so just change this to single encoding here - //but it's possible it will mess up if the path has some weird characters that should be double encoded maybe??? If you had weird symbols in your version number? - canonical += encodeURI(path) + '\n'; - - let qsKeys = Object.keys(querystring); - qsKeys.sort(); - - //encodeURIComponent does NOT encode ', but we need it to be encoded. escape() is considered deprecated, so encode ' - //manually - function encodeValue(v) { - return encodeURIComponent(v).replace(/'/g,'%27'); - } - - let qsEntries = qsKeys.map(k => `${k}=${encodeValue(querystring[k])}`); - canonical += qsEntries.join('&') + '\n'; - - let headerKeys = Object.keys(headers).sort(); - let headerEntries = headerKeys.map(h => h.toLowerCase() + ':' + headers[h].replace(/^\s*|\s*$/g, '').replace(' +', ' ')); - canonical += headerEntries.join('\n') + '\n\n'; - - canonical += createSignedHeaders(headers) + '\n'; - canonical += sha256(payload); - - return canonical; - } - - function createAuthHeader(accessKey, timestamp, region, service, headers, signature) { - let date = timestamp.substr(0,8); - let signedHeaders = createSignedHeaders(headers); - return `AWS4-HMAC-SHA256 Credential=${accessKey}/${date}/${region}/${service}/aws4_request, SignedHeaders=${signedHeaders}, Signature=${signature}`; - } - - let timestamp = new Date().toISOString().replace(/(-|:|\.\d\d\d)/g, ''); // YYYYMMDD'T'HHmmSS'Z' - let datestamp = timestamp.substr(0,8); - - let sessionTokenHeader = sessionToken ? {'x-amz-security-token': sessionToken} : {}; - - let reqHeaders = Object.assign({ - Accept : 'application/json', - Host : host, - 'Content-Type' : 'application/json', - 'x-amz-date' : timestamp, - 'x-amz-content-sha256' : sha256(payload) - }, sessionTokenHeader, headers); // Passed in headers override these... - - let canonicalRequest = createCanonicalRequest(method, path, querystring, reqHeaders, payload); - let stringToSign = createStringToSign(timestamp, region, service, canonicalRequest); - let signingKey = createSigningKey(secretKey, datestamp, region, service); - let signature = hmacSha256(stringToSign, signingKey, true); - let authHeader = createAuthHeader(accessKey, timestamp, region, service, reqHeaders, signature); - - reqHeaders.Authorization = authHeader; - - //Now, lets finally do a HTTP REQUEST!!! - request(method, encodeURI(path), reqHeaders, querystring, payload, (err, result) => { - if (err) { - reject(err); - } else { - if (result.statusCode >= 300 && result.statusCode < 400 && result.headers.location) { - const url = new URL(result.headers.location); - headers.Host = url.hostname; - resolve(awsApiRequest({ - ...options, - host: url.hostname - })); - } else { - resolve(result); - } - } - }); - }); -} - -function createResult(data, res) { - if (!data || data.length === 0) { - return { statusCode: res.statusCode, headers: res.headers, data:''}; - } - if (data && data.length > 0 && res.headers['content-type'] === 'application/json') { - return { statusCode : res.statusCode, headers: res.headers, data : JSON.parse(data)}; - } else { - return { statusCode : res.statusCode, headers: res.headers, data}; - } -} - -function request(method, path, headers, querystring, data, callback) { - - let qs = Object.keys(querystring).map(k => `${k}=${encodeURIComponent(querystring[k])}`).join('&'); - path += '?' + qs; - let hostname = headers.Host; - delete headers.Host; - headers['Content-Length'] = data.length; - const port = 443; - try { - const options = { hostname, port, path, method, headers }; - const req = https.request(options, res => { - - let chunks = []; - res.on('data', d => chunks.push(d)); - res.on('end', () => { - let buffer = Buffer.concat(chunks); - if (res.headers['content-encoding'] === 'gzip') { - zlib.gunzip(buffer, (err, decoded) => { - if (err) { - callback(err); - } else { - callback(null, createResult(decoded, res)); - } - }); - } else { - callback(null, createResult(buffer, res)); - } - }); - - }); - req.on('error', err => callback(err)); - - if (data) { - req.write(data); - } - req.end(); - } catch(err) { - callback(err); - } -} - -module.exports = awsApiRequest; diff --git a/beanstalk-deploy.js b/beanstalk-deploy.js deleted file mode 100644 index 007ad1b..0000000 --- a/beanstalk-deploy.js +++ /dev/null @@ -1,478 +0,0 @@ -#!/usr/bin/env node -// Author: Einar Egilsson, https://github.com/einaregilsson/beanstalk-deploy - -const awsApiRequest = require('./aws-api-request'); -const fs = require('fs'); - -const IS_GITHUB_ACTION = !!process.env.GITHUB_ACTIONS; - -if (IS_GITHUB_ACTION) { - console.error = msg => console.log(`::error::${msg}`); - console.warn = msg => console.log(`::warning::${msg}`); -} - -function createStorageLocation() { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: {Operation: 'CreateStorageLocation', Version: '2010-12-01'} - }); -} - -function checkIfFileExistsInS3(bucket, s3Key) { - - return awsApiRequest({ - service : 's3', - host: `${bucket}.s3.amazonaws.com`, - path : s3Key, - method: 'HEAD' - }); -} - -function readFile(path) { - return new Promise((resolve, reject) => { - fs.readFile(path, (err, data) => { - if (err) { - reject(err); - } - resolve(data); - }); - }); -} - -function uploadFileToS3(bucket, s3Key, filebuffer) { - return awsApiRequest({ - service : 's3', - host: `${bucket}.s3.amazonaws.com`, - path : s3Key, - method: 'PUT', - headers: { 'Content-Type' : 'application/octet-stream'}, - payload: filebuffer - }); -} - -function createBeanstalkVersion(application, bucket, s3Key, versionLabel, versionDescription) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'CreateApplicationVersion', - Version: '2010-12-01', - ApplicationName : application, - VersionLabel : versionLabel, - Description : versionDescription, - 'SourceBundle.S3Bucket' : bucket, - 'SourceBundle.S3Key' : s3Key.substr(1) //Don't want leading / here - } - }); -} - -function deployBeanstalkVersion(application, environmentName, versionLabel) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'UpdateEnvironment', - Version: '2010-12-01', - ApplicationName : application, - EnvironmentName : environmentName, - VersionLabel : versionLabel - } - }); -} - -function describeEvents(application, environmentName, startTime) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'DescribeEvents', - Version: '2010-12-01', - ApplicationName : application, - Severity : 'TRACE', - EnvironmentName : environmentName, - StartTime : startTime.toISOString().replace(/(-|:|\.\d\d\d)/g, '') - } - }); -} - -function describeEnvironments(application, environmentName) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'DescribeEnvironments', - Version: '2010-12-01', - ApplicationName : application, - 'EnvironmentNames.members.1' : environmentName //Yes, that's the horrible way to pass an array... - } - }); -} - -function getApplicationVersion(application, versionLabel) { - return awsApiRequest({ - service: 'elasticbeanstalk', - querystring: { - Operation: 'DescribeApplicationVersions', - Version: '2010-12-01', - ApplicationName : application, - 'VersionLabels.members.1' : versionLabel //Yes, that's the horrible way to pass an array... - } - }); -} - -function expect(status, result, extraErrorMessage) { - if (status !== result.statusCode) {  - if (extraErrorMessage) { - console.log(extraErrorMessage); - } - if (result.headers['content-type'] !== 'application/json') { - throw new Error(`Status: ${result.statusCode}. Message: ${result.data}`); - } else { - throw new Error(`Status: ${result.statusCode}. Code: ${result.data.Error.Code}, Message: ${result.data.Error.Message}`); - } - } -} - -//Uploads zip file, creates new version and deploys it -function deployNewVersion(application, environmentName, versionLabel, versionDescription, file, waitUntilDeploymentIsFinished, waitForRecoverySeconds) { - - let s3Key = `/${application}/${versionLabel}.zip`; - let bucket, deployStart, fileBuffer; - - readFile(file).then(result => { - fileBuffer = result; - return createStorageLocation(); - }).then(result => { - expect(200, result ); - bucket = result.data.CreateStorageLocationResponse.CreateStorageLocationResult.S3Bucket; - console.log(`Uploading file to bucket ${bucket}`); - return checkIfFileExistsInS3(bucket, s3Key); - }).then(result => { - if (result.statusCode === 200) { - throw new Error(`Version ${versionLabel} already exists in S3!`); - } - expect(404, result); - return uploadFileToS3(bucket, s3Key, fileBuffer); - }).then(result => { - expect(200, result); - console.log(`New build successfully uploaded to S3, bucket=${bucket}, key=${s3Key}`); - return createBeanstalkVersion(application, bucket, s3Key, versionLabel, versionDescription); - }).then(result => { - expect(200, result); - console.log(`Created new application version ${versionLabel} in Beanstalk.`); - if (!environmentName) { - console.log(`No environment name given, so exiting now without deploying the new version ${versionLabel} anywhere.`); - process.exit(0); - } - deployStart = new Date(); - console.log(`Starting deployment of version ${versionLabel} to environment ${environmentName}`); - return deployBeanstalkVersion(application, environmentName, versionLabel, waitForRecoverySeconds); - }).then(result => { - expect(200, result); - - if (waitUntilDeploymentIsFinished) { - console.log('Deployment started, "wait_for_deployment" was true...\n'); - return waitForDeployment(application, environmentName, versionLabel, deployStart, waitForRecoverySeconds); - } else { - console.log('Deployment started, parameter "wait_for_deployment" was false, so action is finished.'); - console.log('**** IMPORTANT: Please verify manually that the deployment succeeds!'); - process.exit(0); - } - - }).then(envAfterDeployment => { - if (envAfterDeployment.Health === 'Green') { - console.log('Environment update successful!'); - process.exit(0); - } else { - console.warn(`Environment update finished, but environment health is: ${envAfterDeployment.Health}, HealthStatus: ${envAfterDeployment.HealthStatus}`); - process.exit(1); - } - }).catch(err => { - console.error(`Deployment failed: ${err}`); - process.exit(2); - }); -} - -//Deploys existing version in EB -function deployExistingVersion(application, environmentName, versionLabel, waitUntilDeploymentIsFinished, waitForRecoverySeconds) { - let deployStart = new Date(); - console.log(`Deploying existing version ${versionLabel}`); - - deployBeanstalkVersion(application, environmentName, versionLabel).then(result => { - expect(200, result); - if (waitUntilDeploymentIsFinished) { - console.log('Deployment started, "wait_for_deployment" was true...\n'); - return waitForDeployment(application, environmentName, versionLabel, deployStart, waitForRecoverySeconds); - } else { - console.log('Deployment started, parameter "wait_for_deployment" was false, so action is finished.'); - console.log('**** IMPORTANT: Please verify manually that the deployment succeeds!'); - process.exit(0); - } - }).then(envAfterDeployment => { - if (envAfterDeployment.Health === 'Green') { - console.log('Environment update successful!'); - process.exit(0); - } else { - console.warn(`Environment update finished, but environment health is: ${envAfterDeployment.Health}, HealthStatus: ${envAfterDeployment.HealthStatus}`); - process.exit(1); - } - }).catch(err => { - console.error(`Deployment failed: ${err}`); - process.exit(2); - }); -} - - -function strip(val) { - //Strip leadig or trailing whitespace - return (val || '').replace(/^\s*|\s*$/g, ''); -} - -function main() { - - let application, - environmentName, - versionLabel, - versionDescription, - region, - file, - useExistingVersionIfAvailable, - waitForRecoverySeconds = 30, - waitUntilDeploymentIsFinished = true; //Whether or not to wait for the deployment to complete... - - if (IS_GITHUB_ACTION) { //Running in GitHub Actions - application = strip(process.env.INPUT_APPLICATION_NAME); - environmentName = strip(process.env.INPUT_ENVIRONMENT_NAME); - versionLabel = strip(process.env.INPUT_VERSION_LABEL); - versionDescription = strip(process.env.INPUT_VERSION_DESCRIPTION); - file = strip(process.env.INPUT_DEPLOYMENT_PACKAGE); - - awsApiRequest.accessKey = strip(process.env.INPUT_AWS_ACCESS_KEY); - awsApiRequest.secretKey = strip(process.env.INPUT_AWS_SECRET_KEY); - awsApiRequest.sessionToken = strip(process.env.INPUT_AWS_SESSION_TOKEN); - awsApiRequest.region = strip(process.env.INPUT_REGION); - - if ((process.env.INPUT_WAIT_FOR_DEPLOYMENT || '').toLowerCase() == 'false') { - waitUntilDeploymentIsFinished = false; - } - - if (process.env.INPUT_WAIT_FOR_ENVIRONMENT_RECOVERY) { - waitForRecoverySeconds = parseInt(process.env.INPUT_WAIT_FOR_ENVIRONMENT_RECOVERY); - } - useExistingVersionIfAvailable = process.env.INPUT_USE_EXISTING_VERSION_IF_AVAILABLE == 'true' || process.env.INPUT_USE_EXISTING_VERSION_IF_AVAILABLE == 'True'; - - } else { //Running as command line script - if (process.argv.length < 6) { - console.log('\nbeanstalk-deploy: Deploy a zip file to AWS Elastic Beanstalk'); - console.log('https://github.com/einaregilsson/beanstalk-deploy\n'); - console.log('Usage: beanstalk-deploy.js []\n'); - console.log('Environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be defined for the program to work.'); - console.log('If is skipped the script will attempt to deploy an existing version named .\n'); - process.exit(1); - } - - [application, environmentName, versionLabel, region, file] = process.argv.slice(2); - versionDescription = ''; //Not available for this. - useExistingVersionIfAvailable = false; //This option is not available in the console version - - awsApiRequest.accessKey = strip(process.env.AWS_ACCESS_KEY_ID); - awsApiRequest.secretKey = strip(process.env.AWS_SECRET_ACCESS_KEY); - awsApiRequest.sessionToken = strip(process.env.AWS_SESSION_TOKEN); - awsApiRequest.region = strip(region); - } - - console.log('Beanstalk-Deploy: GitHub Action for deploying to Elastic Beanstalk.'); - console.log('https://github.com/einaregilsson/beanstalk-deploy'); - console.log(''); - - if (!awsApiRequest.region) { - console.error('Deployment failed: Region not specified!'); - process.exit(2); - } - if (!awsApiRequest.accessKey) { - console.error('Deployment failed: AWS Access Key not specified!'); - process.exit(2); - } - if (!awsApiRequest.secretKey) { - console.error('Deployment failed: AWS Secret Key not specified!'); - process.exit(2); - } - - - console.log(' ***** Input parameters were: ***** '); - console.log(' Application: ' + application); - console.log(' Environment: ' + environmentName); - console.log(' Version Label: ' + versionLabel); - console.log(' Version description: ' + versionDescription); - console.log(' AWS Region: ' + awsApiRequest.region); - console.log(' File: ' + file); - console.log(' AWS Access Key: ' + awsApiRequest.accessKey.length + ' characters long, starts with ' + awsApiRequest.accessKey.charAt(0)); - console.log(' AWS Secret Key: ' + awsApiRequest.secretKey.length + ' characters long, starts with ' + awsApiRequest.secretKey.charAt(0)); - console.log(' Wait for deployment: ' + waitUntilDeploymentIsFinished); - console.log(' Recovery wait time: ' + waitForRecoverySeconds); - console.log(''); - - getApplicationVersion(application, versionLabel).then(result => { - - expect(200, result); - - let versionsList = result.data.DescribeApplicationVersionsResponse.DescribeApplicationVersionsResult.ApplicationVersions; - let versionAlreadyExists = versionsList.length === 1; - - if (versionAlreadyExists) { - - if (!environmentName) { - console.error(`You have no environment set, so we are trying to only create version ${versionLabel}, but it already exists in Beanstalk!`); - process.exit(2); - } else if (file && !useExistingVersionIfAvailable) { - console.error(`Deployment failed: Version ${versionLabel} already exists. Either remove the "deployment_package" parameter to deploy existing version, or set the "use_existing_version_if_available" parameter to "true" to use existing version if it exists and deployment package if it doesn't.`); - process.exit(2); - } else { - if (file && useExistingVersionIfAvailable) { - console.log(`Ignoring deployment package ${file} since version ${versionLabel} already exists and "use_existing_version_if_available" is set to true.`); - } - console.log(`Deploying existing version ${versionLabel}, version info:`); - console.log(JSON.stringify(versionsList[0], null, 2)); - deployExistingVersion(application, environmentName, versionLabel, waitUntilDeploymentIsFinished, waitForRecoverySeconds); - } - } else { - if (file) { - deployNewVersion(application, environmentName, versionLabel, versionDescription, file, waitUntilDeploymentIsFinished, waitForRecoverySeconds); - } else { - console.error(`Deployment failed: No deployment package given but version ${versionLabel} doesn't exist, so nothing to deploy!`); - process.exit(2); - } - } - }).catch(err => { - console.error(`Deployment failed: ${err}`); - process.exit(2); - }); -} - -function formatTimespan(since) { - let elapsed = new Date().getTime() - since; - let seconds = Math.floor(elapsed / 1000); - let minutes = Math.floor(seconds / 60); - seconds -= (minutes * 60); - return `${minutes}m${seconds}s`; -} - -//Wait until the new version is deployed, printing any events happening during the wait... -function waitForDeployment(application, environmentName, versionLabel, start, waitForRecoverySeconds) { - let counter = 0; - let degraded = false; - let healThreshold; - let deploymentFailed = false; - - const SECOND = 1000; - const MINUTE = 60 * SECOND; - - let waitPeriod = 10 * SECOND; //Start at ten seconds, increase slowly, long deployments have been erroring with too many requests. - let waitStart = new Date().getTime(); - - let eventCalls = 0, environmentCalls = 0; // Getting throttled on these print out how many we're doing... - - let consecutiveThrottleErrors = 0; - - return new Promise((resolve, reject) => { - function update() { - - let elapsed = new Date().getTime() - waitStart; - - //Limit update requests for really long deploys - if (elapsed > (10 * MINUTE)) { - waitPeriod = 30 * SECOND; - } else if (elapsed > 5 * MINUTE) { - waitPeriod = 20 * SECOND; - } - - describeEvents(application, environmentName, start).then(result => { - eventCalls++; - - - //Allow a few throttling failures... - if (result.statusCode === 400 && result.data && result.data.Error && result.data.Error.Code == 'Throttling') { - consecutiveThrottleErrors++; - console.log(`Request to DescribeEvents was throttled, that's ${consecutiveThrottleErrors} throttle errors in a row...`); - return; - } - - consecutiveThrottleErrors = 0; //Reset the throttling count - - expect(200, result, `Failed in call to describeEvents, have done ${eventCalls} calls to describeEvents, ${environmentCalls} calls to describeEnvironments in ${formatTimespan(waitStart)}`); - let events = result.data.DescribeEventsResponse.DescribeEventsResult.Events.reverse(); //They show up in desc, we want asc for logging... - for (let ev of events) { - let date = new Date(ev.EventDate * 1000); //Seconds to milliseconds, - console.log(`${date.toISOString().substr(11,8)} ${ev.Severity}: ${ev.Message}`); - if (ev.Message.match(/Failed to deploy application/)) { - deploymentFailed = true; //wait until next iteration to finish, to get the final messages... - } - } - if (events.length > 0) { - start = new Date(events[events.length-1].EventDate * 1000 + 1000); //Add extra second so we don't get the same message next time... - } - }).catch(reject); - - describeEnvironments(application, environmentName).then(result => { - environmentCalls++; - - //Allow a few throttling failures... - if (result.statusCode === 400 && result.data && result.data.Error && result.data.Error.Code == 'Throttling') { - consecutiveThrottleErrors++; - console.log(`Request to DescribeEnvironments was throttled, that's ${consecutiveThrottleErrors} throttle errors in a row...`); - if (consecutiveThrottleErrors >= 5) { - throw new Error(`Deployment failed, got ${consecutiveThrottleErrors} throttling errors in a row while waiting for deployment`); - } - - setTimeout(update, waitPeriod); - return; - } - - expect(200, result, `Failed in call to describeEnvironments, have done ${eventCalls} calls to describeEvents, ${environmentCalls} calls to describeEnvironments in ${formatTimespan(waitStart)}`); - - consecutiveThrottleErrors = 0; - counter++; - let env = result.data.DescribeEnvironmentsResponse.DescribeEnvironmentsResult.Environments[0]; - if (env.VersionLabel === versionLabel && env.Status === 'Ready') { - if (!degraded) { - console.log(`Deployment finished. Version updated to ${env.VersionLabel}`); - console.log(`Status for ${application}-${environmentName} is ${env.Status}, Health: ${env.Health}, HealthStatus: ${env.HealthStatus}`); - - if (env.Health === 'Green') { - resolve(env); - } else { - console.warn(`Environment update finished, but health is ${env.Health} and health status is ${env.HealthStatus}. Giving it ${waitForRecoverySeconds} seconds to recover...`); - degraded = true; - healThreshold = new Date(new Date().getTime() + waitForRecoverySeconds * SECOND); - setTimeout(update, waitPeriod); - } - } else { - if (env.Health === 'Green') { - console.log(`Environment has recovered, health is now ${env.Health}, health status is ${env.HealthStatus}`); - resolve(env); - } else { - if (new Date().getTime() > healThreshold.getTime()) { - reject(new Error(`Environment still has health ${env.Health} ${waitForRecoverySeconds} seconds after update finished!`)); - } else { - let left = Math.floor((healThreshold.getTime() - new Date().getTime()) / 1000); - console.warn(`Environment still has health: ${env.Health} and health status ${env.HealthStatus}. Waiting ${left} more seconds before failing...`); - setTimeout(update, waitPeriod); - } - } - } - } else if (deploymentFailed) { - let msg = `Deployment failed! Current State: Version: ${env.VersionLabel}, Health: ${env.Health}, Health Status: ${env.HealthStatus}`; - console.log(`${new Date().toISOString().substr(11,8)} ERROR: ${msg}`); - reject(new Error(msg)); - } else { - if (counter % 6 === 0 && !deploymentFailed) { - console.log(`${new Date().toISOString().substr(11,8)} INFO: Still updating, status is "${env.Status}", health is "${env.Health}", health status is "${env.HealthStatus}"`); - } - setTimeout(update, waitPeriod); - } - }).catch(reject); - } - - update(); - }); -} - -main(); - - diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..56a2c19 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import perfectionist from 'eslint-plugin-perfectionist'; + +export default tseslint.config( + { + ignores: ['**/*.js'], + }, + eslint.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + perfectionist.configs['recommended-natural'] +); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..4a524fd --- /dev/null +++ b/jest.config.js @@ -0,0 +1,19 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + testEnvironment: 'node', + transform: { + '^.+\\.tsx?$': ['ts-jest', {}], + }, + coverageDirectory: 'coverage', + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/*.test.{ts,tsx}', + '!src/**/index.ts', + '!src/**/types.ts', + ], + coverageReporters: ['text', 'lcov'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + modulePathIgnorePatterns: ['/dist/'], +}; diff --git a/make-zip-for-aws.js b/make-zip-for-aws.js deleted file mode 100644 index e05d1e8..0000000 --- a/make-zip-for-aws.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - - Use archiver package to make a zip to use with aws - - https://www.npmjs.com/package/archiver - - https://github.com/archiverjs/node-archiver - - */ -// require modules -const fs = require('fs'); -const archiver = require('archiver'); - -// create a file to stream archive data to. -const output = fs.createWriteStream(__dirname + '/for-aws.zip'); -const archive = archiver('zip', { - zlib: { level: 9 } // Sets the compression level. -}); - -// listen for all archive data to be written -// 'close' event is fired only when a file descriptor is involved -output.on('close', function() { - console.log(archive.pointer() + ' total bytes zipped'); - console.log('Zip file is ready.'); -}); - -// good practice to catch warnings (ie stat failures and other non-blocking errors) -archive.on('warning', function(err) { - if (err.code === 'ENOENT') { - // log warning - console.log({err}) - } else { - // throw error - throw err; - } -}); - -// good practice to catch this error explicitly -archive.on('error', function(err) { - throw err; -}); - -// pipe archive data to the file -archive.pipe(output); - -// append both package json files -archive.glob('package*.json'); -// append all files in the src folder -archive.directory('src/'); - -// finalize the archive (ie we are done appending files but streams have to finish yet) -// 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand -archive.finalize(); diff --git a/package-lock.json b/package-lock.json deleted file mode 100755 index b888a7c..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2056 +0,0 @@ -{ - "name": "week-10", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "dev": true - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dev": true, - "requires": { - "defer-to-connect": "^1.0.1" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - } - }, - "ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", - "dev": true, - "requires": { - "string-width": "^3.0.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "archiver": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.0.2.tgz", - "integrity": "sha512-Tq3yV/T4wxBsD2Wign8W9VQKhaUxzzRmjEiSoOK0SLqPgDP/N1TKdYyBeIEu56T4I9iO4fKTTR0mN9NWkBA0sg==", - "requires": { - "archiver-utils": "^2.1.0", - "async": "^3.2.0", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.0.0", - "tar-stream": "^2.1.4", - "zip-stream": "^4.0.0" - } - }, - "archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "requires": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true - }, - "bl": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - } - } - }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - } - }, - "boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", - "dev": true, - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "bson": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", - "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" - }, - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dev": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true - } - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" - } - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "cli-boxes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", - "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==", - "dev": true - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" - }, - "compress-commons": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.1.tgz", - "integrity": "sha512-xZm9o6iikekkI0GnXCmAl3LQGZj5TBDj0zLowsqi7tJtEa3FMGSEcHcqrSJIrOAk1UG/NBbDn/F1q+MG/p/EsA==", - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - } - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-security-policy-builder": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz", - "integrity": "sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w==" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "requires": { - "buffer": "^5.1.0" - } - }, - "crc32-stream": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.0.tgz", - "integrity": "sha512-tyMw2IeUX6t9jhgXI6um0eKfWq4EIDpfv5m7GX4Jzp7eVelQ360xd8EPXJhp2mHwLQIkqlnMLjzqSZI3a+0wRw==", - "requires": { - "crc": "^3.4.4", - "readable-stream": "^3.4.0" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true - }, - "dasherize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", - "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "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==", - "dev": true - }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "dns-prefetch-control": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz", - "integrity": "sha1-YN20V3dOF48flBXwyrsOhbCzALI=" - }, - "dont-sniff-mimetype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz", - "integrity": "sha1-WTKJDcn04vGeXrAqIAJuXl78j1g=" - }, - "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "env-cmd": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz", - "integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==", - "requires": { - "commander": "^4.0.0", - "cross-spawn": "^7.0.0" - } - }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "expect-ct": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.1.1.tgz", - "integrity": "sha512-ngXzTfoRGG7fYens3/RMb6yYoVLvLMfmsSllP/mZPxNHgFq41TmPSLF/nLY7fwoclI2vElvAmILFWGUYqdjfCg==" - }, - "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "feature-policy": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.2.0.tgz", - "integrity": "sha512-2hGrlv6efG4hscYVZeaYjpzpT6I2OZgYqE2yDUzeAcKj2D1SH0AsEzqJNXzdoglEddcIXQQYop3lD97XpG75Jw==" - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "frameguard": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.0.0.tgz", - "integrity": "sha1-e8rUae57lukdEs6zlZx4I1qScuk=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "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==" - }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "git-config-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-1.0.1.tgz", - "integrity": "sha1-bTP37WPbDQ4RgTFQO6s6ykfVRmQ=", - "requires": { - "extend-shallow": "^2.0.1", - "fs-exists-sync": "^0.1.0", - "homedir-polyfill": "^1.0.0" - } - }, - "git-user-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-user-name/-/git-user-name-2.0.0.tgz", - "integrity": "sha512-1DC8rUNm2I5V9v4eIpK6PSjKCp9bI0t6Wl05WSk+xEMS8GhR8GWzxM3aGZfPrfuqEfWxSbui5/pQJryJFXqCzQ==", - "requires": { - "extend-shallow": "^2.0.1", - "git-config-path": "^1.0.1", - "parse-git-config": "^1.1.1" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "global-dirs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", - "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", - "dev": true, - "requires": { - "ini": "^1.3.5" - } - }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dev": true, - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true - }, - "helmet": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.15.1.tgz", - "integrity": "sha512-hgoNe/sjKlKNvJ3g9Gz149H14BjMMWOCmW/DTXl7IfyKGtIK37GePwZrHNfr4aPXdKVyXcTj26RgRFbPKDy9lw==", - "requires": { - "depd": "2.0.0", - "dns-prefetch-control": "0.1.0", - "dont-sniff-mimetype": "1.0.0", - "expect-ct": "0.1.1", - "feature-policy": "0.2.0", - "frameguard": "3.0.0", - "helmet-crossdomain": "0.3.0", - "helmet-csp": "2.7.1", - "hide-powered-by": "1.0.0", - "hpkp": "2.0.0", - "hsts": "2.1.0", - "ienoopen": "1.0.0", - "nocache": "2.0.0", - "referrer-policy": "1.1.0", - "x-xss-protection": "1.1.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, - "helmet-crossdomain": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.3.0.tgz", - "integrity": "sha512-YiXhj0E35nC4Na5EPE4mTfoXMf9JTGpN4OtB4aLqShKuH9d2HNaJX5MQoglO6STVka0uMsHyG5lCut5Kzsy7Lg==" - }, - "helmet-csp": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.7.1.tgz", - "integrity": "sha512-sCHwywg4daQ2mY0YYwXSZRsgcCeerUwxMwNixGA7aMLkVmPTYBl7gJoZDHOZyXkqPrtuDT3s2B1A+RLI7WxSdQ==", - "requires": { - "camelize": "1.0.0", - "content-security-policy-builder": "2.0.0", - "dasherize": "2.0.0", - "platform": "1.3.5" - } - }, - "hide-powered-by": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.0.0.tgz", - "integrity": "sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=" - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hpkp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", - "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" - }, - "hsts": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.1.0.tgz", - "integrity": "sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA==" - }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "ienoopen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.0.0.tgz", - "integrity": "sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms=" - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "dev": true, - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - } - }, - "is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", - "dev": true - }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dev": true, - "requires": { - "json-buffer": "3.0.0" - } - }, - "latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "dev": true, - "requires": { - "package-json": "^6.3.0" - } - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "requires": { - "readable-stream": "^2.0.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" - }, - "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", - "requires": { - "mime-db": "~1.38.0" - } - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mongodb": { - "version": "3.1.13", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.13.tgz", - "integrity": "sha512-sz2dhvBZQWf3LRNDhbd30KHVzdjZx9IKC0L+kSZ/gzYquCF5zPOgGqRz6sSCqYZtKP2ekB4nfLxhGtzGHnIKxA==", - "requires": { - "mongodb-core": "3.1.11", - "safe-buffer": "^5.1.2" - } - }, - "mongodb-core": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.11.tgz", - "integrity": "sha512-rD2US2s5qk/ckbiiGFHeu+yKYDXdJ1G87F6CG3YdaZpzdOm5zpoAZd/EKbPmFO6cQZ+XVXBXBJ660sSI0gc6qg==", - "requires": { - "bson": "^1.1.0", - "require_optional": "^1.0.1", - "safe-buffer": "^5.1.2", - "saslprep": "^1.0.0" - } - }, - "morgan": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", - "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", - "requires": { - "basic-auth": "~2.0.0", - "debug": "2.6.9", - "depd": "~1.1.2", - "on-finished": "~2.3.0", - "on-headers": "~1.0.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "nocache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", - "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" - }, - "nodemon": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz", - "integrity": "sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ==", - "dev": true, - "requires": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", - "semver": "^5.7.1", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^4.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "dev": true - }, - "package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "dev": true, - "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "parse-git-config": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-1.1.1.tgz", - "integrity": "sha1-06mYQxcTL1c5hxK7pDjhKVkN34w=", - "requires": { - "extend-shallow": "^2.0.1", - "fs-exists-sync": "^0.1.0", - "git-config-path": "^1.0.1", - "ini": "^1.3.4" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, - "platform": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", - "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" - } - }, - "pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pupa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", - "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", - "dev": true, - "requires": { - "escape-goat": "^2.0.0" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdir-glob": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.0.tgz", - "integrity": "sha512-KgT0oXPIDQRRRYFf+06AUaodICTep2Q5635BORLzTEzp7rEqcR14a47j3Vzm3ix7FeI1lp8mYyG7r8lTB06Pyg==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "referrer-policy": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.1.0.tgz", - "integrity": "sha1-NXdOtzW/UPtsB46DM0tHI1AgfXk=" - }, - "registry-auth-token": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz", - "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, - "registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" - } - }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "dev": true, - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "saslprep": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", - "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", - "optional": true, - "requires": { - "sparse-bitfield": "^3.0.3" - } - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" - }, - "semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "dev": true, - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", - "optional": true, - "requires": { - "memory-pager": "^1.0.2" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "tar-stream": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", - "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "term-size": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", - "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", - "dev": true - }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - } - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "undefsafe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", - "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", - "dev": true, - "requires": { - "debug": "^2.2.0" - } - }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "requires": { - "crypto-random-string": "^2.0.0" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "update-notifier": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz", - "integrity": "sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==", - "dev": true, - "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - } - }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "dev": true, - "requires": { - "prepend-http": "^2.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "dev": true, - "requires": { - "string-width": "^4.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "x-xss-protection": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.1.0.tgz", - "integrity": "sha512-rx3GzJlgEeZ08MIcDsU2vY2B1QEriUKJTSiNHHUIem6eg9pzVOr2TL3Y4Pd6TMAM5D5azGjcxqI62piITBDHVg==" - }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true - }, - "zip-a-folder": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/zip-a-folder/-/zip-a-folder-0.0.12.tgz", - "integrity": "sha512-wZGiWgp3z2TocBlzx3S5tsLgPbT39qG2uIZmn2MhYLVjhKIr2nMhg7i4iPDL4W3XvMDaOEEVU5ZB0Y/Pt6BLvA==", - "requires": { - "archiver": "^3.1.1" - }, - "dependencies": { - "archiver": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", - "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==", - "requires": { - "archiver-utils": "^2.1.0", - "async": "^2.6.3", - "buffer-crc32": "^0.2.1", - "glob": "^7.1.4", - "readable-stream": "^3.4.0", - "tar-stream": "^2.1.0", - "zip-stream": "^2.1.2" - } - }, - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - } - }, - "compress-commons": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", - "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==", - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^3.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^2.3.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "crc32-stream": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz", - "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==", - "requires": { - "crc": "^3.4.4", - "readable-stream": "^3.4.0" - } - }, - "zip-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", - "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==", - "requires": { - "archiver-utils": "^2.1.0", - "compress-commons": "^2.1.1", - "readable-stream": "^3.4.0" - } - } - } - }, - "zip-stream": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.0.2.tgz", - "integrity": "sha512-TGxB2g+1ur6MHkvM644DuZr8Uzyz0k0OYWtS3YlpfWBEmK4woaC2t3+pozEL3dBfIPmpgmClR5B2QRcMgGt22g==", - "requires": { - "archiver-utils": "^2.1.0", - "compress-commons": "^4.0.0", - "readable-stream": "^3.6.0" - } - } - } -} diff --git a/package.json b/package.json index 294a56a..33038c6 100755 --- a/package.json +++ b/package.json @@ -1,30 +1,76 @@ { - "name": "week-10", - "version": "1.0.0", + "name": "backend-api", + "version": "1.0.1", "description": "", - "main": "./src/index.js", + "main": "./src/index.ts", + "private": true, "scripts": { - "start": "node ./src/index.js", - "dev": "env-cmd nodemon ./src/index.js", - "test": "node ./src/index.js", - "zip-for-aws": "node make-zip-for-aws.js" + "prebuild": "rm -rf dist", + "build": "tsc", + "start": "node dist/index.js", + "dev": "env-cmd ts-node src/index.ts", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts --no-ignore", + "format": "prettier --write ." }, - "keywords": [], + "imports": { + "#*": "./src/*" + }, + "keywords": [ + "mongodb", + "express", + "api", + "rest" + ], "author": "", "license": "ISC", "dependencies": { - "archiver": "^5.0.2", - "body-parser": "^1.18.3", + "@types/swagger-jsdoc": "^6.0.4", + "bcrypt": "^6.0.0", + "compression": "^1.8.0", "cors": "^2.8.5", + "dotenv": "^16.5.0", "env-cmd": "^10.1.0", - "express": "^4.16.4", - "git-user-name": "^2.0.0", + "express": "^4.21.2", + "express-rate-limit": "^7.5.0", "helmet": "^3.15.1", - "mongodb": "^3.1.13", + "jsonwebtoken": "^9.0.2", + "mongodb": "^6.16.0", "morgan": "^1.9.1", - "zip-a-folder": "0.0.12" + "nodemon": "^3.1.10", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { - "nodemon": "^2.0.4" + "@eslint/js": "^9.27.0", + "@types/bcrypt": "^5.0.2", + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.18", + "@types/express": "^5.0.2", + "@types/helmet": "^0.0.48", + "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.9", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.18", + "@types/supertest": "^6.0.3", + "@types/swagger-ui-express": "^4.1.8", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "eslint": "^9.27.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-perfectionist": "^4.13.0", + "eslint-plugin-prettier": "^5.4.0", + "jest": "^29.7.0", + "mongodb-memory-server": "^10.1.4", + "prettier": "^3.5.3", + "supertest": "^7.1.0", + "ts-jest": "^29.3.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.1" } } diff --git a/src/__tests__/http/app.http b/src/__tests__/http/app.http new file mode 100644 index 0000000..10bc4e0 --- /dev/null +++ b/src/__tests__/http/app.http @@ -0,0 +1,8 @@ +### Home page should redirect to /health +GET http://localhost:3001 + +### Get /health +GET http://localhost:3001/health + +### Test a 404 +GET http://localhost:3001/testing-404 diff --git a/src/__tests__/http/app.test.ts b/src/__tests__/http/app.test.ts new file mode 100644 index 0000000..a0ccd0a --- /dev/null +++ b/src/__tests__/http/app.test.ts @@ -0,0 +1,31 @@ +import 'dotenv/config'; +import request, { Response } from 'supertest'; + +import app from '../../app'; + +interface ResponseBody { + message?: string; + status: string; +} + +describe('Health Check Endpoint', () => { + it('should return 302 and redirect to /health', async () => { + const res: Response = await request(app).get('/'); + expect(res.status).toBe(302); + expect(res.headers.location).toBe('/health'); + }); + + it('should return 200 and status OK', async () => { + const res: Response = await request(app).get('/health'); + const body = res.body as ResponseBody; + expect(body.status).toBe('OK'); + expect(res.statusCode).toEqual(200); + }); + + it('should return 404 for non-existent endpoint', async () => { + const res: Response = await request(app).get('/non-existent'); + const body = res.body as ResponseBody; + expect(res.statusCode).toEqual(404); + expect(body.message).toBe('Route not found'); + }); +}); diff --git a/src/__tests__/http/authentication.test.ts b/src/__tests__/http/authentication.test.ts new file mode 100644 index 0000000..ed5bd33 --- /dev/null +++ b/src/__tests__/http/authentication.test.ts @@ -0,0 +1,62 @@ +import 'dotenv/config'; +import request, { Response } from 'supertest'; + +import app from '../../app'; +import { stopDatabase } from '../../database/mongo-common'; + +// Define expected response shapes +interface AuthResponse { + message?: string; + token: string; +} + +describe('Authentication JWT', () => { + afterAll(async () => { + await stopDatabase(); // Ensure database connection is closed + }); + + const testUser = { + email: 'testuser@example.com', + password: 'SecurePass123!', + }; + + it('should register a new user', async () => { + const res: Response = await request(app).post('/auth/register').send(testUser).expect(201); + const body = res.body as AuthResponse; + expect(body).toHaveProperty('message'); + }); + + it('should login with valid credentials', async () => { + const res: Response = await request(app).post('/auth/login').send(testUser).expect(200); + const body = res.body as AuthResponse; + expect(body).toHaveProperty('token'); + expect(typeof body.token).toBe('string'); + }); + + it('should return user profile with valid token', async () => { + const loginRes: Response = await request(app).post('/auth/login').send(testUser).expect(200); + const body = loginRes.body as AuthResponse; + + const res: Response = await request(app) + .get('/auth/me') + .set('Authorization', `Bearer ${body.token}`) + .expect(200); + + const userBody = res.body as { email: string }; + expect(res.statusCode).toEqual(200); + expect(userBody).toHaveProperty('email', testUser.email); + }); + + it('should reject request with invalid token', async () => { + const invalidToken = 'this.is.an.invalid.token'; + + const res: Response = await request(app) + .get('/auth/me') + .set('Authorization', `Bearer ${invalidToken}`) + .expect(401); + + const body = res.body as AuthResponse; + expect(body).toHaveProperty('message'); + expect(typeof body.message).toBe('string'); + }); +}); diff --git a/src/http_tests/stores.http b/src/__tests__/http/stores.http old mode 100755 new mode 100644 similarity index 56% rename from src/http_tests/stores.http rename to src/__tests__/http/stores.http index 651d77a..bbceeef --- a/src/http_tests/stores.http +++ b/src/__tests__/http/stores.http @@ -9,18 +9,18 @@ POST http://localhost:3001/stores Content-Type: application/json { - "Store Profile": "Nevada Golf Emprium", - "Shipping Info": "99 Nowhere Drive, Nevada" + "store_profile": "Nevada Golf Emprium", + "shipping_address": "99 Nowhere Drive, Nevada" } ### Test the PUT which should change a store -PUT http://localhost:3001/stores/5f2caef3b78cd6525812e063 +PUT http://localhost:3001/stores/6820ae08990eaee632a18472 Content-Type: application/json { - "Store Profile": "Nevada Golf Emporium" + "metadata": "68203238d1857e2fae0b6093" } ### Test DELETE which should delete a store -DELETE http://localhost:3001/stores/5f2caef3b78cd6525812e063 \ No newline at end of file +DELETE http://localhost:3001/stores/6820ae08990eaee632a18472 \ No newline at end of file diff --git a/src/__tests__/http/stores.test.ts b/src/__tests__/http/stores.test.ts new file mode 100644 index 0000000..91133f2 --- /dev/null +++ b/src/__tests__/http/stores.test.ts @@ -0,0 +1,80 @@ +import 'dotenv/config'; +import request, { Response } from 'supertest'; + +import app from '../../app'; // Adjust the path as necessary +import { stopDatabase } from '../../database/mongo-common'; + +interface Store { + _id?: string; // Optionally include the ID in responses + metadata?: string; + shipping_address: string; + store_profile: string; +} + +describe('Store "Collections" Endpoint', () => { + afterAll(async () => { + await stopDatabase(); // Ensure database connection is closed + }); + + // Test POST /stores to create a new store + it('should create a new store', async () => { + const storeData: Store = { + shipping_address: '99 Nowhere Drive, Nevada', + store_profile: 'Nevada Golf Emprium', + }; + + const res: Response = await request(app) + .post('/stores') + .send(storeData) + .set('Content-Type', 'application/json'); + + const body = res.body as Store; + expect(res.statusCode).toEqual(201); // Expecting 201 Created + expect(body.store_profile).toBe(storeData.store_profile); + expect(body.shipping_address).toBe(storeData.shipping_address); + }); + + // Test GET /stores to fetch all stores + it('should return a list of stores', async () => { + const res: Response = await request(app).get('/stores'); + const body = res.body as Store[]; + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); // Expecting an array of stores + expect(body.length).toBeGreaterThan(0); // Should contain at least one store + }); + + // Test PUT /stores/:id to update an existing store + it('should update an existing store', async () => { + const stores: Response = await request(app).get('/stores'); + const storesBody = stores.body as Store[]; + const storeId = storesBody[0]._id; + const updatedData: Partial = { + metadata: '68203238d1857e2fae0b6093', + }; + + if (storeId) { + const res: Response = await request(app) + .put(`/stores/${storeId}`) + .send(updatedData) + .set('Content-Type', 'application/json'); + + const body = res.body as Store; + expect(res.statusCode).toEqual(200); + expect(body.metadata).toBe(updatedData.metadata); + } + }); + + // Test DELETE /stores/:id to delete a store + it('should delete a store', async () => { + const stores: Response = await request(app).get('/stores'); + const storesBody = stores.body as Store[]; + const storeId = storesBody[0]._id; // Get the ID of the first store + + if (storeId) { + const res: Response = await request(app).delete(`/stores/${storeId}`); + const body = res.body as { message: string }; + expect(res.statusCode).toEqual(200); + expect(body.message).toBe('Store deleted'); // Ensure that the response contains the message + } + }); +}); diff --git a/src/__tests__/unit/errorHandler.test.ts b/src/__tests__/unit/errorHandler.test.ts new file mode 100644 index 0000000..1b1d34b --- /dev/null +++ b/src/__tests__/unit/errorHandler.test.ts @@ -0,0 +1,50 @@ +import { Request, Response } from 'express'; + +import errorHandler from '../../midleware/errorHandler'; + +describe('errorHandler middleware', () => { + const mockReq = {} as Request; + + let mockStatus: jest.Mock; + let mockJson: jest.Mock; + let mockRes: Response; + + beforeEach(() => { + mockStatus = jest.fn().mockReturnThis(); + mockJson = jest.fn(); + + mockRes = { + json: mockJson, + status: mockStatus, + } as unknown as Response; + }); + + const mockNext = jest.fn(); + + it('should respond with 500 and error message in non-production', () => { + process.env.NODE_ENV = 'development'; // or 'test' + const err = new Error('Something went wrong'); + + errorHandler(err, mockReq, mockRes, mockNext); + + expect(mockStatus).toHaveBeenCalledWith(500); + expect(mockJson).toHaveBeenCalledWith( + expect.objectContaining({ + error: 'Something went wrong', + message: 'Internal Server Error', + }) + ); + }); + + it('should not include error details in production', () => { + process.env.NODE_ENV = 'production'; + const err = new Error('Production error'); + + errorHandler(err, mockReq, mockRes, mockNext); + + expect(mockStatus).toHaveBeenCalledWith(500); + expect(mockJson).toHaveBeenCalledWith({ + message: 'Internal Server Error', + }); + }); +}); diff --git a/src/__tests__/unit/git-user-name.test.ts b/src/__tests__/unit/git-user-name.test.ts new file mode 100644 index 0000000..e1c97bb --- /dev/null +++ b/src/__tests__/unit/git-user-name.test.ts @@ -0,0 +1,38 @@ +import { execSync } from 'child_process'; + +import getGitUserName from '../../utils/git-user-name'; + +jest.mock('child_process'); + +describe('getGitUserName', () => { + afterEach(() => { + jest.resetAllMocks(); + delete process.env.GITHUB_ACTIONS; + delete process.env.GITHUB_ACTOR; + }); + + it('returns GITHUB_ACTOR when running in GitHub Actions', () => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.GITHUB_ACTOR = 'github-test-user'; + expect(getGitUserName()).toBe('github-test-user'); + }); + + it('returns "github-actions" if GITHUB_ACTOR is missing in GitHub Actions', () => { + process.env.GITHUB_ACTIONS = 'true'; + delete process.env.GITHUB_ACTOR; + expect(getGitUserName()).toBe('github-actions'); + }); + + it('returns git config user.name when not in GitHub Actions', () => { + (execSync as jest.Mock).mockReturnValue('Test User\n'); + expect(getGitUserName()).toBe('Test User'); + expect(execSync).toHaveBeenCalledWith('git config --get user.name', { encoding: 'utf8' }); + }); + + it('returns "unknown" if execSync throws', () => { + (execSync as jest.Mock).mockImplementation(() => { + throw new Error('fail'); + }); + expect(getGitUserName()).toBe('unknown'); + }); +}); diff --git a/src/__tests__/unit/stores.test.ts b/src/__tests__/unit/stores.test.ts new file mode 100644 index 0000000..e34ade2 --- /dev/null +++ b/src/__tests__/unit/stores.test.ts @@ -0,0 +1,37 @@ +// tests/database/stores.test.ts +import { ObjectId } from 'mongodb'; + +import { getDatabase } from '../../database/mongo-common'; +import { deleteStore, updateStore } from '../../database/stores'; + +jest.mock('../../database/mongo-common'); + +const mockCollection = { + deleteOne: jest.fn(), + findOne: jest.fn(), + updateOne: jest.fn(), +}; + +(getDatabase as jest.Mock).mockResolvedValue({ + collection: () => mockCollection, +}); + +describe('stores.ts unit tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return "No store found with that id" if delete count is 0', async () => { + mockCollection.deleteOne.mockResolvedValueOnce({ deletedCount: 0 }); + const response = await deleteStore(new ObjectId().toHexString()); + expect(response).toEqual({ message: 'No store found with that id' }); + }); + + it('should return null if store not found after update', async () => { + mockCollection.updateOne.mockResolvedValueOnce({}); + mockCollection.findOne.mockResolvedValueOnce(null); + + const result = await updateStore(new ObjectId().toHexString(), { name: 'Updated' }); + expect(result).toBeNull(); + }); +}); diff --git a/src/app-common.js b/src/app-common.js deleted file mode 100755 index 0898a10..0000000 --- a/src/app-common.js +++ /dev/null @@ -1,36 +0,0 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const cors = require('cors'); -const helmet = require('helmet'); -const morgan = require('morgan'); -const {startDatabase} = require('./database/mongo-common'); -// alternative: -// const mongo = require('./database/mongo-common'); -// mongo.startDatabase - -// Other entities: Logos, CustomizationOptions, Materials, Patterns - -// Bonus items: , Customer info, etc - -// defining the Express app -const app = express(); - -// adding Helmet to enhance your API's security -app.use(helmet()); - -// using bodyParser to parse JSON bodies into JS objects -// adds a `.body` property to the request so that our -// handler functions can easily work with that incoming data -app.use(bodyParser.json()); - -// enabling CORS for all requests (not very secure) -app.use(cors()); - -// adding morgan to log HTTP requests -app.use(morgan('combined')); - -module.exports = { - app, - startDatabase -} - diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..cd55ad3 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,40 @@ +import express, { Application } from 'express'; +import swaggerUi from 'swagger-ui-express'; + +import swaggerSpec from './documentation/swaggerOptions'; +import compression from './midleware/compression'; +import cors from './midleware/cors'; +import errorHandler from './midleware/errorHandler'; +import helmet from './midleware/helmet'; +import json from './midleware/json'; +import morgan from './midleware/morgan'; +import rateLimiter from './midleware/rateLimiter'; +import authRouter from './routes/authRoute'; +import healthRouter from './routes/healthRoute'; +import notFoundRouter from './routes/notFoundRoute'; +import rootRouter from './routes/rootRoute'; +import storesRouter from './routes/storesRoute'; + +const app: Application = express(); + +// Disable Express identifying header +app.disable('x-powered-by'); + +// Apply Middleware +app.use(helmet); +app.use(json); +app.use(cors); +app.use(morgan); +app.use(errorHandler); +app.use(rateLimiter); +app.use(compression); + +// Set Routes +app.use('/', rootRouter); +app.use('/health', healthRouter); +app.use('/stores', storesRouter); +app.use('/auth', authRouter); +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); +app.use('*', notFoundRouter); + +export default app; diff --git a/src/database/categories.js b/src/database/categories.js deleted file mode 100755 index 62cbce7..0000000 --- a/src/database/categories.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'categories'; - -async function createCategory(logo) { - const database = await getDatabase(); - logo.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(logo); - return insertedId; -} - -async function getCategories() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteCategory(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateCategory(id, logo) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete logo._id; - - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...logo, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createCategory, - getCategories, - deleteCategory, - updateCategory, -}; diff --git a/src/database/logos.js b/src/database/logos.js deleted file mode 100755 index bd29179..0000000 --- a/src/database/logos.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'logos'; - -async function createLogo(logo) { - const database = await getDatabase(); - logo.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(logo); - return insertedId; -} - -async function getLogos() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteLogo(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateLogo(id, logo) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete logo._id; - - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...logo, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createLogo, - getLogos, - deleteLogo, - updateLogo, -}; diff --git a/src/database/mongo-common.js b/src/database/mongo-common.js deleted file mode 100755 index 3630dd2..0000000 --- a/src/database/mongo-common.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - All configuration that is required for a shared mongo server hosted in the cloud - */ -const {MongoClient} = require('mongodb'); - -let database = null; -const mongoDBURL = process.env.MONGO_URL; - -async function startDatabase() { - const connection = await MongoClient.connect(mongoDBURL, {useNewUrlParser: true}); - database = connection.db(); -} - -async function getDatabase() { - if (!database) await startDatabase(); - return database; -} - -module.exports = { - getDatabase, - startDatabase, -}; diff --git a/src/database/mongo-common.ts b/src/database/mongo-common.ts new file mode 100644 index 0000000..f01cac5 --- /dev/null +++ b/src/database/mongo-common.ts @@ -0,0 +1,63 @@ +/** + * Shared MongoDB configuration for cloud-hosted MongoDB instance. + * Documentation: https://mongodb.github.io/node-mongodb-native/6.16/classes/MongoClient.html + */ + +import { Db, MongoClient } from 'mongodb'; +import { MongoMemoryServer } from 'mongodb-memory-server'; + +const mongoDBURL = process.env.MONGO_URL; + +if (!mongoDBURL && process.env.NODE_ENV !== 'test') { + throw new Error('MONGO_URL environment variable is not set'); +} + +let client: MongoClient | null = null; +let database: Db | null = null; +let mongoServer: MongoMemoryServer | null = null; // store reference to in-memory server for shutdown + +const getRightMongoDBURL = async (): Promise => { + const env = process.env.NODE_ENV ?? 'development'; + + if (env === 'test') { + mongoServer = await MongoMemoryServer.create(); + return mongoServer.getUri(); + } + + if (['development', 'production'].includes(env)) { + if (!mongoDBURL) throw new Error('MONGO_URL is not defined'); + return mongoDBURL; + } + + throw new Error(`Unsupported NODE_ENV: ${env}`); +}; + +export async function getDatabase(): Promise { + return database ?? (await startDatabase()); +} + +export async function startDatabase(uri: null | string = null): Promise { + if (client && database) { + return database; + } + + const dbURI = uri ?? (await getRightMongoDBURL()); + + client = new MongoClient(dbURI); + await client.connect(); + database = client.db(); + return database; +} + +export async function stopDatabase(): Promise { + if (client) { + await client.close(); + client = null; + database = null; + } + + if (mongoServer) { + await mongoServer.stop(); + mongoServer = null; + } +} diff --git a/src/database/product-types.js b/src/database/product-types.js deleted file mode 100755 index 6d4dc24..0000000 --- a/src/database/product-types.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'product-types'; - -async function createProductType(productType) { - const database = await getDatabase(); - productType.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(productType); - return insertedId; -} - -async function getProductTypes() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteProductType(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateProductType(id, productType) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete productType._id; - productType.updatedBy = getUserName() - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...productType, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createProductType, - getProductTypes, - deleteProductType, - updateProductType, -}; \ No newline at end of file diff --git a/src/database/products.js b/src/database/products.js deleted file mode 100755 index cf2c6d2..0000000 --- a/src/database/products.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'products'; - -async function createProduct(product) { - const database = await getDatabase(); - product.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(product); - return insertedId; -} - -async function getProducts() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteProduct(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateProduct(id, product) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete product._id; - product.updatedBy = getUserName() - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...product, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createProduct, - getProducts, - deleteProduct, - updateProduct, -}; diff --git a/src/database/stores.js b/src/database/stores.js deleted file mode 100755 index f7694be..0000000 --- a/src/database/stores.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'stores'; - -async function createStore(store) { - const database = await getDatabase(); - store.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(store); - return insertedId; -} - -async function getStores() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteStore(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateStore(id, store) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete store._id; - - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...store, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createStore, - getStores, - deleteStore, - updateStore, -}; diff --git a/src/database/stores.ts b/src/database/stores.ts new file mode 100644 index 0000000..d7f7043 --- /dev/null +++ b/src/database/stores.ts @@ -0,0 +1,73 @@ +import { ObjectId } from 'mongodb'; + +import getUserName from '../utils/git-user-name'; +import { getDatabase } from './mongo-common'; + +// Define the Store interface +export interface Store { + _id: ObjectId; + addedBy?: string; + metadata?: string; + name: string; +} + +const collectionName = 'stores'; + +// Create a Store +async function createStore(store: Store): Promise { + const database = await getDatabase(); + store.addedBy = getUserName(); + + const storeToInsert = { ...store, _id: store._id }; + const { insertedId } = await database.collection(collectionName).insertOne(storeToInsert); + + // Return the store document with the inserted _id + return (await database.collection(collectionName).findOne({ _id: insertedId })) as null | Store; +} + +// Delete a store by id +async function deleteStore(_id: string): Promise<{ message: string }> { + const database = await getDatabase(); + + const result = await database.collection(collectionName).deleteOne({ + _id: new ObjectId(_id), + }); + + if (result.deletedCount === 0) { + return { message: 'No store found with that id' }; + } + + return { message: 'Store deleted' }; +} + +// Get all stores +async function getStores(): Promise { + const database = await getDatabase(); + const stores = await database.collection(collectionName).find({}).toArray(); + return stores.map(store => ({ + _id: store._id, + addedBy: store.addedBy, + name: store.name, + })); +} + +// Update a store +async function updateStore(id: string, store: Partial): Promise { + const database = await getDatabase(); + delete store._id; + + await database.collection(collectionName).updateOne({ _id: new ObjectId(id) }, { $set: store }); + + const updated = await database + .collection(collectionName) + .findOne({ _id: new ObjectId(id) }); + if (!updated) return null; + return { + _id: new ObjectId(updated._id), + addedBy: updated.addedBy, + metadata: updated.metadata, + name: updated.name, + }; +} + +export { createStore, deleteStore, getStores, updateStore }; diff --git a/src/database/variations.js b/src/database/variations.js deleted file mode 100755 index 5fddc4b..0000000 --- a/src/database/variations.js +++ /dev/null @@ -1,56 +0,0 @@ -const {getDatabase} = require('./mongo-common'); -// https://docs.mongodb.com/manual/reference/method/ObjectId/ -const {ObjectID} = require('mongodb'); - -const getUserName = require('git-user-name'); - -// a "collection" in mongo is a lot like a list which is a lot like an Array -const collectionName = 'variations'; - -async function createVariation(variation) { - const database = await getDatabase(); - variation.addedBy = getUserName() - // for `insertOne` info, see https://docs.mongodb.com/manual/reference/method/js-collection/ - const {insertedId} = await database.collection(collectionName).insertOne(variation); - return insertedId; -} - -async function getVariations() { - const database = await getDatabase(); - // `find` https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find - return await database.collection(collectionName).find({}).toArray(); -} - -async function deleteVariation(id) { - const database = await getDatabase(); - // https://docs.mongodb.com/manual/reference/method/ObjectId/ - // for `deleteOne` info see https://docs.mongodb.com/manual/reference/method/js-collection/ - await database.collection(collectionName).deleteOne({ - _id: new ObjectID(id), - }); -} - -async function updateVariation(id, variation) { - const database = await getDatabase(); - - // `delete` is new to you. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete - delete variation._id; - - // https://docs.mongodb.com/manual/reference/method/db.collection.update/ - await database.collection(collectionName).update( - { _id: new ObjectID(id), }, - { - $set: { - ...variation, - }, - }, - ); -} - -// export the functions that can be used by the main app code -module.exports = { - createVariation, - getVariations, - deleteVariation, - updateVariation, -}; \ No newline at end of file diff --git a/src/documentation/swaggerOptions.ts b/src/documentation/swaggerOptions.ts new file mode 100644 index 0000000..a2bb2d2 --- /dev/null +++ b/src/documentation/swaggerOptions.ts @@ -0,0 +1,33 @@ +import swaggerJsdoc, { Options } from 'swagger-jsdoc'; + +import { description, name, version } from '../../package.json'; + +const options: Options = { + apis: ['./src/**/*.ts'], + definition: { + components: { + securitySchemes: { + bearerAuth: { + bearerFormat: 'JWT', + scheme: 'bearer', + type: 'http', + }, + }, + }, + info: { + description, + title: name, + version, + }, + openapi: '3.0.0', + security: [ + { + bearerAuth: [], + }, + ], + }, +}; + +const swaggerSpec = swaggerJsdoc(options); + +export default swaggerSpec; diff --git a/src/http_tests/categories.http b/src/http_tests/categories.http deleted file mode 100755 index 81d700a..0000000 --- a/src/http_tests/categories.http +++ /dev/null @@ -1,27 +0,0 @@ -### Test the categories -### categories are the labels under which products are organized - -GET http://localhost:3001/categories - -### Test creating a category after you build the code for adding a category -POST http://localhost:3001/categories -Content-Type: application/json - -{ - "category": "Men's Apparel" -} - -### Test the PUT which should change a category -PUT http://localhost:3001/categories/5f30519cdd1f2c20488d027c -Content-Type: application/json - -{ - "category": "Unisex Apparel" -} - -### Test the DELETE which should remove a logo - - -DELETE http://localhost:3001/categories/5f30519cdd1f2c20488d027c - - diff --git a/src/http_tests/index.http b/src/http_tests/index.http deleted file mode 100755 index 5753bf9..0000000 --- a/src/http_tests/index.http +++ /dev/null @@ -1,6 +0,0 @@ -### Test the home page endpoint - -GET http://localhost:3001 - -### Test a 404 -GET http://localhost:3001/testing-404 diff --git a/src/http_tests/logos.http b/src/http_tests/logos.http deleted file mode 100755 index 7d33c00..0000000 --- a/src/http_tests/logos.http +++ /dev/null @@ -1,26 +0,0 @@ -### Test the home page endpoint - -GET http://localhost:3001/logos - -### Test creating a logo after you build the code for adding a logo -POST http://localhost:3001/logos -Content-Type: application/json - -{ - "name": "Titleist", - "descirption": "As a child i thought this was pronounced Tit Leist", - "colors": ["Black", "Red"] -} - -### Test the PUT which should change a logo -PUT http://localhost:3001/logos/5f2c919f77f3d857d0566ee6 -Content-Type: application/json - -{ - "name": "Super Logo" -} - -### Test the DELETE which should remove a logo - - -DELETE http://localhost:3001/logos/5f2c920ed615b533c42c9d0d diff --git a/src/http_tests/product-types.http b/src/http_tests/product-types.http deleted file mode 100755 index 283b7ce..0000000 --- a/src/http_tests/product-types.http +++ /dev/null @@ -1,29 +0,0 @@ -### Test the types -### types are the product types -# Product types (or product classes) are groups of products which share the same attributes. - -# Product attributes contain additional product information, e.g. ISBN, UPC, Brand, which is displayed in storefront and included in product feeds when exporting to marketplaces like Google Shopping, eBay, Amazon ads etc. - -GET http://localhost:3001/product-types - -### Test creating a type - -POST http://localhost:3001/product-types -Content-Type: application/json - -{ - "Type": "Polo Shirt" -} - -### Test the PUT which should change a product type - -PUT http://localhost:3001/product-types/5f30598df68fc848702a50f5 -Content-Type: application/json - -{ - "Type": "Yolo Shirt" -} - -### Test the DELETE which should delete a product type - -DELETE http://localhost:3001/product-types/5f30598df68fc848702a50f5 \ No newline at end of file diff --git a/src/http_tests/products.http b/src/http_tests/products.http deleted file mode 100755 index 2c1e39f..0000000 --- a/src/http_tests/products.http +++ /dev/null @@ -1,25 +0,0 @@ -### Test the home page endpoint - -GET http://localhost:3001/products - -### Test creating a product -POST http://localhost:3001/products -Content-Type: application/json - -{ - "title": "Beer Coozie", - "description": "The most important part of any round of golf" -} - -### Test the PUT which should change a product -### id of 5f2b54de320478eb4b1603eb is temporary while server is running -PUT http://localhost:3001/products/5f2c7a7832d9964f9d97826e -Content-Type: application/json - -{ - "title": "Amazing UV Shirt" -} - -### Test the DELETE which should delete a product -DELETE http://localhost:3001/products/5f2c7a7832d9964f9d97826e - diff --git a/src/http_tests/variations.http b/src/http_tests/variations.http deleted file mode 100755 index 5459d19..0000000 --- a/src/http_tests/variations.http +++ /dev/null @@ -1,25 +0,0 @@ -### Test the variations -### variations are the different styles that can be applied to a product - -GET http://localhost:3001/variations - -### Test creating a variation - -POST http://localhost:3001/variations -Content-Type: application/json - -{ - "variation": "Red" -} - -### Test the PUT which should change a variation - -PUT http://localhost:3001/variations/5f305ed5a159c55254bb16d5 -Content-Type: application/json - -{ - "variation": "Dark Red" -} - -### Test DELETE which should delete a variation -DELETE http://localhost:3001/variations/5f305ed5a159c55254bb16d5 \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100755 index 1d3d126..0000000 --- a/src/index.js +++ /dev/null @@ -1,54 +0,0 @@ -// This file is: ./src/index.js - -//importing the dependencies -const { - app, - startDatabase -} = require('./app-common.js'); -const { request } = require('express'); - -// support production deployment on a port configured on the hosting server -// default to the dev port number otherwise -const port = process.env.PORT || 3001; - -const MONGO_URL = process.env.MONGO_URL; - -// connect to our database then start the web server -// https://www.mongodb.com/ -if (MONGO_URL) { - - // endpoint to return top level api - // much like a switch statement - app.get('/', async (req, res) => { - res.send({ - message: "Storefront API. See documentation for use." - }); - }); - - app.use('/products', require('./routes/productsRoutes')) - app.use('/logos', require('./routes/logosRoutes')) - app.use('/stores', require('./routes/storesRoutes')) - app.use('/categories', require('./routes/categoriesRoutes')) - app.use('/product-types', require('./routes/product-typesRoutes')) - app.use('/variations', require('./routes/variationsRoutes')) - - startDatabase().then(async () => { - // `then` start the web server after the database starts - app.listen(port, async () => { - console.log(`Web server has started on port ${port}`); - }); - }); -} else { - - // endpoint to return top level api - // much like a switch statement - app.all('*', async (req, res) => { - res.send({ - message: "MONGO_URL not configured. See documentation." - }); - }); - - app.listen(port, async () => { - console.log(`Web server has started on port ${port}`); - }); -} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c457226 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,66 @@ +// src/server.ts +import 'dotenv/config'; +import { Server } from 'http'; + +import app from './app'; +import { startDatabase, stopDatabase } from './database/mongo-common'; + +const PORT: number = parseInt(process.env.PORT ?? '3001', 10); +const MONGO_URL: string | undefined = process.env.MONGO_URL; + +let server: Server | undefined; + +function gracefulShutdown(signal: string): void { + console.log(`\nReceived ${signal}, shutting down...`); + if (server) { + server.close(() => { + console.log('HTTP server closed'); + stopDatabase() + .then(() => { + console.log('Database connection closed'); + process.exit(0); + }) + .catch((err: unknown) => { + console.error('Error during shutdown:', err); + process.exit(1); + }); + }); + } else { + process.exit(0); + } +} + +async function startServer(): Promise { + if (!MONGO_URL) { + app.all('*', (_, res) => { + res.status(500).send({ + message: 'MONGO_URL not configured. See documentation.', + }); + }); + + server = app.listen(PORT, () => { + console.log(`Server running without DB on port ${String(PORT)}`); + }); + + return; + } + + try { + await startDatabase(); + + server = app.listen(PORT, () => { + console.log(`Server started on port ${String(PORT)}`); + }); + } catch (err: unknown) { + console.error('Failed to start database:', err); + process.exit(1); + } +} + +['SIGINT', 'SIGTERM'].forEach(signal => { + process.on(signal, () => { + gracefulShutdown(signal); + }); +}); + +void startServer(); diff --git a/src/midleware/authMiddleware.ts b/src/midleware/authMiddleware.ts new file mode 100644 index 0000000..4bd2016 --- /dev/null +++ b/src/midleware/authMiddleware.ts @@ -0,0 +1,29 @@ +import { NextFunction, Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET; + +if (!JWT_SECRET) { + throw new Error('JWT_SECRET is not defined'); +} + +const authMiddleware = (req: Request, res: Response, next: NextFunction): void => { + const authHeader = req.headers.authorization; + const token = authHeader?.split(' ')[1]; + + if (!token) { + res.status(401).json({ message: 'No token provided' }); + return; + } + + jwt.verify(token, JWT_SECRET, err => { + if (err) { + res.status(401).json({ message: 'Invalid or expired token' }); + return; + } + + next(); + }); +}; + +export default authMiddleware; diff --git a/src/midleware/compression.ts b/src/midleware/compression.ts new file mode 100644 index 0000000..968c3ae --- /dev/null +++ b/src/midleware/compression.ts @@ -0,0 +1,5 @@ +import compression from 'compression'; + +const compressionMiddleware = compression(); + +export default compressionMiddleware; diff --git a/src/midleware/cors.ts b/src/midleware/cors.ts new file mode 100644 index 0000000..04f2ffb --- /dev/null +++ b/src/midleware/cors.ts @@ -0,0 +1,29 @@ +import cors, { CorsOptions } from 'cors'; + +// Load environment variables with fallback values +const ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS ?? ''; +const ALLOWED_METHODS = process.env.CORS_ALLOWED_METHODS ?? 'GET,POST,PUT,DELETE'; +const ALLOWED_HEADERS = process.env.CORS_ALLOWED_HEADERS ?? 'Content-Type,Authorization'; + +// Type-safe CORS options +const corsOptions: CorsOptions = { + allowedHeaders: ALLOWED_HEADERS.split(',') + .map(h => h.trim()) + .filter(Boolean), + credentials: true, + methods: ALLOWED_METHODS.split(',') + .map(m => m.trim()) + .filter(Boolean), + origin: (origin: string | undefined, callback: (error: Error | null, allow: boolean) => void) => { + const allowedOrigins = ALLOWED_ORIGINS.split(',') + .map(o => o.trim()) + .filter(Boolean); + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS'), false); + } + }, +}; + +export default cors(corsOptions); diff --git a/src/midleware/errorHandler.ts b/src/midleware/errorHandler.ts new file mode 100644 index 0000000..71c76cc --- /dev/null +++ b/src/midleware/errorHandler.ts @@ -0,0 +1,14 @@ +import { NextFunction, Request, Response } from 'express'; + +// using _ to indicate that the parameter is not used +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction): void => { + const response = { + message: 'Internal Server Error', + ...(process.env.NODE_ENV !== 'production' && { error: err.message }), + }; + + res.status(500).json(response); +}; + +export default errorHandler; diff --git a/src/midleware/helmet.ts b/src/midleware/helmet.ts new file mode 100644 index 0000000..fc6aab0 --- /dev/null +++ b/src/midleware/helmet.ts @@ -0,0 +1,18 @@ +import { RequestHandler } from 'express'; +import helmet from 'helmet'; + +const configureHelmet = (): RequestHandler => // Explicitly type as RequestHandler + helmet({ + contentSecurityPolicy: false, + expectCt: false, + hidePoweredBy: true, + hsts: { + includeSubDomains: true, + maxAge: 63072000, // 2 years + preload: true, + }, + noSniff: true, + referrerPolicy: { policy: 'no-referrer' }, + }); + +export default configureHelmet(); diff --git a/src/midleware/json.ts b/src/midleware/json.ts new file mode 100644 index 0000000..4662242 --- /dev/null +++ b/src/midleware/json.ts @@ -0,0 +1,16 @@ +import { json } from 'express'; + +// Custom JSON middleware configuration +const configuredJson = json({ + limit: '1mb', + reviver: (key: string, value: unknown): unknown => { + if (key === 'date' && typeof value === 'string') { + return new Date(value); + } + return value; + }, + strict: false, + type: ['application/json', 'application/vnd.api+json'], +}); + +export default configuredJson; diff --git a/src/midleware/morgan.ts b/src/midleware/morgan.ts new file mode 100644 index 0000000..85f2240 --- /dev/null +++ b/src/midleware/morgan.ts @@ -0,0 +1,6 @@ +import morgan from 'morgan'; + +// Morgan configuration +const configureMorgan = morgan('dev'); + +export default configureMorgan; diff --git a/src/midleware/rateLimiter.ts b/src/midleware/rateLimiter.ts new file mode 100644 index 0000000..6a3351a --- /dev/null +++ b/src/midleware/rateLimiter.ts @@ -0,0 +1,8 @@ +import rateLimit from 'express-rate-limit'; + +const limiter = rateLimit({ + max: 100, // limit each IP to 100 requests per window + windowMs: 15 * 60 * 1000, // 15 minutes +}); + +export default limiter; diff --git a/src/routes/authRoute.ts b/src/routes/authRoute.ts new file mode 100644 index 0000000..08261e5 --- /dev/null +++ b/src/routes/authRoute.ts @@ -0,0 +1,207 @@ +import bcrypt from 'bcrypt'; +import { Request, Response, Router } from 'express'; +import jwt from 'jsonwebtoken'; +import { ObjectId } from 'mongodb'; + +import { getDatabase } from '../database/mongo-common'; +import authMiddleware from '../midleware/authMiddleware'; + +const router: Router = Router(); +const JWT_SECRET = process.env.JWT_SECRET; + +if (!JWT_SECRET) { + throw new Error('JWT_SECRET is not defined'); +} + +interface User { + _id?: ObjectId; + email: string; + password: string; +} + +// register endpoint +/** + * @swagger + * /auth/register: + * post: + * summary: Register a new user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * format: email + * password: + * type: string + * format: password + * responses: + * 201: + * description: User registered successfully + * 400: + * description: Email and password are required + * 409: + * description: Email already taken + * 500: + * description: Internal server error + */ +router.post('/register', async (req: Request, res: Response): Promise => { + const { email, password } = req.body as User; + + if (!email || !password) { + res.status(400).json({ message: 'Email and password are required' }); + return; + } + + try { + const db = await getDatabase(); + const usersCollection = db.collection('users'); + + const existingUser = await usersCollection.findOne({ email }); + if (existingUser) { + res.status(409).json({ message: 'Email already taken' }); + return; + } + + const hashedPassword = await bcrypt.hash(password, 10); + await usersCollection.insertOne({ email, password: hashedPassword }); + + res.status(201).json({ message: 'User registered successfully' }); + return; + } catch (error) { + console.error('Error registering user:', error); + res.status(500).json({ message: 'Internal server error' }); + return; + } +}); + +// Login endpoint +/** + * @swagger + * /auth/login: + * post: + * summary: Log in an existing user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * format: email + * password: + * type: string + * format: password + * responses: + * 200: + * description: User logged in successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * token: + * type: string + * 401: + * description: Invalid credentials + * 500: + * description: Internal server error + */ + +router.post('/login', async (req: Request, res: Response): Promise => { + const { email, password } = req.body as User; + + try { + const db = await getDatabase(); + const usersCollection = db.collection('users'); + + const user = await usersCollection.findOne({ email }); + if (!user) { + res.status(401).json({ message: 'Invalid credentials' }); + return; + } + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + res.status(401).json({ message: 'Invalid credentials' }); + } + + const token = jwt.sign({ email: user.email, userId: user._id }, JWT_SECRET, { + expiresIn: '1h', + }); + + res.status(200).json({ message: 'User logged in successfully', token }); + } catch (error) { + console.error('Error logging in:', error); + res.status(500).json({ message: 'Internal server error' }); + } +}); + +// responds with user data +/** + * @swagger + * /auth/me: + * get: + * summary: Get current authenticated user info + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Returns user email + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * 401: + * description: No token provided or unauthorized + * 500: + * description: Internal server error + */ + +router.get('/me', authMiddleware, async (req: Request, res: Response): Promise => { + const token = req.headers.authorization?.split(' ')[1]; + + if (!token) { + res.status(401).json({ message: 'No token provided' }); + return; + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as { email: string; userId: string }; + const db = await getDatabase(); + const usersCollection = db.collection('users'); + + const user = await usersCollection.findOne({ _id: new ObjectId(decoded.userId) }); + + if (!user) { + res.status(201).json({ message: 'User not found' }); + return; + } + + res.status(200).json({ email: user.email }); + } catch (error) { + console.error('Error fetching user data:', error); + res.status(500).json({ message: 'Internal server error' }); + } +}); + +export default router; diff --git a/src/routes/categoriesRoutes.js b/src/routes/categoriesRoutes.js deleted file mode 100755 index 70b57d2..0000000 --- a/src/routes/categoriesRoutes.js +++ /dev/null @@ -1,30 +0,0 @@ -const router = require('express').Router(); -const {deleteCategory, updateCategory, createCategory, getCategories} = require('../database/categories'); - -router.get('/', async (req, res) => { - res.send(await getCategories()); -}); - -router.post('/', async (apiRequest, apiResponse) => { - const newCategory = apiRequest.body; - await createCategory(newCategory); - apiResponse.send({ - message: 'New Category created.', - allCategories: await getCategories(), - }); -}); - -router.delete('/:categoryId', async (apiRequest, apiResponse) => { - await deleteCategory(apiRequest.params.categoryId); - apiResponse.send({ message: 'Category deleted.' }); -}); - -// endpoint to update a Category -router.put('/:id', async (apiRequest, apiResponse) => { - const updatedCategory = apiRequest.body; - console.log({ updatedCategory}) - await updateCategory(apiRequest.params.id, updatedCategory); - apiResponse.send({ message: 'Category updated.' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/healthRoute.ts b/src/routes/healthRoute.ts new file mode 100644 index 0000000..b2bee86 --- /dev/null +++ b/src/routes/healthRoute.ts @@ -0,0 +1,40 @@ +import { Request, Response, Router } from 'express'; + +const router: Router = Router(); +/** + * @swagger + * /health: + * get: + * tags: + * - Health + * summary: Health check + * description: Returns service status, current timestamp, and uptime. + * responses: + * 200: + * description: Service is running + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: OK + * timestamp: + * type: string + * format: date-time + * example: "2025-05-17T12:34:56.789Z" + * uptime: + * type: number + * description: Time in seconds since the server started + * example: 123.456 + */ +router.get('/', (req: Request, res: Response): void => { + res.status(200).json({ + status: 'OK', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + }); +}); + +export default router; diff --git a/src/routes/logosRoutes.js b/src/routes/logosRoutes.js deleted file mode 100755 index 4331801..0000000 --- a/src/routes/logosRoutes.js +++ /dev/null @@ -1,31 +0,0 @@ - -const router = require('express').Router(); -const {deleteLogo, updateLogo, createLogo, getLogos} = require('../database/logos'); - -router.get('/', async (req, res) => { - res.send(await getLogos()); -}); - -router.post('/', async (apiRequest, apiResponse) => { - const newLogo = apiRequest.body; - await createLogo(newLogo); - apiResponse.send({ - message: 'New logo created.', - allLogos: await getLogos(), - }); -}); - -router.delete('/:logoId', async (apiRequest, apiResponse) => { - await deleteLogo(apiRequest.params.logoId); - apiResponse.send({ message: 'logo deleted.' }); -}); - -// endpoint to update a logo -router.put('/:id', async (apiRequest, apiResponse) => { - const updatedLogo = apiRequest.body; - console.log({ updateLogo }) - await updateLogo(apiRequest.params.id, updatedLogo); - apiResponse.send({ message: 'logo updated.' }); -}); - -module.exports = router; diff --git a/src/routes/notFoundRoute.ts b/src/routes/notFoundRoute.ts new file mode 100644 index 0000000..258aef3 --- /dev/null +++ b/src/routes/notFoundRoute.ts @@ -0,0 +1,14 @@ +import { Request, Response, Router } from 'express'; + +const router: Router = Router(); + +router.all('*', (req: Request, res: Response) => { + res.status(404).json({ + endpoint: req.originalUrl, + message: 'Route not found', + method: req.method, + timestamp: new Date().toISOString(), + }); +}); + +export default router; diff --git a/src/routes/product-typesRoutes.js b/src/routes/product-typesRoutes.js deleted file mode 100755 index 70dc55e..0000000 --- a/src/routes/product-typesRoutes.js +++ /dev/null @@ -1,38 +0,0 @@ -const router = require('express').Router(); -const {deleteProductType, updateProductType, createProductType, getProductTypes} = require('../database/product-types'); - -router.get('/', async (apiRequest, apiResponse) => { - apiResponse.send(await getProductTypes()); -}); - -// we name our parameters apiRequest and apiResponse here but -// there is no strong reason these variables could not be named `req` and `res` or `request` and `response` -// the reason for this naming is so we are thinking about "api" tonight -router.post('/', async (apiRequest, apiResponse) => { - const newProductType = apiRequest.body; - await createProductType(newProductType); - apiResponse.send({ - message: 'New product type created.', - allProductTypes: await getProductTypes(), - thanks: true - }); -}); - -// endpoint to delete a product -router.delete('/:productTypeId', async (apiRequest, apiResponse) => { - await deleteProductType(apiRequest.params.productTypeId); - apiResponse.send({ message: 'Product type deleted.' }); -}); - -// endpoint to update a product -router.put('/:productTypeId', async (apiRequest, apiResponse) => { - const updatedProductType = apiRequest.body; - console.log({ updatedProductType}) - await updateProductType(apiRequest.params.productTypeId, updatedProductType); - apiResponse.send({ message: 'Product type updated.' }); -}); - -module.exports = router; - - - diff --git a/src/routes/productsRoutes.js b/src/routes/productsRoutes.js deleted file mode 100755 index b50ca1d..0000000 --- a/src/routes/productsRoutes.js +++ /dev/null @@ -1,39 +0,0 @@ - -const router = require('express').Router(); -const {deleteProduct, updateProduct, createProduct, getProducts} = require('../database/products'); - -router.get('/', async (apiRequest, apiResponse) => { - apiResponse.send(await getProducts()); -}); - -// we name our parameters apiRequest and apiResponse here but -// there is no strong reason these variables could not be named `req` and `res` or `request` and `response` -// the reason for this naming is so we are thinking about "api" tonight -router.post('/', async (apiRequest, apiResponse) => { - const newProduct = apiRequest.body; - await createProduct(newProduct); - apiResponse.send({ - message: 'New product created.', - allProducts: await getProducts(), - thanks: true - }); -}); - -// endpoint to delete a product -router.delete('/:productId', async (apiRequest, apiResponse) => { - await deleteProduct(apiRequest.params.productId); - apiResponse.send({ message: 'Product deleted.' }); -}); - -// endpoint to update a product -router.put('/:id', async (apiRequest, apiResponse) => { - const updatedProduct = apiRequest.body; - console.log({ updatedProduct}) - await updateProduct(apiRequest.params.id, updatedProduct); - apiResponse.send({ message: 'Product updated.' }); -}); - -module.exports = router; - - - diff --git a/src/routes/rootRoute.ts b/src/routes/rootRoute.ts new file mode 100644 index 0000000..e140900 --- /dev/null +++ b/src/routes/rootRoute.ts @@ -0,0 +1,9 @@ +import { Request, Response, Router } from 'express'; + +const router: Router = Router(); + +router.get('/', (_: Request, res: Response): void => { + res.redirect('/health'); +}); + +export default router; diff --git a/src/routes/storesRoute.ts b/src/routes/storesRoute.ts new file mode 100644 index 0000000..dc0270c --- /dev/null +++ b/src/routes/storesRoute.ts @@ -0,0 +1,31 @@ +import { Request, Response, Router } from 'express'; + +import { createStore, deleteStore, getStores, Store, updateStore } from '../database/stores'; + +const router: Router = Router(); + +router.get('/', async (req: Request, res: Response): Promise => { + const stores = await getStores(); + res.json(stores); +}); + +router.post('/', async (req: Request, res: Response): Promise => { + const newStore = req.body as Store; + const createdStore = await createStore(newStore); + res.status(201).json(createdStore); +}); + +router.delete('/:_id', async (req: Request, res: Response): Promise => { + const { _id } = req.params; + const result = await deleteStore(_id); + res.json(result); +}); + +// Endpoint to update a Store +router.put('/:_id', async (req: Request, res: Response): Promise => { + const updatedStore = req.body as Store; + const result = await updateStore(req.params._id, updatedStore); + res.json(result); +}); + +export default router; diff --git a/src/routes/storesRoutes.js b/src/routes/storesRoutes.js deleted file mode 100755 index f5671a1..0000000 --- a/src/routes/storesRoutes.js +++ /dev/null @@ -1,31 +0,0 @@ - -const router = require('express').Router(); -const {deleteStore, updateStore, createStore, getStores} = require('../database/stores'); - -router.get('/', async (req, res) => { - res.send(await getStores()); -}); - -router.post('/', async (apiRequest, apiResponse) => { - const newStore = apiRequest.body; - await createStore(newStore); - apiResponse.send({ - message: 'New Store created.', - allStores: await getStores(), - }); -}); - -router.delete('/:storeId', async (apiRequest, apiResponse) => { - await deleteStore(apiRequest.params.storeId); - apiResponse.send({ message: 'Store deleted.' }); -}); - -// endpoint to update a Store -router.put('/:storeId', async (apiRequest, apiResponse) => { - const updatedStore = apiRequest.body; - console.log({ updateStore }) - await updateStore(apiRequest.params.storeId, updatedStore); - apiResponse.send({ message: 'Store updated.' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/variationsRoutes.js b/src/routes/variationsRoutes.js deleted file mode 100755 index c1b2f8b..0000000 --- a/src/routes/variationsRoutes.js +++ /dev/null @@ -1,30 +0,0 @@ -const router = require('express').Router(); -const {deleteVariation, updateVariation, createVariation, getVariations} = require('../database/variations'); - -router.get('/', async (req, res) => { - res.send(await getVariations()); -}); - -router.post('/', async (apiRequest, apiResponse) => { - const newVariation = apiRequest.body; - await createVariation(newVariation); - apiResponse.send({ - message: 'New variation created.', - allStores: await getVariations(), - }); -}); - -router.delete('/:variationId', async (apiRequest, apiResponse) => { - await deleteVariation(apiRequest.params.variationId); - apiResponse.send({ message: 'Variation deleted.' }); -}); - -// endpoint to update a Store -router.put('/:variationId', async (apiRequest, apiResponse) => { - const updatedVariation = apiRequest.body; - console.log({ updateVariation }) - await updateVariation(apiRequest.params.variationId, updatedVariation); - apiResponse.send({ message: 'Variation updated.' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/utils/git-user-name.ts b/src/utils/git-user-name.ts new file mode 100644 index 0000000..7dc31b0 --- /dev/null +++ b/src/utils/git-user-name.ts @@ -0,0 +1,18 @@ +import { execSync } from 'child_process'; + +function getGitUserName(): string { + if (process.env.GITHUB_ACTIONS === 'true') { + const githubActor = process.env.GITHUB_ACTOR; + return githubActor ?? 'github-actions'; + } else { + try { + const name = execSync('git config --get user.name', { encoding: 'utf8' }).trim(); + return name || 'unknown'; + } catch (err) { + console.error('Error getting git user name:', err); + return 'unknown'; + } + } +} + +export default getGitUserName; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4cf4576 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES6", // Use ES6 as the output target for better module handling + "module": "CommonJS", // Use CommonJS modules since Node.js uses them + "moduleResolution": "node", // Use Node module resolution for imports + "esModuleInterop": true, // Allow default imports from non-ES modules + "skipLibCheck": true, // Skip type checking of declaration files for faster builds + "strict": true, // Enable strict type-checking options + "forceConsistentCasingInFileNames": true, // Enforce consistent casing in file names + "outDir": "./dist", // Specify where compiled JavaScript files go + "baseUrl": ".", // Base URL to resolve non-relative modules + "types": ["node", "jest"], // Include types for Node.js and Jest for testing + "allowJs": true, // Allow JavaScript files to be included in the compilation + "resolveJsonModule": true // Allow importing of JSON files + }, + "include": [ + "src/**/*.ts", // Include all TS files in the `src` folder + "tests/**/*.ts" // Include test files in a separate `tests` folder + ], + "exclude": [ + "node_modules", // Exclude node_modules + "dist" // Exclude the `dist` folder where compiled JS files are stored + ] +}