diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6a9870968e93df0c20c5b054fc6afb5fabe2c78e..88c620dd790fcc45156cb8fbf7f76512055c3bee 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,5 +64,5 @@ demo:now: url: $NOW_URL script: - npm i -g --unsafe-perm now - - cd ${HOME} - - npm run deploy_now \ No newline at end of file + - cd ${HOME}/now + - now --public --docker --token $NOW_TOKEN -e AWS_S3_ACCESS_KEY=$AWS_S3_ACCESS_KEY -e AWS_S3_SECRET_KEY=$AWS_S3_SECRET_KEY -e AWS_S3_REGION=$AWS_S3_REGION -e AWS_S3_BUCKET=$AWS_S3_BUCKET -e AWS_SES_SECRET_KEY=$AWS_SES_SECRET_KEY -e AWS_SES_ACCESS_KEY=$AWS_SES_ACCESS_KEY -e AWS_SES_REGION=$AWS_SES_REGION -e EMAIL_SENDER=$EMAIL_SENDER -e secret=$SECRET \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 698c88f458e85c2f87bdb64b4f9722ea6a2c6bfc..6b7916c6b878cd4d86b238fdca907fe1ea0aefbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM xpub/xpub:base COPY package.json yarn.lock ./ COPY lerna.json .babelrc .eslintignore .eslintrc .prettierrc .stylelintignore .stylelintrc ./ COPY packages packages +COPY now now RUN [ "yarn", "config", "set", "workspaces-experimental", "true" ] diff --git a/package.json b/package.json index d3ba020bbae08ea43170d63411b295dfcbc9a6bd..079e1d213d174a5d15d3b0943eded9c29c5e2881 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,7 @@ "lint:style": "stylelint packages/**/*.scss packages/**/*.css", "precommit": "lint-staged", "styleguide": "lerna run styleguide", - "test": "lerna run test", - "deploy_now": "cd now && now --public --docker --token $NOW_TOKEN -e AWS_S3_ACCESS_KEY=$AWS_S3_ACCESS_KEY -e AWS_S3_SECRET_KEY=$AWS_S3_SECRET_KEY -e AWS_S3_REGION=$AWS_S3_REGION -e AWS_S3_BUCKET=$AWS_S3_BUCKET -e AWS_SES_SECRET_KEY=$AWS_SES_SECRET_KEY -e AWS_SES_ACCESS_KEY=$AWS_SES_ACCESS_KEY -e AWS_SES_REGION=$AWS_SES_REGION -e EMAIL_SENDER=$EMAIL_SENDER -e secret=$SECRET" + "test": "lerna run test" }, "lint-staged": { "*.js": [ diff --git a/packages/component-local-aws/index.js b/packages/component-local-aws/index.js new file mode 100644 index 0000000000000000000000000000000000000000..140b65ac0183438e989b7e7f552b3498baa32820 --- /dev/null +++ b/packages/component-local-aws/index.js @@ -0,0 +1,5 @@ +require('dotenv').config() + +module.exports = { + backend: () => app => require('./src/FileBackend')(app), +} diff --git a/packages/component-local-aws/package.json b/packages/component-local-aws/package.json new file mode 100644 index 0000000000000000000000000000000000000000..b38c67c0a3e07d5a3d4596db3ad356a20eb1c2ca --- /dev/null +++ b/packages/component-local-aws/package.json @@ -0,0 +1,18 @@ +{ + "name": "component-local-aws", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "aws-sdk": "^2.185.0", + "body-parser": "^1.17.2", + "multer": "^1.3.0", + "multer-s3": "^2.7.0", + "node-mocks-http": "^1.6.6", + "nodemailer": "^4.4.2" + }, + "peerDependencies": { + "@pubsweet/logger": "^0.0.1", + "pubsweet-server": "^1.0.1" + } +} diff --git a/packages/component-local-aws/src/FileBackend.js b/packages/component-local-aws/src/FileBackend.js new file mode 100644 index 0000000000000000000000000000000000000000..b7866aa58574c21c3773ca443041bda361270cfe --- /dev/null +++ b/packages/component-local-aws/src/FileBackend.js @@ -0,0 +1,45 @@ +const _ = require('lodash') +const AWS = require('aws-sdk') +const config = require('config') + +const s3Config = _.get(config, 'pubsweet-component-aws-s3') + +const FileBackend = app => { + const authBearer = app.locals.passport.authenticate('bearer', { + session: false, + }) + AWS.config.update({ + secretAccessKey: s3Config.secretAccessKey, + accessKeyId: s3Config.accessKeyId, + region: s3Config.region, + }) + const s3 = new AWS.S3() + const upload = require('./middeware/upload').setupMulter(s3) + + app.post( + '/api/files', + authBearer, + upload.single('file'), + require('./routeHandlers/postFile'), + ) + + app.get( + '/api/files/:fragmentId/:fileId', + authBearer, + require('./routeHandlers/getSignedUrl')(s3, s3Config), + ) + + app.get( + '/api/files/:fragmentId', + authBearer, + require('./routeHandlers/zipFiles')(s3, s3Config), + ) + + app.delete( + '/api/files/:fragmentId/:fileId', + authBearer, + require('./routeHandlers/deleteFile')(s3, s3Config), + ) +} + +module.exports = FileBackend diff --git a/packages/component-local-aws/src/FileBackend.test.js b/packages/component-local-aws/src/FileBackend.test.js new file mode 100644 index 0000000000000000000000000000000000000000..95a026ff5866440953da6c75cee6042589d1abe1 --- /dev/null +++ b/packages/component-local-aws/src/FileBackend.test.js @@ -0,0 +1,188 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const httpMocks = require('node-mocks-http') + +const zipHandler = require('./routeHandlers/zipFiles') + +describe('ValidateFile for multer fileFilter', () => { + it('should return TRUE when fileType is supplementary', () => { + const validateFile = require('./middeware/upload').validateFile( + ...buildValidateFileParams('supplementary', 'image/png'), + ) + expect(validateFile).toBe(true) + }) + it('should return TRUE when fileType is manuscripts or coverLetter and the file is either Word Doc or PDF', () => { + const randFileType = getRandValueFromArray(['manuscripts', 'coverLetter']) + const randMimeType = getRandValueFromArray([ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ]) + + const validateFile = require('./middeware/upload').validateFile( + ...buildValidateFileParams(randFileType, randMimeType), + ) + expect(validateFile).toBe(true) + }) + it('should return FALSE when fileType is manuscripts or coverLetter and the file is neither Word Doc or PDF', () => { + const randFileType = getRandValueFromArray(['manuscripts', 'coverLetter']) + const randMimeType = getRandValueFromArray([ + 'text/plain', + 'text/html', + 'image/jpeg', + 'image/png', + ]) + + const validateFile = require('./middeware/upload').validateFile( + ...buildValidateFileParams(randFileType, randMimeType), + ) + expect(validateFile).toBe(false) + }) +}) + +describe('Upload file route handler', () => { + it('should return success when the file passed validation', async () => { + const file = { + key: '123abc', + originalname: 'file.txt', + size: 128, + } + const req = httpMocks.createRequest({ + file, + }) + const res = httpMocks.createResponse() + await require('./routeHandlers/postFile')(req, res) + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.id).toEqual(file.key) + expect(data.name).toEqual(file.originalname) + expect(data.size).toEqual(file.size) + }) + it('should return an error when the file failed validation', async () => { + const req = httpMocks.createRequest({ + fileValidationError: 'Only Word documents and PDFs are allowed', + }) + const res = httpMocks.createResponse() + await require('./routeHandlers/postFile')(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual(req.fileValidationError) + }) +}) + +describe('Zip files endpoint', () => { + const testObject = { + Key: 'KeyToMyHeart', + Body: Buffer.alloc(10), + Metadata: { + filetype: 'manuscripts', + filename: 'test.txt', + }, + } + const mockBucket = { + RandomKey: { + Contents: [testObject, testObject], + }, + } + + const s3 = {} + const mocks = {} + let mockArchiver = null + const s3Config = { + bucket: 'TestBucket', + } + beforeAll(() => { + s3.getObject = jest.fn((params, cb) => { + cb(null, testObject) + }) + + s3.listObjects = jest.fn((params, cb) => { + cb(null, mockBucket[params.Prefix]) + }) + mocks.pipe = jest.fn() + mocks.append = jest.fn() + mocks.finalize = jest.fn() + mocks.attachment = jest.fn() + + mockArchiver = jest.fn(p => ({ + pipe: mocks.pipe, + append: mocks.append, + finalize: mocks.finalize, + })) + }) + afterEach(() => { + s3.getObject.mockClear() + s3.listObjects.mockClear() + mocks.pipe.mockClear() + mocks.append.mockClear() + mocks.attachment.mockClear() + }) + it(`no zipping if no files found`, async () => { + const zipFiles = zipHandler(s3, s3Config, mockArchiver) + const request = httpMocks.createRequest({ + method: 'GET', + params: { + fragmentId: 'NotFoundKey', + }, + }) + const response = httpMocks.createResponse() + response.attachment = mocks.attachment + + await zipFiles(request, response) + + expect(s3.listObjects.mock.calls).toHaveLength(1) + expect(s3.getObject.mock.calls).toHaveLength(0) + expect(mocks.attachment.mock.calls).toHaveLength(0) + expect(mocks.append.mock.calls).toHaveLength(0) + expect(mocks.pipe.mock.calls).toHaveLength(0) + + const responseData = JSON.parse(response._getData()) + + expect(responseData.message).toEqual( + `No files found for the requested manuscript.`, + ) + expect(response._getStatusCode()).toEqual(204) + }) + it('zips all the files', async () => { + const zipFiles = zipHandler(s3, s3Config, mockArchiver) + + const request = httpMocks.createRequest({ + method: 'GET', + params: { + fragmentId: 'RandomKey', + }, + }) + const response = httpMocks.createResponse() + response.attachment = mocks.attachment + + await zipFiles(request, response) + + expect(s3.listObjects.mock.calls).toHaveLength(1) + expect(s3.getObject.mock.calls).toHaveLength(2) + expect(mocks.attachment.mock.calls).toHaveLength(1) + expect(mocks.attachment.mock.calls[0][0]).toEqual('RandomKey-archive.zip') + expect(mocks.append.mock.calls).toHaveLength(2) + expect(mocks.pipe.mock.calls).toHaveLength(1) + expect(response._getStatusCode()).toEqual(200) + }) +}) + +const getRandValueFromArray = arr => arr[Math.floor(Math.random() * arr.length)] + +const buildValidateFileParams = (fileType, mimetype) => { + const req = { + body: { + fileType, + }, + } + const file = { + mimetype, + } + const cb = (p1, p2) => { + if (p2 === true) return true + return false + } + + return [req, file, cb] +} diff --git a/packages/component-local-aws/src/middeware/upload.js b/packages/component-local-aws/src/middeware/upload.js new file mode 100644 index 0000000000000000000000000000000000000000..f46f4176621cfb62e3cfec3598a3ae0219b588e2 --- /dev/null +++ b/packages/component-local-aws/src/middeware/upload.js @@ -0,0 +1,53 @@ +const Joi = require('joi') +const uuid = require('uuid') +const config = require('config') +const multer = require('multer') +const { get } = require('lodash') +const multerS3 = require('multer-s3') + +const s3Config = get(config, 'pubsweet-component-aws-s3') +const uploadValidations = require(s3Config.validations) + +const setupMulter = s3 => { + const upload = multer({ + storage: multerS3({ + s3, + bucket: s3Config.bucket, + contentType: (req, file, cb) => { + cb(null, file.mimetype) + }, + metadata: (req, file, cb) => { + cb(null, { + FileType: get(req, 'body.fileType'), + FileName: get(file, 'originalname'), + }) + }, + key: (req, file, cb) => { + const fileKey = `${req.body.fragmentId}/${uuid.v4()}` + cb(null, fileKey) + }, + }), + fileFilter: (req, file, cb) => validateFile(req, file, cb), + }) + + return upload +} + +const validateFile = (req, file, cb) => { + const { fileType } = req.body + const { mimetype } = file + + const valid = Joi.validate({ [fileType]: mimetype }, uploadValidations) + + if (valid.error) { + req.fileValidationError = valid.error.message + return cb(null, false) + } + + return cb(null, true) +} + +module.exports = { + setupMulter, + validateFile, +} diff --git a/packages/component-local-aws/src/routeHandlers/deleteFile.js b/packages/component-local-aws/src/routeHandlers/deleteFile.js new file mode 100644 index 0000000000000000000000000000000000000000..9aa8974b76a053f0ea4229464c1f8ec9830c8fa3 --- /dev/null +++ b/packages/component-local-aws/src/routeHandlers/deleteFile.js @@ -0,0 +1,22 @@ +const { promisify } = require('util') +const logger = require('@pubsweet/logger') + +const deleteFile = (s3, s3Config) => { + const asyncDeleteObject = promisify(s3.deleteObject.bind(s3)) + return async (req, res) => { + const params = { + Bucket: s3Config.bucket, + Key: `${req.params.fragmentId}/${req.params.fileId}`, + } + + try { + await asyncDeleteObject(params) + res.status(204) + } catch (err) { + logger.error(err.message) + res.status(err.statusCode).json({ error: err.message }) + } + } +} + +module.exports = deleteFile diff --git a/packages/component-local-aws/src/routeHandlers/getSignedUrl.js b/packages/component-local-aws/src/routeHandlers/getSignedUrl.js new file mode 100644 index 0000000000000000000000000000000000000000..edbad710242df2428920f69405c4313bd9f6c452 --- /dev/null +++ b/packages/component-local-aws/src/routeHandlers/getSignedUrl.js @@ -0,0 +1,24 @@ +const { promisify } = require('util') +const logger = require('@pubsweet/logger') + +const getSignedUrl = (s3, s3Config) => { + const asyncGetSignedUrl = promisify(s3.getSignedUrl.bind(s3)) + return async (req, res) => { + const params = { + Bucket: s3Config.bucket, + Key: `${req.params.fragmentId}/${req.params.fileId}`, + } + + try { + const signedUrl = await asyncGetSignedUrl(params) + res.status(200).json({ + signedUrl, + }) + } catch (err) { + logger.error(err.message) + res.status(err.statusCode).json({ error: err.message }) + } + } +} + +module.exports = getSignedUrl diff --git a/packages/component-local-aws/src/routeHandlers/postFile.js b/packages/component-local-aws/src/routeHandlers/postFile.js new file mode 100644 index 0000000000000000000000000000000000000000..228c78fed2d5914cf1134453255053d7524c63d0 --- /dev/null +++ b/packages/component-local-aws/src/routeHandlers/postFile.js @@ -0,0 +1,15 @@ +const logger = require('@pubsweet/logger') + +module.exports = async (req, res) => { + if (req.fileValidationError !== undefined) { + logger.error(req.fileValidationError) + return res.status(400).json({ error: req.fileValidationError }) + } + logger.debug(`${req.file.originalname} has been uploaded`) + + return res.status(200).json({ + id: req.file.key, + name: req.file.originalname, + size: req.file.size, + }) +} diff --git a/packages/component-local-aws/src/routeHandlers/zipFiles.js b/packages/component-local-aws/src/routeHandlers/zipFiles.js new file mode 100644 index 0000000000000000000000000000000000000000..272eccbfb8e93b7a96b6c573259c047d613f0a06 --- /dev/null +++ b/packages/component-local-aws/src/routeHandlers/zipFiles.js @@ -0,0 +1,55 @@ +const { get } = require('lodash') +const { promisify } = require('util') +const nodeArchiver = require('archiver') +const logger = require('@pubsweet/logger') + +const zipFiles = (s3, s3Config, archiver = nodeArchiver) => { + const asyncGetObject = promisify(s3.getObject.bind(s3)) + const asyncListObjects = promisify(s3.listObjects.bind(s3)) + return async (req, res) => { + const { fragmentId } = req.params + try { + const params = { + Bucket: s3Config.bucket, + Prefix: `${fragmentId}`, + } + const s3Items = await asyncListObjects(params) + + if (s3Items) { + const s3Files = await Promise.all( + s3Items.Contents.map(content => + asyncGetObject({ + Bucket: s3Config.bucket, + Key: content.Key, + }), + ), + ) + + if (s3Files) { + const archive = archiver('zip') + archive.pipe(res) + res.attachment(`${fragmentId}-archive.zip`) + + s3Files.forEach(f => { + archive.append(f.Body, { + name: `${get(f, 'Metadata.filetype') || 'supplementary'}/${get( + f, + 'Metadata.filename', + ) || f.ETag}`, + }) + }) + archive.finalize() + } + } else { + res.status(204).json({ + message: `No files found for the requested manuscript.`, + }) + } + } catch (err) { + logger.error(err.message) + res.status(err.statusCode).json({ error: err.message }) + } + } +} + +module.exports = zipFiles diff --git a/packages/components-faraday/src/components/AuthorList/AuthorAdder.js b/packages/components-faraday/src/components/AuthorList/AuthorAdder.js index eccca432675ccaadea8061465644fc4afe690571..2cb1d3258fbd47afa1004fa0fb6af36b29a46314 100644 --- a/packages/components-faraday/src/components/AuthorList/AuthorAdder.js +++ b/packages/components-faraday/src/components/AuthorList/AuthorAdder.js @@ -27,7 +27,7 @@ const AuthorAdder = ({ isFetching, }) => ( <Root> - <Button onClick={setEditMode(true)} primary> + <Button data-test="button-add-author" onClick={setEditMode(true)} primary> {authors.length === 0 ? '+ Add submitting author' : '+ Add author'} </Button> {editMode && ( @@ -54,9 +54,15 @@ const AuthorAdder = ({ <MenuItem label="Country*" name="country" options={countries} /> </Row> <ButtonsContainer> - <Button onClick={setEditMode(false)}>Cancel</Button> + <Button data-test="button-cancel-author" onClick={setEditMode(false)}> + Cancel + </Button> {!isFetching ? ( - <Button onClick={handleSubmit} primary> + <Button + data-test="button-save-author" + onClick={handleSubmit} + primary + > Save </Button> ) : ( diff --git a/packages/components-faraday/src/components/AuthorList/FormItems.js b/packages/components-faraday/src/components/AuthorList/FormItems.js index 807a34fe4cd3103414ecfb16871e67a9ad9851bc..96040013537ca947984c8151894f1e09fa4848e0 100644 --- a/packages/components-faraday/src/components/AuthorList/FormItems.js +++ b/packages/components-faraday/src/components/AuthorList/FormItems.js @@ -19,7 +19,7 @@ export const ValidatedTextField = ({ } export const MenuItem = ({ label, name, options }) => ( - <MenuItemRoot> + <MenuItemRoot data-test="country-selector-author"> <StyledLabel>{label}</StyledLabel> <ValidatedField component={input => <Menu {...input} options={options} />} diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index 7e5dd322fd9ad1d535bcd19ca395381a77d92e5f..af8cd34a48f317f033810fc98d953c6a40cb9404 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -33,7 +33,6 @@ const DashboardCard = ({ const manuscriptMeta = `${type} - ${ journalIssueType ? journalIssueType.label : 'N/A' }` - return version ? ( <Card data-test={customId}> <ListView> @@ -46,7 +45,11 @@ const DashboardCard = ({ /> </LeftDetails> <RightDetails flex="2"> - <ZipFiles disabled={!hasFiles} fragmentId={version.id}> + <ZipFiles + archiveName={`ID-${project.customId}`} + disabled={!hasFiles} + fragmentId={version.id} + > <ClickableIcon disabled={!hasFiles}> <Icon>download</Icon> </ClickableIcon> diff --git a/packages/components-faraday/src/components/Dashboard/ZipFiles.js b/packages/components-faraday/src/components/Dashboard/ZipFiles.js index 8f6e9ece1807e90e2ffa9ed214c47ed49e3c99e2..12f38e9a77e1e8a6c69e8ee3d469b859c494dcd6 100644 --- a/packages/components-faraday/src/components/Dashboard/ZipFiles.js +++ b/packages/components-faraday/src/components/Dashboard/ZipFiles.js @@ -45,7 +45,14 @@ const Zip = compose( })), withState('fetching', 'setFetching', false), withHandlers({ - downloadFiles: ({ fragmentId, token, setFetching, archiveName }) => () => { + downloadFiles: ({ + fragmentId, + collectionId, + token, + setFetching, + archiveName, + }) => () => { + const getUrl = `${window.location.origin}/api/files/${fragmentId}` if (cache[fragmentId]) { const fileName = archiveName || `${fragmentId}-archive.zip` @@ -71,7 +78,7 @@ const Zip = compose( } } } - xhr.open('GET', `${window.location.origin}/api/fileZip/${fragmentId}`) + xhr.open('GET', getUrl) xhr.responseType = 'blob' xhr.setRequestHeader('Authorization', `Bearer ${token}`) xhr.send() diff --git a/packages/components-faraday/src/components/Files/FileSection.js b/packages/components-faraday/src/components/Files/FileSection.js index 453006fd28b79946f691ea5b8ef8140537731d9d..88459761fc6e566bb217373fd84d176ff2153c0b 100644 --- a/packages/components-faraday/src/components/Files/FileSection.js +++ b/packages/components-faraday/src/components/Files/FileSection.js @@ -65,7 +65,10 @@ const FileSection = ({ disabled={disabledFilepicker()} onUpload={addFile} > - <UploadButton disabled={disabledFilepicker()}> + <UploadButton + data-test={`button-upload-${listId}`} + disabled={disabledFilepicker()} + > <Icon color={ disabledFilepicker() diff --git a/packages/components-faraday/src/redux/files.js b/packages/components-faraday/src/redux/files.js index e1ba5b9d3766f3dc1992671a15fa460c9e2d1bba..0013b437ea13f916448b563abcc2133a6c6b665d 100644 --- a/packages/components-faraday/src/redux/files.js +++ b/packages/components-faraday/src/redux/files.js @@ -68,7 +68,7 @@ export const getFileError = state => state.files.error // thunked actions export const uploadFile = (file, type, fragmentId) => dispatch => { dispatch(uploadRequest(type)) - return request('/file', createFileData(file, type, fragmentId)).then( + return request('/files', createFileData(file, type, fragmentId)).then( r => { dispatch(uploadSuccess()) return r @@ -82,7 +82,7 @@ export const uploadFile = (file, type, fragmentId) => dispatch => { export const deleteFile = fileId => dispatch => { dispatch(removeRequest()) - return remove(`/file/${fileId}`) + return remove(`/files/${fileId}`) .then(r => { dispatch(removeSuccess()) return r diff --git a/packages/xpub-faraday/config/components.json b/packages/xpub-faraday/config/components.json index 8c449cd1a00c8a703c2a0a55cced468541dcbb92..7a1555ce9bc5d36ac95e3eca4e33f466e66511e4 100644 --- a/packages/xpub-faraday/config/components.json +++ b/packages/xpub-faraday/config/components.json @@ -7,5 +7,6 @@ "pubsweet-components-faraday", "@pubsweet/component-aws-s3", "pubsweet-component-invite", - "component-aws-download" + "component-aws-download", + "component-local-aws" ] diff --git a/yarn.lock b/yarn.lock index e24936ff287cce17c86c68a7597c79d9a3443183..69e6ba522167896f8da8df04186b054e218c2ccb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7210,10 +7210,6 @@ pkginfo@0.3.x: version "0.3.1" resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" -pkginfo@0.x.x: - version "0.4.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" - platform@1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" @@ -7909,18 +7905,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prompt@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prompt/-/prompt-1.0.0.tgz#8e57123c396ab988897fb327fd3aedc3e735e4fe" - dependencies: - colors "^1.1.2" - pkginfo "0.x.x" - read "1.0.x" - revalidator "0.1.x" - utile "0.3.x" - winston "2.1.x" - -prompt@flatiron/prompt#1c95d1d8d333b5fbc13fa5f0619f3dcf0d514f87: +prompt@^1.0.0, prompt@flatiron/prompt#1c95d1d8d333b5fbc13fa5f0619f3dcf0d514f87: version "1.0.0" resolved "https://codeload.github.com/flatiron/prompt/tar.gz/1c95d1d8d333b5fbc13fa5f0619f3dcf0d514f87" dependencies: @@ -10669,18 +10654,6 @@ winston@0.8.x: pkginfo "0.3.x" stack-trace "0.0.x" -winston@2.1.x: - version "2.1.1" - resolved "https://registry.yarnpkg.com/winston/-/winston-2.1.1.tgz#3c9349d196207fd1bdff9d4bc43ef72510e3a12e" - dependencies: - async "~1.0.0" - colors "1.0.x" - cycle "1.0.x" - eyes "0.1.x" - isstream "0.1.x" - pkginfo "0.3.x" - stack-trace "0.0.x" - winston@2.x, winston@^2.2.0, winston@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.0.tgz#808050b93d52661ed9fb6c26b3f0c826708b0aee"